diff --git a/README.md b/README.md index 662ba0589..961f95327 100644 --- a/README.md +++ b/README.md @@ -128,7 +128,7 @@ Is this a new installation? Initialize the database: Is this for local development? Bootstrap the database with dummy data and users: ``` -(venv) $ python src/manage.py bootstrap_devsite +(venv) $ python src/manage.py bootstrap_devsite [--threads ] [--years ] [--writable-years ] [--verbosity <{0,1,2,3}>] ``` This creates some user accounts. Run the following command to see their email @@ -201,6 +201,7 @@ Then go to the admin interface and add the camp. ## Contributors * Alexander Færøy https://github.com/ahf * Benjamin Bach https://github.com/benjaoming +* Christian Henriksen https://github.com/0xunicorn * coral https://github.com/coral * Flemming Jacobsen https://github.com/batmule * Florian Klink https://github.com/flokli diff --git a/src/bornhack/settings.py b/src/bornhack/settings.py index d557f09d5..c48cde5ce 100644 --- a/src/bornhack/settings.py +++ b/src/bornhack/settings.py @@ -251,9 +251,6 @@ "OAUTH2_VALIDATOR_CLASS": "bornhack.oauth_validators.BornhackOAuth2Validator", } -# only used for bootstrap-devsite -UPCOMING_CAMP_YEAR = 2031 - # django-tables2 settings DJANGO_TABLES2_TEMPLATE = "django_tables2/bootstrap5-responsive.html" DJANGO_TABLES2_TABLE_ATTRS = { diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py index e52db4551..285e43839 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -6,9 +6,11 @@ import random import sys import uuid -from zoneinfo import ZoneInfo +from collections import defaultdict from datetime import datetime from datetime import timedelta +from itertools import chain +from zoneinfo import ZoneInfo import factory from allauth.account.models import EmailAddress @@ -22,7 +24,9 @@ from django.core.exceptions import ValidationError from django.utils import timezone from django.utils.crypto import get_random_string +from django.utils.text import slugify from faker import Faker +from psycopg2.extras import DateTimeTZRange from camps.models import Camp from camps.models import Permission as CampPermission @@ -45,9 +49,9 @@ from economy.models import Chain from economy.models import Credebtor from economy.models import Expense -from economy.models import Revenue from economy.models import Pos from economy.models import Reimbursement +from economy.models import Revenue from events.factories import EventProposalFactory from events.factories import EventProposalUrlFactory from events.factories import SpeakerProposalFactory @@ -95,7 +99,6 @@ from tokens.models import Token from tokens.models import TokenCategory from tokens.models import TokenFind -from utils.slugs import unique_slugify from villages.models import Village from .functions import output_fake_md_description @@ -103,6 +106,24 @@ fake = Faker() tz = ZoneInfo(settings.TIME_ZONE) logger = logging.getLogger(f"bornhack.{__name__}") +CAMP_MAP = { + 2016: {"colour": "#004dff", "tagline": "Initial Commit"}, + 2017: {"colour": "#750787", "tagline": "Make Tradition"}, + 2018: {"colour": "#008026", "tagline": "scale it"}, + 2019: {"colour": "#ffed00", "tagline": "a new /home", "light_text": False}, + 2020: {"colour": "#ff8c00", "tagline": "Make Clean"}, + 2021: {"colour": "#e40303", "tagline": "Continuous Delivery"}, + 2022: {"colour": "#000000", "tagline": "black ~/hack"}, + 2023: {"colour": "#613915", "tagline": "make legacy"}, + 2024: {"colour": "#73d7ee", "tagline": "Feature Creep", "light_text": False}, + 2025: {"colour": "#ffafc7", "tagline": "10 Badges", "light_text": False}, + 2026: {"colour": "#ffffff", "tagline": "Undecided", "light_text": False}, + 2027: {"colour": "#004dff", "tagline": "Undecided"}, + 2028: {"colour": "#750787", "tagline": "Undecided"}, + 2029: {"colour": "#008026", "tagline": "Undecided"}, + 2030: {"colour": "#ffed00", "tagline": "Undecided", "light_text": False}, + 2031: {"colour": "#ff8c00", "tagline": "Undecided"}, +} class Bootstrap: @@ -116,49 +137,51 @@ class Bootstrap: product_categories: dict quickfeedback_options: dict - def create_camps(self, camps: dict) -> None: - """Creates all camps from a dict of camps.""" + def create_camps(self, camps_list: list) -> list[Camp]: + """Create camps from a list.""" self.output("Creating camps...") camp_instances = [] - - for camp in camps: - year = camp["year"] - read_only = camp["read_only"] - camp_instances.append( - ( - Camp.objects.create( - title=f"BornHack {year}", - tagline=camp["tagline"], - slug=f"bornhack-{year}", - shortslug=f"bornhack-{year}", - buildup=( - datetime(year, 8, 25, 12, 0, tzinfo=tz), - datetime(year, 8, 27, 12, 0, tzinfo=tz), - ), - camp=( - datetime(year, 8, 27, 12, 0, tzinfo=tz), - datetime(year, 9, 3, 12, 0, tzinfo=tz), - ), - teardown=( - datetime(year, 9, 3, 12, 0, tzinfo=tz), - datetime(year, 9, 5, 12, 0, tzinfo=tz), - ), - colour=camp["colour"], - light_text=camp.get("light_text", True), - ), - read_only, + for data in camps_list: + year = data["year"] + read_only = data["read_only"] + + camp = Camp( + title=f"BornHack {year}", + tagline=data["tagline"], + slug=f"bornhack-{year}", + shortslug=f"bornhack-{year}", + call_for_participation_open=(not read_only), + call_for_sponsors_open=(not read_only), + buildup=DateTimeTZRange( + datetime(year, 8, 25, 12, 0, tzinfo=tz), + datetime(year, 8, 27, 12, 0, tzinfo=tz), ), + camp=DateTimeTZRange( + datetime(year, 8, 27, 12, 0, tzinfo=tz), + datetime(year, 9, 3, 12, 0, tzinfo=tz), + ), + teardown=DateTimeTZRange( + datetime(year, 9, 3, 12, 0, tzinfo=tz), + datetime(year, 9, 5, 12, 0, tzinfo=tz), + ), + colour=data["colour"], + light_text=data.get("light_text", True), ) + camp_instances.append(camp) + + Camp.objects.bulk_create(camp_instances) self.camps = camp_instances + return camp_instances + def create_event_routing_types(self) -> None: """Create event routing types.""" t, created = Type.objects.get_or_create(name="public_credit_name_changed") t, created = Type.objects.get_or_create(name="ticket_created") - def create_users(self, amount: int) -> None: + def create_users(self, amount: int = 16) -> None: """Create users.""" self.output("Creating users...") @@ -301,42 +324,43 @@ def create_facilities(self, facility_types: dict) -> dict: """Create facilities.""" facilities = {} self.output("Creating facilities...") - facilities["toilet1"] = Facility.objects.create( + facilities["toilet1"] = Facility( facility_type=facility_types["toilet"], name="Toilet NOC East", description="Toilet on the east side of the NOC building", location=Point(9.939783, 55.387217), ) - facilities["toilet2"] = Facility.objects.create( + facilities["toilet2"] = Facility( facility_type=facility_types["toilet"], name="Toilet NOC West", description="Toilet on the west side of the NOC building", location=Point(9.93967, 55.387197), ) - facilities["pdp1"] = Facility.objects.create( + facilities["pdp1"] = Facility( facility_type=facility_types["power"], name="PDP1", description="In orga area", location=Point(9.94079, 55.388022), ) - facilities["pdp2"] = Facility.objects.create( + facilities["pdp2"] = Facility( facility_type=facility_types["power"], name="PDP2", description="In bar area", location=Point(9.942036, 55.387891), ) - facilities["pdp3"] = Facility.objects.create( + facilities["pdp3"] = Facility( facility_type=facility_types["power"], name="PDP3", description="In speaker tent", location=Point(9.938416, 55.387109), ) - facilities["pdp4"] = Facility.objects.create( + facilities["pdp4"] = Facility( facility_type=facility_types["power"], name="PDP4", description="In food area", location=Point(9.940146, 55.386983), ) + Facility.objects.bulk_create(facilities.values()) return facilities def create_facility_feedbacks( @@ -347,44 +371,47 @@ def create_facility_feedbacks( ) -> None: """Create facility feedbacks.""" self.output("Creating facility feedbacks...") - FacilityFeedback.objects.create( - user=users[1], - facility=facilities["toilet1"], - quick_feedback=options["attention"], - comment="Something smells wrong", - urgent=True, - ) - FacilityFeedback.objects.create( - user=users[2], - facility=facilities["toilet1"], - quick_feedback=options["toiletpaper"], - urgent=False, - ) - FacilityFeedback.objects.create( - facility=facilities["toilet2"], - quick_feedback=options["cleaning"], - comment="This place needs cleaning please. Anonymous feedback.", - urgent=False, - ) - FacilityFeedback.objects.create( - facility=facilities["pdp1"], - quick_feedback=options["attention"], - comment="Rain cover needs some work, and we need more free plugs! This feedback is submitted anonymously.", - urgent=False, - ) - FacilityFeedback.objects.create( - user=users[5], - facility=facilities["pdp2"], - quick_feedback=options["power"], - comment="No power, please help", - urgent=True, - ) + feedback = [ + FacilityFeedback( + user=users[1], + facility=facilities["toilet1"], + quick_feedback=options["attention"], + comment="Something smells wrong", + urgent=True, + ), + FacilityFeedback( + user=users[2], + facility=facilities["toilet1"], + quick_feedback=options["toiletpaper"], + urgent=False, + ), + FacilityFeedback( + facility=facilities["toilet2"], + quick_feedback=options["cleaning"], + comment="This place needs cleaning please. Anonymous feedback.", + urgent=False, + ), + FacilityFeedback( + facility=facilities["pdp1"], + quick_feedback=options["attention"], + comment="Rain cover needs some work, and we need more free plugs! This feedback is submitted anonymously.", + urgent=False, + ), + FacilityFeedback( + user=users[5], + facility=facilities["pdp2"], + quick_feedback=options["power"], + comment="No power, please help", + urgent=True, + ), + ] + FacilityFeedback.objects.bulk_create(feedback) def create_event_types(self) -> None: """Create event types.""" types = {} self.output("Creating event types...") - types["workshop"] = EventType.objects.create( + types["workshop"] = EventType( name="Workshop", slug="workshop", color="#ff9900", @@ -398,7 +425,7 @@ def create_event_types(self) -> None: support_speaker_event_conflicts=True, ) - types["talk"] = EventType.objects.create( + types["talk"] = EventType( name="Talk", slug="talk", color="#2D9595", @@ -412,7 +439,7 @@ def create_event_types(self) -> None: support_speaker_event_conflicts=True, ) - types["lightning"] = EventType.objects.create( + types["lightning"] = EventType( name="Lightning Talk", slug="lightning-talk", color="#ff0000", @@ -425,7 +452,7 @@ def create_event_types(self) -> None: support_speaker_event_conflicts=True, ) - types["music"] = EventType.objects.create( + types["music"] = EventType( name="Music Act", slug="music", color="#1D0095", @@ -439,7 +466,7 @@ def create_event_types(self) -> None: support_speaker_event_conflicts=True, ) - types["keynote"] = EventType.objects.create( + types["keynote"] = EventType( name="Keynote", slug="keynote", color="#FF3453", @@ -452,7 +479,7 @@ def create_event_types(self) -> None: support_speaker_event_conflicts=True, ) - types["debate"] = EventType.objects.create( + types["debate"] = EventType( name="Debate", slug="debate", color="#F734C3", @@ -466,7 +493,7 @@ def create_event_types(self) -> None: support_speaker_event_conflicts=True, ) - types["facility"] = EventType.objects.create( + types["facility"] = EventType( name="Facilities", slug="facilities", color="#cccccc", @@ -479,7 +506,7 @@ def create_event_types(self) -> None: support_speaker_event_conflicts=False, ) - types["recreational"] = EventType.objects.create( + types["recreational"] = EventType( name="Recreational Event", slug="recreational-event", color="#0000ff", @@ -493,6 +520,8 @@ def create_event_types(self) -> None: support_speaker_event_conflicts=True, ) + EventType.objects.bulk_create(types.values()) + self.event_types = types def create_url_types(self) -> None: @@ -573,75 +602,87 @@ def create_product_categories(self) -> None: """Create product categories.""" categories = {} self.output("Creating productcategories...") - categories["transportation"] = ProductCategory.objects.create( + categories["transportation"] = ProductCategory( name="Transportation", slug="transportation", ) - categories["merchandise"] = ProductCategory.objects.create( + categories["merchandise"] = ProductCategory( name="Merchandise", slug="merchandise", ) - categories["tickets"] = ProductCategory.objects.create( + categories["tickets"] = ProductCategory( name="Tickets", slug="tickets", ) - categories["villages"] = ProductCategory.objects.create( + categories["villages"] = ProductCategory( name="Villages", slug="villages", ) - categories["facilities"] = ProductCategory.objects.create( + categories["facilities"] = ProductCategory( name="Facilities", slug="facilities", ) - categories["packages"] = ProductCategory.objects.create( + categories["packages"] = ProductCategory( name="Packages", slug="packages", ) + ProductCategory.objects.bulk_create(categories.values()) self.product_categories = categories def create_camp_ticket_types(self, camp: Camp) -> dict: """Create camp ticket types.""" types = {} self.output(f"Creating tickettypes for {camp.year}...") - types["adult_full_week"] = TicketType.objects.create( + types["adult_full_week"] = TicketType( name="Adult Full Week", camp=camp, ) - camp.ticket_type_full_week_adult = types["adult_full_week"] - types["adult_one_day"] = TicketType.objects.create( + types["adult_one_day"] = TicketType( name="Adult One Day", camp=camp, ) - camp.ticket_type_one_day_adult = types["adult_one_day"] - types["child_full_week"] = TicketType.objects.create( + types["child_full_week"] = TicketType( name="Child Full Week", camp=camp, ) - camp.ticket_type_full_week_child = types["child_full_week"] - types["child_one_day"] = TicketType.objects.create( + types["child_one_day"] = TicketType( name="Child One Day", camp=camp, ) - camp.ticket_type_one_day_child = types["child_one_day"] - types["village"] = TicketType.objects.create( + types["village"] = TicketType( name="Village", camp=camp, ) - types["merchandise"] = TicketType.objects.create( + types["merchandise"] = TicketType( name="Merchandise", camp=camp, ) - types["facilities"] = TicketType.objects.create( + types["facilities"] = TicketType( name="Facilities", camp=camp, single_ticket_per_product=True, ) - types["transportation"] = TicketType.objects.create( + types["transportation"] = TicketType( name="Transportation", camp=camp, ) + TicketType.objects.bulk_create(types.values()) + + camp.ticket_type_full_week_adult = types["adult_full_week"] + camp.ticket_type_one_day_adult = types["adult_one_day"] + camp.ticket_type_full_week_child = types["child_full_week"] + camp.ticket_type_one_day_child = types["child_one_day"] + camp.save( + update_fields=[ + "ticket_type_full_week_adult", + "ticket_type_one_day_adult", + "ticket_type_full_week_child", + "ticket_type_one_day_child", + ], + ) + return types def create_camp_products( @@ -655,7 +696,7 @@ def create_camp_products( camp_prefix = f"BornHack {camp.year}" name = f"{camp_prefix} Standard ticket" - products["ticket1"] = Product.objects.create( + products["ticket1"] = Product( name=name, description="A ticket", price=1200, @@ -669,7 +710,7 @@ def create_camp_products( ) name = f"{camp_prefix} Hacker ticket" - products["ticket2"] = Product.objects.create( + products["ticket2"] = Product( name=name, description="Another ticket", price=1337, @@ -683,7 +724,7 @@ def create_camp_products( ) name = f"{camp_prefix} Child Ticket (5-15 year old)" - products["child_ticket"] = Product.objects.create( + products["child_ticket"] = Product( name=name, description="A child ticket", price=495, @@ -697,7 +738,7 @@ def create_camp_products( ) name = f"{camp_prefix} One day ticket" - products["one_day_ticket"] = Product.objects.create( + products["one_day_ticket"] = Product( name=name, description="One day ticket", price=300, @@ -711,7 +752,7 @@ def create_camp_products( ) name = f"{camp_prefix} One day ticket child" - products["one_day_ticket_child"] = Product.objects.create( + products["one_day_ticket_child"] = Product( name=name, description="One day ticket child", price=165, @@ -725,7 +766,7 @@ def create_camp_products( ) name = f"{camp_prefix} Village tent 3x3 meters, no floor" - products["tent1"] = Product.objects.create( + products["tent1"] = Product( name=name, description="A description of the tent goes here", price=3325, @@ -739,7 +780,7 @@ def create_camp_products( ) name = f"{camp_prefix} Village tent 3x3 meters, with floor" - products["tent2"] = Product.objects.create( + products["tent2"] = Product( name=name, description="A description of the tent goes here", price=3675, @@ -753,7 +794,7 @@ def create_camp_products( ) name = f"{camp_prefix} T-shirt Large" - products["t-shirt-large"] = Product.objects.create( + products["t-shirt-large"] = Product( name=name, description="A description of the t-shirt goes here", price=150, @@ -767,7 +808,7 @@ def create_camp_products( ) name = f"{camp_prefix} T-shirt Medium" - products["t-shirt-medium"] = Product.objects.create( + products["t-shirt-medium"] = Product( name=name, description="A description of the t-shirt goes here", price=150, @@ -781,7 +822,7 @@ def create_camp_products( ) name = f"{camp_prefix} T-shirt Small" - products["t-shirt-small"] = Product.objects.create( + products["t-shirt-small"] = Product( name=name, description="A description of the t-shirt goes here", price=150, @@ -795,7 +836,7 @@ def create_camp_products( ) name = "100 HAX" - products["hax"] = Product.objects.create( + products["hax"] = Product( name=name, description="100 HAX", price=100, @@ -809,7 +850,7 @@ def create_camp_products( ) name = "Corporate Hackers Small" - products["corporate_hackers_small"] = Product.objects.create( + products["corporate_hackers_small"] = Product( name=name, description="Send your company to BornHack in style with one of our corporate packages!", price=18000, @@ -820,6 +861,9 @@ def create_camp_products( ), slug=f"{camp.slug}-corporate-hackers-small", ) + + Product.objects.bulk_create(products.values()) + products["corporate_hackers_small"].sub_products.add( products["ticket1"], through_defaults={ @@ -930,49 +974,49 @@ def create_event_locations(self, camp: Camp) -> dict: """Create all event locations.""" locations = {} self.output(f"Creating event_locations for {camp.year}...") - locations["speakers_tent"] = EventLocation.objects.create( + locations["speakers_tent"] = EventLocation( name="Speakers Tent", slug="speakers-tent", icon="comment", camp=camp, capacity=150, ) - locations["workshop_room_1"] = EventLocation.objects.create( + locations["workshop_room_1"] = EventLocation( name="Workshop room 1 (big)", slug="workshop-room-1", icon="briefcase", camp=camp, capacity=50, ) - locations["workshop_room_2"] = EventLocation.objects.create( + locations["workshop_room_2"] = EventLocation( name="Workshop room 2 (small)", slug="workshop-room-2", icon="briefcase", camp=camp, capacity=25, ) - locations["workshop_room_3"] = EventLocation.objects.create( + locations["workshop_room_3"] = EventLocation( name="Workshop room 3 (small)", slug="workshop-room-3", icon="briefcase", camp=camp, capacity=25, ) - locations["bar_area"] = EventLocation.objects.create( + locations["bar_area"] = EventLocation( name="Bar Area", slug="bar-area", icon="glass-cheers", camp=camp, capacity=50, ) - locations["food_area"] = EventLocation.objects.create( + locations["food_area"] = EventLocation( name="Food Area", slug="food-area", icon="utensils", camp=camp, capacity=50, ) - locations["infodesk"] = EventLocation.objects.create( + locations["infodesk"] = EventLocation( name="Infodesk", slug="infodesk", icon="info", @@ -980,6 +1024,8 @@ def create_event_locations(self, camp: Camp) -> dict: capacity=20, ) + EventLocation.objects.bulk_create(locations.values()) + # add workshop room conflicts (the big root can not be used while either # of the small rooms are in use, and vice versa) locations["workshop_room_1"].conflicts.add(locations["workshop_room_2"]) @@ -990,16 +1036,19 @@ def create_event_locations(self, camp: Camp) -> dict: def create_camp_news(self, camp: Camp) -> None: """Create camp news.""" self.output(f"Creating news for {camp.year}...") - NewsItem.objects.create( - title=f"Welcome to {camp.title}", - content="news body here with html support", - published_at=datetime(camp.year, 8, 27, 12, 0, tzinfo=tz), - ) - NewsItem.objects.create( - title=f"{camp.title} is over", - content="news body here", - published_at=datetime(camp.year, 9, 4, 12, 0, tzinfo=tz), - ) + news = [ + NewsItem( + title=f"Welcome to {camp.title}", + content="news body here with html support", + published_at=datetime(camp.year, 8, 27, 12, 0, tzinfo=tz), + ), + NewsItem( + title=f"{camp.title} is over", + content="news body here", + published_at=datetime(camp.year, 9, 4, 12, 0, tzinfo=tz), + ), + ] + NewsItem.objects.bulk_create(news) def create_camp_event_sessions( self, @@ -1016,9 +1065,10 @@ def create_camp_event_sessions( camp=camp, event_type=event_types["talk"], event_location=event_locations["speakers_tent"], + event_duration_minutes=60, when=( datetime(start.year, start.month, start.day, 11, 0, tzinfo=tz), - datetime(start.year, start.month, start.day, 18, 0, tzinfo=tz), + datetime(start.year, start.month, start.day, 12, 0, tzinfo=tz), ), ) EventSession.objects.create( @@ -1035,6 +1085,7 @@ def create_camp_event_sessions( camp=camp, event_type=event_types["music"], event_location=event_locations["bar_area"], + event_duration_minutes=180, when=( datetime(start.year, start.month, start.day, 22, 0, tzinfo=tz), datetime(start.year, start.month, start.day, 22, 0, tzinfo=tz) + timedelta(hours=3), @@ -1044,6 +1095,7 @@ def create_camp_event_sessions( camp=camp, event_type=event_types["workshop"], event_location=event_locations["workshop_room_1"], + event_duration_minutes=360, when=( datetime(start.year, start.month, start.day, 12, 0, tzinfo=tz), datetime(start.year, start.month, start.day, 18, 0, tzinfo=tz), @@ -1053,6 +1105,7 @@ def create_camp_event_sessions( camp=camp, event_type=event_types["workshop"], event_location=event_locations["workshop_room_2"], + event_duration_minutes=360, when=( datetime(start.year, start.month, start.day, 12, 0, tzinfo=tz), datetime(start.year, start.month, start.day, 18, 0, tzinfo=tz), @@ -1062,6 +1115,7 @@ def create_camp_event_sessions( camp=camp, event_type=event_types["workshop"], event_location=event_locations["workshop_room_3"], + event_duration_minutes=360, when=( datetime(start.year, start.month, start.day, 12, 0, tzinfo=tz), datetime(start.year, start.month, start.day, 18, 0, tzinfo=tz), @@ -1073,6 +1127,7 @@ def create_camp_event_sessions( camp=camp, event_type=event_types["keynote"], event_location=event_locations["speakers_tent"], + event_duration_minutes=90, when=( datetime( day.lower.year, @@ -1324,37 +1379,40 @@ def create_camp_rescheduling(self, camp: Camp, autoschedule: bool) -> None: def create_camp_villages(self, camp: Camp, users: dict) -> None: """Create camp villages.""" self.output(f"Creating villages for {camp.year}...") - Village.objects.create( - contact=users[1], - camp=camp, - name="Baconsvin", - slug="baconsvin", - approved=True, - location=Point(9.9401295, 55.3881695), - description="The camp with the doorbell-pig! Baconsvin is a group of happy people from Denmark " - "doing a lot of open source, and are always happy to talk about infosec, hacking, BSD, and much more. " - "A lot of the organizers of BornHack live in Baconsvin village. " - "Come by and squeeze the pig and sign our guestbook!", - ) - Village.objects.create( - contact=users[2], - camp=camp, - name="NetworkWarriors", - slug="networkwarriors", - approved=True, - description="We will have a tent which house the NOC people, various lab equipment people " - "can play with, and have fun. If you want to talk about networking, come by, and if you have " - "trouble with the Bornhack network contact us.", - ) - Village.objects.create( - contact=users[3], - camp=camp, - name="TheCamp.dk", - slug="the-camp", - description="This village is representing TheCamp.dk, an annual danish tech camp held in July. " - "The official subjects for this event is open source software, network and security. " - "In reality we are interested in anything from computers to illumination soap bubbles and irish coffee", - ) + villages = [ + Village( + contact=users[1], + camp=camp, + name="Baconsvin", + slug="baconsvin", + approved=True, + location=Point(9.9401295, 55.3881695), + description="The camp with the doorbell-pig! Baconsvin is a group of happy people from Denmark " + "doing a lot of open source, and are always happy to talk about infosec, hacking, BSD, and much more. " + "A lot of the organizers of BornHack live in Baconsvin village. " + "Come by and squeeze the pig and sign our guestbook!", + ), + Village( + contact=users[2], + camp=camp, + name="NetworkWarriors", + slug="networkwarriors", + approved=True, + description="We will have a tent which house the NOC people, various lab equipment people " + "can play with, and have fun. If you want to talk about networking, come by, and if you have " + "trouble with the Bornhack network contact us.", + ), + Village( + contact=users[3], + camp=camp, + name="TheCamp.dk", + slug="the-camp", + description="This village is representing TheCamp.dk, an annual danish tech camp held in July. " + "The official subjects for this event is open source software, network and security. " + "In reality we are interested in anything from computers to illumination soap bubbles and irish coffee", + ), + ] + Village.objects.bulk_create(villages) def create_camp_teams(self, camp: Camp) -> dict: """Create camp teams.""" @@ -1431,51 +1489,64 @@ def create_camp_teams(self, camp: Camp) -> dict: def create_camp_team_tasks(self, camp: Camp, teams: dict) -> None: """Create camp team tasks.""" self.output(f"Creating TeamTasks for {camp.year}...") - TeamTask.objects.create( - team=teams["noc"], - name="Setup private networks", - description="All the private networks need to be setup", - ) - TeamTask.objects.create( - team=teams["noc"], - name="Setup public networks", - description="All the public networks need to be setup", - ) - TeamTask.objects.create( - team=teams["noc"], - name="Deploy access points", - description="All access points need to be deployed", - ) - TeamTask.objects.create( - team=teams["noc"], - name="Deploy fiber cables", - description="We need the fiber deployed where necessary", - ) - TeamTask.objects.create( - team=teams["bar"], - name="List of booze", - description="A list of the different booze we need to have in the bar durng bornhack", - ) - TeamTask.objects.create( - team=teams["bar"], - name="Chairs", - description="We need a solution for chairs", - ) - TeamTask.objects.create( - team=teams["bar"], - name="Taps", - description="Taps must be ordered", - ) - TeamTask.objects.create( - team=teams["bar"], - name="Coffee", - description="We need to get some coffee for our coffee machine", - ) - TeamTask.objects.create( - team=teams["bar"], - name="Ice", - description="We need ice cubes and crushed ice in the bar", - ) + tasks = [ + TeamTask( + team=teams["noc"], + name="Setup private networks", + description="All the private networks need to be setup", + slug=slugify(f"{camp.year} Setup private networks"), + ), + TeamTask( + team=teams["noc"], + name="Setup public networks", + description="All the public networks need to be setup", + slug=slugify(f"{camp.year} Setup public networks"), + ), + TeamTask( + team=teams["noc"], + name="Deploy access points", + description="All access points need to be deployed", + slug=slugify(f"{camp.year} Deploy access points"), + ), + TeamTask( + team=teams["noc"], + name="Deploy fiber cables", + description="We need the fiber deployed where necessary", + slug=slugify(f"{camp.year} deploy fiber cables"), + ), + TeamTask( + team=teams["bar"], + name="List of booze", + description="A list of the different booze we need to have in the bar durng bornhack", + slug=slugify(f"{camp.year} list of booze"), + ), + TeamTask( + team=teams["bar"], + name="Chairs", + description="We need a solution for chairs", + slug=slugify(f"{camp.year} chairs"), + ), + TeamTask( + team=teams["bar"], + name="Taps", + description="Taps must be ordered", + slug=slugify(f"{camp.year} taps"), + ), + TeamTask( + team=teams["bar"], + name="Coffee", + description="We need to get some coffee for our coffee machine", + slug=slugify(f"{camp.year} coffee"), + ), + TeamTask( + team=teams["bar"], + name="Ice", + description="We need ice cubes and crushed ice in the bar", + slug=slugify(f"{camp.year} ice"), + ), + ] + + TeamTask.objects.bulk_create(tasks) def create_camp_team_memberships( self, @@ -1484,115 +1555,146 @@ def create_camp_team_memberships( users: dict, ) -> dict: """Create camp team memberships.""" - memberships = {} + memberships = defaultdict(list) self.output(f"Creating team memberships for {camp.year}...") # noc team - memberships["noc"] = {} - memberships["noc"]["user4"] = TeamMember.objects.create( - team=teams["noc"], - user=users[4], - approved=True, - lead=True, + memberships["noc"].append( + TeamMember( + team=teams["noc"], + user=users[4], + approved=True, + lead=True, + ), ) - memberships["noc"]["user4"].save() - memberships["noc"]["user1"] = TeamMember.objects.create( - team=teams["noc"], - user=users[1], - approved=True, + memberships["noc"].append( + TeamMember( + team=teams["noc"], + user=users[1], + approved=True, + ), ) - memberships["noc"]["user5"] = TeamMember.objects.create( - team=teams["noc"], - user=users[5], - approved=True, + memberships["noc"].append( + TeamMember( + team=teams["noc"], + user=users[5], + approved=True, + ), ) - memberships["noc"]["user2"] = TeamMember.objects.create( - team=teams["noc"], - user=users[2], + memberships["noc"].append( + TeamMember( + team=teams["noc"], + user=users[2], + ), ) # bar team - memberships["bar"] = {} - memberships["bar"]["user1"] = TeamMember.objects.create( - team=teams["bar"], - user=users[1], - approved=True, - lead=True, + memberships["bar"].append( + TeamMember( + team=teams["bar"], + user=users[1], + approved=True, + lead=True, + ), ) - memberships["bar"]["user3"] = TeamMember.objects.create( - team=teams["bar"], - user=users[3], - approved=True, - lead=True, + memberships["bar"].append( + TeamMember( + team=teams["bar"], + user=users[3], + approved=True, + lead=True, + ), ) - memberships["bar"]["user2"] = TeamMember.objects.create( - team=teams["bar"], - user=users[2], - approved=True, + memberships["bar"].append( + TeamMember( + team=teams["bar"], + user=users[2], + approved=True, + ), ) - memberships["bar"]["user7"] = TeamMember.objects.create( - team=teams["bar"], - user=users[7], - approved=True, + memberships["bar"].append( + TeamMember( + team=teams["bar"], + user=users[7], + approved=True, + ), ) - memberships["bar"]["user8"] = TeamMember.objects.create( - team=teams["bar"], - user=users[8], + memberships["bar"].append( + TeamMember( + team=teams["bar"], + user=users[8], + ), ) - # orga team - memberships["orga"] = {} - memberships["orga"]["user8"] = TeamMember.objects.create( - team=teams["orga"], - user=users[8], - approved=True, - lead=True, + memberships["orga"].append( + TeamMember( + team=teams["orga"], + user=users[8], + approved=True, + lead=True, + ), ) - memberships["orga"]["user9"] = TeamMember.objects.create( - team=teams["orga"], - user=users[9], - approved=True, - lead=True, + memberships["orga"].append( + TeamMember( + team=teams["orga"], + user=users[9], + approved=True, + lead=True, + ), ) - memberships["orga"]["user4"] = TeamMember.objects.create( - team=teams["orga"], - user=users[4], - approved=True, - lead=True, + memberships["orga"].append( + TeamMember( + team=teams["orga"], + user=users[4], + approved=True, + lead=True, + ), ) # shuttle team - memberships["shuttle"] = {} - memberships["shuttle"]["user7"] = TeamMember.objects.create( - team=teams["shuttle"], - user=users[7], - approved=True, - lead=True, + memberships["shuttle"].append( + TeamMember( + team=teams["shuttle"], + user=users[7], + approved=True, + lead=True, + ), ) - memberships["shuttle"]["user3"] = TeamMember.objects.create( - team=teams["shuttle"], - user=users[3], - approved=True, + memberships["shuttle"].append( + TeamMember( + team=teams["shuttle"], + user=users[3], + approved=True, + ), ) - memberships["shuttle"]["user9"] = TeamMember.objects.create( - team=teams["shuttle"], - user=users[9], + memberships["shuttle"].append( + TeamMember( + team=teams["shuttle"], + user=users[9], + ), ) - # economy team also gets a member - TeamMember.objects.create( - team=teams["economy"], - user=users[0], - lead=True, - approved=True, + memberships["economy"].append( + TeamMember( + team=teams["economy"], + user=users[0], + lead=True, + approved=True, + ), ) # gis team also gets a member - TeamMember.objects.create( - team=teams["gis"], - user=users[0], - lead=True, - approved=True, + memberships["gis"].append( + TeamMember( + team=teams["gis"], + user=users[0], + lead=True, + approved=True, + ), ) + + all_members = list(chain.from_iterable(memberships.values())) + TeamMember.objects.bulk_create(all_members) + return memberships def create_camp_team_shifts( @@ -1604,7 +1706,7 @@ def create_camp_team_shifts( """Create camp team shifts.""" shifts = {} self.output(f"Creating team shifts for {camp.year}...") - shifts[0] = TeamShift.objects.create( + shifts[0] = TeamShift( team=teams["shuttle"], shift_range=( datetime(camp.year, 8, 27, 2, 0, tzinfo=tz), @@ -1612,8 +1714,7 @@ def create_camp_team_shifts( ), people_required=1, ) - shifts[0].team_members.add(team_memberships["shuttle"]["user7"]) - shifts[1] = TeamShift.objects.create( + shifts[1] = TeamShift( team=teams["shuttle"], shift_range=( datetime(camp.year, 8, 27, 8, 0, tzinfo=tz), @@ -1621,7 +1722,7 @@ def create_camp_team_shifts( ), people_required=1, ) - shifts[2] = TeamShift.objects.create( + shifts[2] = TeamShift( team=teams["shuttle"], shift_range=( datetime(camp.year, 8, 27, 14, 0, tzinfo=tz), @@ -1629,152 +1730,164 @@ def create_camp_team_shifts( ), people_required=1, ) + TeamShift.objects.bulk_create(shifts.values()) + shifts[0].team_members.add(team_memberships["shuttle"][1]) def create_camp_info_categories(self, camp: Camp, teams: dict) -> dict: """Create camp info categories.""" categories = {} self.output(f"Creating infocategories for {camp.year}...") - categories["when"] = InfoCategory.objects.create( + categories["when"] = InfoCategory( team=teams["orga"], headline="When is BornHack happening?", anchor="when", ) - categories["travel"] = InfoCategory.objects.create( + categories["travel"] = InfoCategory( team=teams["orga"], headline="Travel Information", anchor="travel", ) - categories["sleep"] = InfoCategory.objects.create( + categories["sleep"] = InfoCategory( team=teams["orga"], headline="Where do I sleep?", anchor="sleep", ) - categories["noc"] = InfoCategory.objects.create( + categories["noc"] = InfoCategory( team=teams["noc"], headline="Where do I plug in?", anchor="noc", ) - + InfoCategory.objects.bulk_create(categories.values()) return categories def create_camp_info_items(self, camp: Camp, categories: dict) -> None: """Create the camp info items.""" self.output(f"Creating infoitems for {camp.year}...") - InfoItem.objects.create( - category=categories["when"], - headline="Opening", - anchor="opening", - body=f"BornHack {camp.year} starts saturday, august 27th, at noon (12:00). " - "It will be possible to access the venue before noon if for example you arrive early " - "in the morning with the ferry. But please dont expect everything to be ready before noon :)", - ) - InfoItem.objects.create( - category=categories["when"], - headline="Closing", - anchor="closing", - body=f"BornHack {camp.year} ends saturday, september 3rd, at noon (12:00). " - "Rented village tents must be empty and cleaned at this time, ready to take down. " - "Participants must leave the site no later than 17:00 on the closing day " - "(or stay and help us clean up).", - ) - InfoItem.objects.create( - category=categories["travel"], - headline="Public Transportation", - anchor="public-transportation", - body=output_fake_md_description(), - ) - InfoItem.objects.create( - category=categories["travel"], - headline="Bus to and from BornHack", - anchor="bus-to-and-from-bornhack", - body="PROSA, the union of IT-professionals in Denmark, has set up a great deal " - "for BornHack attendees travelling from Copenhagen to BornHack. For only 125kr, " - "about 17 euros, you can be transported to the camp on opening day, and back to " - "Copenhagen at the end of the camp!", - ) - InfoItem.objects.create( - category=categories["when"], - headline="Driving and Parking", - anchor="driving-and-parking", - body=output_fake_md_description(), - ) - InfoItem.objects.create( - category=categories["sleep"], - headline="Camping", - anchor="camping", - body="BornHack is first and foremost a tent camp. You need to bring a tent to sleep in. " - "Most people go with some friends and make a camp somewhere at the venue. " - "See also the section on Villages - you might be able to find some likeminded people to camp with.", - ) - InfoItem.objects.create( - category=categories["sleep"], - headline="Cabins", - anchor="cabins", - body="We rent out a few cabins at the venue with 8 beds each for people who don't want to " - "sleep in tents for some reason. A tent is the cheapest sleeping option (you just need a ticket), " - "but the cabins are there if you want them.", - ) - InfoItem.objects.create( - category=categories["noc"], - headline="Switches", - anchor="switches", - body="We have places for you to get your cable plugged in to a switch", - ) + items = [ + InfoItem( + category=categories["when"], + headline="Opening", + anchor="opening", + body=f"BornHack {camp.year} starts saturday, august 27th, at noon (12:00). " + "It will be possible to access the venue before noon if for example you arrive early " + "in the morning with the ferry. But please dont expect everything to be ready before noon :)", + ), + InfoItem( + category=categories["when"], + headline="Closing", + anchor="closing", + body=f"BornHack {camp.year} ends saturday, september 3rd, at noon (12:00). " + "Rented village tents must be empty and cleaned at this time, ready to take down. " + "Participants must leave the site no later than 17:00 on the closing day " + "(or stay and help us clean up).", + ), + InfoItem( + category=categories["travel"], + headline="Public Transportation", + anchor="public-transportation", + body=output_fake_md_description(), + ), + InfoItem( + category=categories["travel"], + headline="Bus to and from BornHack", + anchor="bus-to-and-from-bornhack", + body="PROSA, the union of IT-professionals in Denmark, has set up a great deal " + "for BornHack attendees travelling from Copenhagen to BornHack. For only 125kr, " + "about 17 euros, you can be transported to the camp on opening day, and back to " + "Copenhagen at the end of the camp!", + ), + InfoItem( + category=categories["when"], + headline="Driving and Parking", + anchor="driving-and-parking", + body=output_fake_md_description(), + ), + InfoItem( + category=categories["sleep"], + headline="Camping", + anchor="camping", + body="BornHack is first and foremost a tent camp. You need to bring a tent to sleep in. " + "Most people go with some friends and make a camp somewhere at the venue. " + "See also the section on Villages - you might be able to find some likeminded people to camp with.", + ), + InfoItem( + category=categories["sleep"], + headline="Cabins", + anchor="cabins", + body="We rent out a few cabins at the venue with 8 beds each for people who don't want to " + "sleep in tents for some reason. A tent is the cheapest sleeping option (you just need a ticket), " + "but the cabins are there if you want them.", + ), + InfoItem( + category=categories["noc"], + headline="Switches", + anchor="switches", + body="We have places for you to get your cable plugged in to a switch", + ), + ] + + InfoItem.objects.bulk_create(items) def create_camp_feedback(self, camp: Camp, users: dict[User]) -> None: """Create camp feedback.""" self.output(f"Creating feedback for {camp.year}...") - CampFeedback.objects.create( - camp=camp, - user=users[1], - feedback="Awesome event, will be back next year", - ) - CampFeedback.objects.create( - camp=camp, - user=users[3], - feedback="Very nice, though a bit more hot water would be awesome", - ) - CampFeedback.objects.create( - camp=camp, - user=users[5], - feedback="Is there a token here?", - ) - CampFeedback.objects.create( - camp=camp, - user=users[9], - feedback="That was fun. Thanks!", - ) + feedback = [ + CampFeedback( + camp=camp, + user=users[1], + feedback="Awesome event, will be back next year", + ), + CampFeedback( + camp=camp, + user=users[3], + feedback="Very nice, though a bit more hot water would be awesome", + ), + CampFeedback( + camp=camp, + user=users[5], + feedback="Is there a token here?", + ), + CampFeedback( + camp=camp, + user=users[9], + feedback="That was fun. Thanks!", + ), + ] + CampFeedback.objects.bulk_create(feedback) def create_camp_rides(self, camp: Camp, users: dict) -> None: """Create camp rides.""" self.output(f"Creating rides for {camp.year}...") - Ride.objects.create( - camp=camp, - user=users[1], - seats=2, - from_location="Copenhagen", - to_location="BornHack", - when=datetime(camp.year, 8, 27, 12, 0, tzinfo=tz), - description="I have space for two people and a little bit of luggage", - ) - Ride.objects.create( - camp=camp, - user=users[1], - seats=2, - from_location="BornHack", - to_location="Copenhagen", - when=datetime(camp.year, 9, 4, 12, 0, tzinfo=tz), - description="I have space for two people and a little bit of luggage", - ) - Ride.objects.create( - camp=camp, - user=users[4], - seats=1, - from_location="Aarhus", - to_location="BornHack", - when=datetime(camp.year, 8, 27, 12, 0, tzinfo=tz), - description="I need a ride and have a large backpack", - ) + rides = [ + Ride( + camp=camp, + user=users[1], + seats=2, + from_location="Copenhagen", + to_location="BornHack", + when=datetime(camp.year, 8, 27, 12, 0, tzinfo=tz), + description="I have space for two people and a little bit of luggage", + ), + Ride( + camp=camp, + user=users[1], + seats=2, + from_location="BornHack", + to_location="Copenhagen", + when=datetime(camp.year, 9, 4, 12, 0, tzinfo=tz), + description="I have space for two people and a little bit of luggage", + ), + Ride( + camp=camp, + user=users[4], + seats=1, + from_location="Aarhus", + to_location="BornHack", + when=datetime(camp.year, 8, 27, 12, 0, tzinfo=tz), + description="I need a ride and have a large backpack", + ), + ] + Ride.objects.bulk_create(rides) def create_camp_cfp(self, camp: Camp) -> None: """Create the camp call for participation.""" @@ -1794,7 +1907,7 @@ def create_camp_sponsor_tiers(self, camp: Camp) -> dict: """Create the camp sponsor tiers.""" tiers = {} self.output(f"Creating sponsor tiers for {camp.year}...") - tiers["platinum"] = SponsorTier.objects.create( + tiers["platinum"] = SponsorTier( name="Platinum sponsors", description="- 10 tickets\n- logo on website\n- physical banner in the speaker's tent\n- " "thanks from the podium\n- recruitment area\n- sponsor meeting with organizers\n- " @@ -1803,7 +1916,7 @@ def create_camp_sponsor_tiers(self, camp: Camp) -> dict: weight=0, week_tickets=10, ) - tiers["gold"] = SponsorTier.objects.create( + tiers["gold"] = SponsorTier( name="Gold sponsors", description="- 10 tickets\n- logo on website\n- physical banner in the speaker's tent\n- " "thanks from the podium\n- recruitment area\n- sponsor meeting with organizers\n- promoted HackMe", @@ -1811,7 +1924,7 @@ def create_camp_sponsor_tiers(self, camp: Camp) -> dict: weight=1, week_tickets=10, ) - tiers["silver"] = SponsorTier.objects.create( + tiers["silver"] = SponsorTier( name="Silver sponsors", description="- 5 tickets\n- logo on website\n- physical banner in the speaker's tent\n- " "thanks from the podium\n- recruitment area\n- sponsor meeting with organizers", @@ -1819,7 +1932,7 @@ def create_camp_sponsor_tiers(self, camp: Camp) -> dict: weight=2, week_tickets=5, ) - tiers["sponsor"] = SponsorTier.objects.create( + tiers["sponsor"] = SponsorTier( name="Sponsors", description="- 2 tickets\n- logo on website\n- physical banner in the speaker's tent\n- " "thanks from the podium\n- recruitment area", @@ -1827,7 +1940,7 @@ def create_camp_sponsor_tiers(self, camp: Camp) -> dict: weight=3, week_tickets=2, ) - + SponsorTier.objects.bulk_create(tiers.values()) return tiers def create_camp_sponsors(self, camp: Camp, tiers: dict) -> list: @@ -1835,7 +1948,7 @@ def create_camp_sponsors(self, camp: Camp, tiers: dict) -> list: sponsors = [] self.output(f"Creating sponsors for {camp.year}...") sponsors.append( - Sponsor.objects.create( + Sponsor( name="PROSA", tier=tiers["platinum"], description="Bus Trip", @@ -1844,7 +1957,7 @@ def create_camp_sponsors(self, camp: Camp, tiers: dict) -> list: ), ) sponsors.append( - Sponsor.objects.create( + Sponsor( name="DKUUG", tier=tiers["platinum"], description="Speakers tent", @@ -1853,7 +1966,7 @@ def create_camp_sponsors(self, camp: Camp, tiers: dict) -> list: ), ) sponsors.append( - Sponsor.objects.create( + Sponsor( name="LetsGo", tier=tiers["silver"], description="Shuttle", @@ -1862,7 +1975,7 @@ def create_camp_sponsors(self, camp: Camp, tiers: dict) -> list: ), ) sponsors.append( - Sponsor.objects.create( + Sponsor( name="Saxo Bank", tier=tiers["gold"], description="Cash Sponsorship", @@ -1871,7 +1984,7 @@ def create_camp_sponsors(self, camp: Camp, tiers: dict) -> list: ), ) sponsors.append( - Sponsor.objects.create( + Sponsor( name="CSIS", tier=tiers["sponsor"], description="Cash Sponsorship", @@ -1879,7 +1992,7 @@ def create_camp_sponsors(self, camp: Camp, tiers: dict) -> list: url="https://csis.dk", ), ) - + Sponsor.objects.bulk_create(sponsors) return sponsors def create_camp_sponsor_tickets( @@ -1911,103 +2024,108 @@ def create_camp_sponsor_tickets( ticket_type=ticket_types["adult_full_week"], ) - def create_token_categories(self, camp: Camp) -> dict[str, TokenCategory]: + def create_token_categories(self) -> None: """Create the camp tokens.""" - self.output(f"Creating token categories for {camp.year}...") + self.output("Creating token categories...") categories = {} - categories["physical"], _ = TokenCategory.objects.get_or_create( + categories["physical"] = TokenCategory( name="Physical", description="Tokens exist in the physical space", ) - categories["phone"], _ = TokenCategory.objects.get_or_create( + categories["phone"] = TokenCategory( name="Phone", description="Tokens exist in a phoney space", ) - categories["electrical"], _ = TokenCategory.objects.get_or_create( + categories["electrical"] = TokenCategory( name="Electrical", description="Tokens with power", ) - categories["internet"], _ = TokenCategory.objects.get_or_create( + categories["internet"] = TokenCategory( name="Internet", description="Tokens exist in the virtual space", ) - categories["website"], _ = TokenCategory.objects.get_or_create( + categories["website"] = TokenCategory( name="Website", description="Tokens exist on the bornhack website", ) - return categories + TokenCategory.objects.bulk_create( + categories.values(), + update_conflicts=True, + update_fields=["description"], + unique_fields=["name"], + ) + self.token_categories = categories - def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: + def create_camp_tokens(self, camp: Camp) -> dict[Token]: """Create the camp tokens.""" tokens = {} self.output(f"Creating tokens for {camp.year}...") - tokens[0] = Token.objects.create( + tokens[0] = Token( camp=camp, token=get_random_string(length=32), - category=categories["physical"], + category=self.token_categories["physical"], hint="Token in a tent", description="Token in the back of the speakers tent (in binary)", active=True, ) - tokens[1] = Token.objects.create( + tokens[1] = Token( camp=camp, token=get_random_string(length=32), - category=categories["internet"], + category=self.token_categories["internet"], hint="Social media", description="Mastodon", active=True, ) - tokens[2] = Token.objects.create( + tokens[2] = Token( camp=camp, token=get_random_string(length=32), - category=categories["website"], + category=self.token_categories["website"], hint="Web server", description="Token hidden in the X-Secret-Token HTTP header on the BornHack website", active=True, ) - tokens[3] = Token.objects.create( + tokens[3] = Token( camp=camp, token=get_random_string(length=32), - category=categories["physical"], + category=self.token_categories["physical"], hint="QR Code", description="Token in infodesk (QR code)", active=True, ) - tokens[4] = Token.objects.create( + tokens[4] = Token( camp=camp, token=get_random_string(length=32), - category=categories["physical"], + category=self.token_categories["physical"], hint="Gadget", description=f"Token on the back of the BornHack {camp.year} badge", active=True, ) - tokens[5] = Token.objects.create( + tokens[5] = Token( camp=camp, token=get_random_string(length=32), - category=categories["website"], + category=self.token_categories["website"], hint="EXIF", description="Token hidden in EXIF data in the logo posted on the website sunday", active=True, ) - + Token.objects.bulk_create(tokens.values()) return tokens - def create_camp_token_finds( - self, - camp: Camp, - tokens: dict[Token], - users: dict[User], - ) -> None: + def create_camp_token_finds(self, camp: Camp, tokens: dict) -> None: """Create the camp token finds.""" self.output(f"Creating token finds for {camp.year}...") - TokenFind.objects.create(token=tokens[3], user=users[4]) - TokenFind.objects.create(token=tokens[5], user=users[4]) - TokenFind.objects.create(token=tokens[2], user=users[7]) - TokenFind.objects.create(token=tokens[1], user=users[3]) - TokenFind.objects.create(token=tokens[4], user=users[2]) - TokenFind.objects.create(token=tokens[5], user=users[6]) + finds = [ + TokenFind(token=tokens[3], user=self.users[4]), + TokenFind(token=tokens[5], user=self.users[4]), + TokenFind(token=tokens[2], user=self.users[7]), + TokenFind(token=tokens[1], user=self.users[3]), + TokenFind(token=tokens[4], user=self.users[2]), + TokenFind(token=tokens[5], user=self.users[6]), + ] for i in range(6): - TokenFind.objects.create(token=tokens[i], user=users[1]) + finds.append(TokenFind(token=tokens[i], user=self.users[1])) + + TokenFind.objects.bulk_create(finds) def create_prize_ticket(self, camp: Camp, ticket_types: dict) -> None: """Create prize tickets""" @@ -2118,17 +2236,20 @@ def create_camp_map_layer(self, camp: Camp) -> None: """Create map layers for camp.""" group = MapGroup.objects.get(name="Generic") team = Team.objects.get(name="Orga", camp=camp) - Layer.objects.create( - name="Non public layer", - slug="hiddenlayer", - description="Hidden layer", - icon="fa fa-list-ul", - group=group, - public=False, - responsible_team=team, + ( + Layer.objects.create( + name="Non public layer", + slug="hiddenlayer", + description="Hidden layer", + icon="fa fa-list-ul", + group=group, + public=False, + responsible_team=team, + ), ) layer = Layer.objects.create( name="Team Area", + slug="teamarea", description="Team areas", icon="fa fa-list-ul", group=group, @@ -2160,365 +2281,202 @@ def create_camp_map_layer(self, camp: Camp) -> None: def create_camp_pos(self, teams: dict[Team]) -> None: """Create POS locations for camp.""" - Pos.objects.create( - name="Infodesk", - team=teams["info"], - external_id="HHR9izotB6HLzgT6k", - ) - Pos.objects.create( - name="Bar", - team=teams["bar"], - external_id="bTasxE2YYXZh35wtQ", - ) + pos = [ + Pos( + name="Infodesk", + team=teams["info"], + external_id="HHR9izotB6HLzgT6k", + ), + Pos( + name="Bar", + team=teams["bar"], + external_id="bTasxE2YYXZh35wtQ", + ), + ] + Pos.objects.bulk_create(pos) def output(self, message: str) -> None: """Method for logging the output.""" logger.info(message) - def bootstrap_full(self, options: dict) -> None: - """Bootstrap a full devsite with all the years.""" - camps = [ - { - "year": 2016, - "tagline": "Initial Commit", - "colour": "#004dff", - "read_only": True, - }, - { - "year": 2017, - "tagline": "Make Tradition", - "colour": "#750787", - "read_only": True, - }, - { - "year": 2018, - "tagline": "scale it", - "colour": "#008026", - "read_only": True, - }, - { - "year": 2019, - "tagline": "a new /home", - "colour": "#ffed00", - "read_only": True, - "light_text": False, - }, - { - "year": 2020, - "tagline": "Make Clean", - "colour": "#ff8c00", - "read_only": True, - }, - { - "year": 2021, - "tagline": "Continuous Delivery", - "colour": "#e40303", - "read_only": True, - }, - { - "year": 2022, - "tagline": "black ~/hack", - "colour": "#000000", - "read_only": True, - }, - { - "year": 2023, - "tagline": "make legacy", - "colour": "#613915", - "read_only": True, - }, - { - "year": 2024, - "tagline": "Feature Creep", - "colour": "#73d7ee", - "read_only": False, - "light_text": False, - }, - { - "year": 2025, - "tagline": "10 Badges", - "colour": "#ffafc7", - "read_only": False, - "light_text": False, - }, - { - "year": 2026, - "tagline": "Undecided", - "colour": "#ffffff", - "read_only": False, - "light_text": False, - }, - { - "year": 2027, - "tagline": "Undecided", - "colour": "#004dff", - "read_only": True, - }, - { - "year": 2028, - "tagline": "Undecided", - "colour": "#750787", - "read_only": True, - }, - { - "year": 2029, - "tagline": "Undecided", - "colour": "#008026", - "read_only": True, - }, - { - "year": 2030, - "tagline": "Undecided", - "colour": "#ffed00", - "read_only": True, - "light_text": False, - }, - { - "year": 2031, - "tagline": "Undecided", - "colour": "#ff8c00", - "read_only": True, - }, - ] - self.create_camps(camps) - self.bootstrap_base(options) + def prepare_camp_list(self, years_range: list, writable_range: list) -> list: + """Prepare camp dataset for bootstrapping.""" + dataset = [] + template = {"colour": "#424242", "tagline": "Undecided"} - def bootstrap_tests(self) -> None: - """Method for bootstrapping the test database.""" - camps = [ - { - "year": 2024, - "tagline": "Feature Creep", - "colour": "#73d7ee", - "read_only": True, - "light_text": False, - }, - { - "year": 2025, - "tagline": "Undecided", - "colour": "#ffafc7", - "read_only": False, - "light_text": False, - }, - { - "year": 2026, - "tagline": "Undecided", - "colour": "#ffffff", - "read_only": False, - "light_text": False, - }, - ] - self.create_camps(camps) - self.create_users(16) - self.create_event_types() - self.create_product_categories() - teams = {} - for camp, read_only in self.camps: - if camp.year <= settings.UPCOMING_CAMP_YEAR: - ticket_types = self.create_camp_ticket_types(camp) - camp_products = self.create_camp_products( - camp, - self.product_categories, - ticket_types, - ) - self.create_orders(self.users, camp_products) - sponsor_tiers = self.create_camp_sponsor_tiers(camp) - camp_sponsors = self.create_camp_sponsors(camp, sponsor_tiers) - self.create_camp_sponsor_tickets( - camp, - camp_sponsors, - sponsor_tiers, - ticket_types, - ) - self.create_prize_ticket(camp, ticket_types) - self.create_camp_tracks(camp) + for year in years_range: + camp = template.copy() + camp["year"] = year + camp["read_only"] = False if year in writable_range else True + predefined = CAMP_MAP.get(year) + if predefined is not None: + camp.update(predefined) - teams[camp.year] = self.create_camp_teams(camp) - self.create_camp_team_memberships(camp, teams[camp.year], self.users) - camp.read_only = read_only - camp.call_for_participation_open = not read_only - camp.call_for_sponsors_open = not read_only - camp.save() + dataset.append(camp) - self.camp = self.camps[1][0] - self.add_team_permissions(self.camp) - self.teams = teams[self.camp.year] - for member in TeamMember.objects.filter(team__camp=self.camp): - member.save() - - def bootstrap_camp(self, options: dict) -> None: - """Bootstrap camp related entities.""" - permissions_added = False - self.teams = {} - for camp, read_only in self.camps: - self.output( - f"----------[ Bornhack {camp.year} ]----------", - ) - - if camp.year <= timezone.now().year: - ticket_types = self.create_camp_ticket_types(camp) - - camp_products = self.create_camp_products( - camp, - self.product_categories, - ticket_types, - ) + return dataset - self.create_orders(self.users, camp_products) - - self.create_camp_tracks(camp) - - locations = self.create_event_locations(camp) - - self.create_camp_news(camp) - - teams = self.create_camp_teams(camp) - self.teams[camp.year] = teams + def bootstrap_global_data(self, prepared_camps: list) -> None: + """Bootstrap global data for the application.""" + self.create_event_routing_types() + self.create_event_types() + self.create_url_types() - if not read_only and not permissions_added: - # add permissions for the first camp that is not read_only - self.add_team_permissions(camp) - permissions_added = True + self.create_users() + self.create_camps(prepared_camps) - self.create_camp_team_tasks(camp, teams) + self.create_news() - team_memberships = self.create_camp_team_memberships( - camp, - teams, - self.users, - ) + self.create_quickfeedback_options() - self.create_camp_team_shifts(camp, teams, team_memberships) + self.create_maps_layer_generic() - self.create_camp_pos(teams) + self.create_credebtors() - self.create_camp_cfp(camp) + self.create_mobilepay_transactions() + self.create_clearhaus_settlements() + self.create_epay_transactions() - self.create_camp_proposals(camp, self.event_types) + self.create_bank_stuff() + self.create_coinify_stuff() - self.create_proposal_urls(camp) + self.create_product_categories() + self.create_token_categories() - self.create_camp_event_sessions(camp, self.event_types, locations) + def bootstrap_camp( + self, + camp: Camp, + autoschedule: bool = True, + read_only: bool = True, + ) -> None: + """Bootstrap camp related entities.""" + permissions_added = False + self.teams = {} - self.generate_speaker_availability(camp) + if camp.year > timezone.now().year: + self.output("Not creating anything for this year yet") - try: - self.approve_speaker_proposals(camp) - except ValidationError: - self.output( - "Name collision, bad luck. Run the bootstrap script again! " - "PRs to make this less annoying welcome :)", - ) - sys.exit(1) + ticket_types = self.create_camp_ticket_types(camp) - self.approve_event_proposals(camp) + camp_products = self.create_camp_products( + camp, + self.product_categories, + ticket_types, + ) - self.create_camp_scheduling(camp, not options["skip_auto_scheduler"]) + self.create_orders(self.users, camp_products) - # shuffle it up - delete and create new random availability - self.generate_speaker_availability(camp) + self.create_camp_tracks(camp) - # and create some speaker<>event conflicts - self.create_camp_speaker_event_conflicts(camp) + locations = self.create_event_locations(camp) - # recalculate the autoschedule - self.create_camp_rescheduling(camp, not options["skip_auto_scheduler"]) + self.create_camp_news(camp) - self.create_camp_villages(camp, self.users) + teams = self.create_camp_teams(camp) + self.teams[camp.year] = teams - facility_types = self.create_facility_types( - teams, - self.quickfeedback_options, - ) + if not camp.read_only and not permissions_added: + # add permissions for the first camp that is not read_only + self.add_team_permissions(camp) + permissions_added = True - facilities = self.create_facilities(facility_types) + self.create_camp_team_tasks(camp, teams) - self.create_facility_feedbacks( - facilities, - self.quickfeedback_options, - self.users, - ) + team_memberships = self.create_camp_team_memberships( + camp, + teams, + self.users, + ) - info_categories = self.create_camp_info_categories(camp, teams) + self.create_camp_team_shifts(camp, teams, team_memberships) - self.create_camp_info_items(camp, info_categories) + self.create_camp_pos(teams) - self.create_camp_feedback(camp, self.users) + self.create_camp_cfp(camp) - self.create_camp_rides(camp, self.users) + self.create_camp_proposals(camp, self.event_types) - self.create_camp_cfs(camp) + self.create_proposal_urls(camp) - sponsor_tiers = self.create_camp_sponsor_tiers(camp) + self.create_camp_event_sessions(camp, self.event_types, locations) - camp_sponsors = self.create_camp_sponsors(camp, sponsor_tiers) + self.generate_speaker_availability(camp) - categories = self.create_token_categories(camp) + try: + self.approve_speaker_proposals(camp) + except ValidationError: + self.output( + "Name collision, bad luck. Run the bootstrap script again! PRs to make this less annoying welcome :)", + ) + sys.exit(1) - tokens = self.create_camp_tokens(camp, categories) + self.approve_event_proposals(camp) - self.create_camp_token_finds(camp, tokens, self.users) + self.create_camp_scheduling(camp, autoschedule) - self.create_camp_expenses(camp) + # shuffle it up - delete and create new random availability + self.generate_speaker_availability(camp) - self.create_camp_reimbursements(camp) + # and create some speaker<>event conflicts + self.create_camp_speaker_event_conflicts(camp) - self.create_camp_revenues(camp) + # recalculate the autoschedule + self.create_camp_rescheduling(camp, autoschedule) - self.create_camp_map_layer(camp) - else: - self.output("Not creating anything for this year yet") + self.create_camp_villages(camp, self.users) - camp.read_only = read_only - camp.call_for_participation_open = not read_only - camp.call_for_sponsors_open = not read_only - camp.save() + facility_types = self.create_facility_types( + teams, + self.quickfeedback_options, + ) - # Update team permissions. - if camp.year == settings.UPCOMING_CAMP_YEAR: - for member in TeamMember.objects.filter(team__camp=camp): - member.save() + facilities = self.create_facilities(facility_types) - def bootstrap_base(self, options: dict) -> None: - """Bootstrap the data for the application.""" - self.output( - "----------[ Running bootstrap_devsite ]----------", + self.create_facility_feedbacks( + facilities, + self.quickfeedback_options, + self.users, ) - self.output("----------[ Global stuff ]----------") + info_categories = self.create_camp_info_categories(camp, teams) - self.create_event_routing_types() - self.create_users(16) - - self.create_news() + self.create_camp_info_items(camp, info_categories) - self.create_event_types() + self.create_camp_feedback(camp, self.users) - self.create_url_types() + self.create_camp_rides(camp, self.users) - self.create_product_categories() + self.create_camp_cfs(camp) - self.create_quickfeedback_options() + sponsor_tiers = self.create_camp_sponsor_tiers(camp) - self.create_mobilepay_transactions() + self.create_camp_sponsors(camp, sponsor_tiers) - self.create_clearhaus_settlements() + tokens = self.create_camp_tokens(camp) - self.create_credebtors() + self.create_camp_token_finds(camp, tokens) - self.create_bank_stuff() + self.create_camp_expenses(camp) - self.create_coinify_stuff() + self.create_camp_reimbursements(camp) - self.create_epay_transactions() + self.create_camp_revenues(camp) - self.create_maps_layer_generic() + self.create_camp_map_layer(camp) - self.bootstrap_camp(options) + if read_only: + self.output(f"Updatin {camp.title} to read-only...") + camp.read_only = True + camp.save(update_fields=["read_only"]) - self.output("----------[ Finishing up ]----------") + if camp.year == timezone.now().year: + self.output("Updating team permissions...") + for member in TeamMember.objects.filter(team__camp=camp): + member.save() - self.output("Adding event routing...") + def post_bootstrap(self): + """Make the last changes after the bootstrapping is done.""" + self.output("Creating event routing...") teams = self.teams[next(reversed(self.teams.keys()))] Routing.objects.create( team=teams["orga"], @@ -2529,4 +2487,44 @@ def bootstrap_base(self, options: dict) -> None: eventtype=Type.objects.get(name="ticket_created"), ) - self.output("done!") + def bootstrap_tests(self) -> None: + """Method for bootstrapping the test database.""" + year = timezone.now().year + year_range = [(year - 1), year, (year + 1)] + camps = self.prepare_camp_list(year_range, [year]) + self.create_camps(camps) + self.create_users() + self.create_event_types() + self.create_product_categories() + teams = {} + for camp in self.camps: + if camp.year > year: + continue + + ticket_types = self.create_camp_ticket_types(camp) + camp_products = self.create_camp_products( + camp, + self.product_categories, + ticket_types, + ) + self.create_orders(self.users, camp_products) + sponsor_tiers = self.create_camp_sponsor_tiers(camp) + camp_sponsors = self.create_camp_sponsors(camp, sponsor_tiers) + self.create_camp_sponsor_tickets( + camp, + camp_sponsors, + sponsor_tiers, + ticket_types, + ) + self.create_prize_ticket(camp, ticket_types) + self.create_camp_tracks(camp) + + teams[camp.year] = self.create_camp_teams(camp) + self.create_camp_team_memberships(camp, teams[camp.year], self.users) + camp.save() + + self.camp = self.camps[1] + self.add_team_permissions(self.camp) + self.teams = teams[self.camp.year] + for member in TeamMember.objects.filter(team__camp=self.camp): + member.save() diff --git a/src/utils/management/commands/bootstrap_devsite.py b/src/utils/management/commands/bootstrap_devsite.py index cc6722c50..35b044605 100644 --- a/src/utils/management/commands/bootstrap_devsite.py +++ b/src/utils/management/commands/bootstrap_devsite.py @@ -1,43 +1,196 @@ from __future__ import annotations import logging +from argparse import ArgumentTypeError +from concurrent.futures import ThreadPoolExecutor +from django.core.management import CommandError from django.core.management import call_command from django.core.management.base import BaseCommand +from django.db import connections +from django.db import transaction from django.utils import timezone +from camps.models import Camp from utils.bootstrap.base import Bootstrap logger = logging.getLogger(f"bornhack.{__name__}") +VERBOSITY_LOG_LEVELS = { + 0: logging.ERROR, + 1: logging.WARNING, + 2: logging.INFO, + 3: logging.DEBUG, +} class Command(BaseCommand): + """Class for `bootstrap_devsite` command.""" + args = "none" help = "Create mock data for development instances" def add_arguments(self, parser) -> None: + """Define the arguments available for this command.""" + parser.add_argument( + "-t", + "--threads", + type=int, + default=2, + help="Specify amount of threads to be used. Default: 2", + ) parser.add_argument( "-s", "--skip-auto-scheduler", action="store_true", help="Don't run the auto-scheduler. This is useful on operating systems for which the solver binary is not packaged, such as OpenBSD.", ) - - def output(self, message) -> None: - self.stdout.write( - "{}: {}".format(timezone.now().strftime("%Y-%m-%d %H:%M:%S"), message), + parser.add_argument( + "-w", + "--writable-years", + type=self._years, + default=[timezone.now().year + i for i in range(2)], + help="Comma separated range of writable years. Example: 2025,2027", + ) + parser.add_argument( + "-y", + "--years", + type=self._years, + default=[i for i in range(2016, timezone.now().year + 6)], + help="Comma separated range of camp years. Example: 2016,2032", ) + def _years(self, value: str) -> list[int]: + """Transform str argument to list of years or raise exception.""" + try: + years = [int(year.strip()) for year in value.split(",")] + return [year for year in range(min(years), max(years) + 1)] + except ValueError: + raise ArgumentTypeError( + "Years must be comma separated integers (e.g. 2026,2027)", + ) + def handle(self, *args, **options) -> None: + """Flush database and run bootstrapper.""" start = timezone.now() - self.output( - self.style.SUCCESS( - "----------[ Deleting all data from database ]----------", - ), - ) + self.decorated_output("Running bootstrap_devsite", "cyan") + + logging.getLogger( + "bornhack", + ).setLevel(VERBOSITY_LOG_LEVELS.get(options["verbosity"], 1)) + + self.validate(options) + + self.decorated_output("Flush all data from database", "purple") call_command("flush", "--noinput") + bootstrap = Bootstrap() bootstrap.output = self.output - bootstrap.bootstrap_full(options) + self.run(bootstrap, options) + duration = timezone.now() - start - self.output(f"bootstrap_devsite took {duration}!") + self.decorated_output( + f"Finished bootstrap_devsite in {duration}!", + "cyan", + ) + + def validate(self, options: dict): + """Validate arguments parsed to command.""" + threads = options["threads"] + if threads is not None: + if threads < 1: + raise CommandError("When specifying threads it must be above 0") + + years = options["years"] + if min(years) < 2016: + raise CommandError("When specifying years the lower limit is 2016") + + writables = options["writable_years"] + if min(writables) < min(years) or max(writables) > max(years): + raise CommandError( + "Writable years is not within range of camp years." + "\nUse (-w/--writable-years YYYY,YYYY) for manual override.", + ) + + def run(self, bootstrap: Bootstrap, options: dict): + """Bootstrap data using threading.""" + years = options["years"] + writable_years = options["writable_years"] + prepared_camps = bootstrap.prepare_camp_list(years, writable_years) + + self.decorated_output("Creating global data", "green") + bootstrap.bootstrap_global_data(prepared_camps) + self.decorated_output("Finished creating global data", "green") + + threads = options["threads"] + self.decorated_output( + f"Bootstrap camp data using {threads} threads", + "purple", + ) + + # Don't bootstrap above last writable camp + BOOTSTRAP_LIMIT = writable_years[-1] + bootstrap_logs = [] + with ThreadPoolExecutor(max_workers=threads) as executor: + for camp in bootstrap.camps: + if camp.year > BOOTSTRAP_LIMIT: + bootstrap_logs.append( + (f"Skipping bootstrap for {camp.title}", "yellow"), + ) + else: + executor.submit( + self.worker_job, + bootstrap, + camp, + (not options["skip_auto_scheduler"]), + False if camp.year in writable_years else True, + ) + bootstrap_logs.append( + (f"Completed bootstrapping of {camp.title}", "green"), + ) + + self.decorated_output("Running post bootstrap tasks", "purple") + bootstrap.post_bootstrap() + self.decorated_output("Finished post bootstrap tasks", "purple") + + for msg, style in bootstrap_logs: + self.decorated_output(msg, style) + + def worker_job( + self, + bootstrap: Bootstrap, + camp: Camp, + schedule: bool, + read_only: bool, + ): + """Execute concurrent bootstrapping atomically + and always close the db connection. + """ + self.decorated_output(f"Executing worker job: {camp.title}", "cyan") + try: + with transaction.atomic(): + bootstrap.bootstrap_camp(camp, schedule, read_only) + finally: + connections.close() + + def decorated_output(self, msg, color="white"): + """Decorate stdout with colored text and ascii art.""" + msg = f"----------[ {msg} ]----------" + self.output(msg, color) + + def output(self, msg, color="white") -> None: + """Formatting stdout with colored text options.""" + color_map = { + "red": self.style.ERROR, + "yellow": self.style.WARNING, + "green": self.style.SUCCESS, + "white": self.style.HTTP_INFO, # DEFAULT/FALLBACK + "cyan": self.style.MIGRATE_HEADING, + "purple": self.style.HTTP_SERVER_ERROR, + } + color = color_map.get(color, self.style.HTTP_INFO) + self.stdout.write( + "{}: {}".format( + timezone.now().strftime("%Y-%m-%d %H:%M:%S"), + color(msg), + ), + ) diff --git a/src/utils/tests.py b/src/utils/tests.py index 45d9d38a5..e47ebcc04 100644 --- a/src/utils/tests.py +++ b/src/utils/tests.py @@ -3,24 +3,107 @@ from __future__ import annotations import logging -from unittest import skip +from argparse import ArgumentTypeError +from copy import deepcopy +import pytest +from django.core.management import CommandError from django.core.management import call_command from django.test import Client from django.test import TestCase +from django.utils import timezone from camps.models import Camp from teams.models import Team from utils.bootstrap.base import Bootstrap +from utils.management.commands import bootstrap_devsite -class TestBootstrapScript(TestCase): - """Test bootstrap_devsite script (touching many codepaths)""" +class TestBootstrapDevsiteCommand: + """Test bootstrap_devsite command.""" - @skip - def test_bootstrap_script(self): - """If no orders have been made, the product is still available.""" - call_command("bootstrap_devsite") + @pytest.fixture + def options(self) -> dict: + """Fixture for default options.""" + year = timezone.now().year + return { + "threads": 4, + "skip_auto_scheduler": False, + "writable_years": [i for i in range(year, year + 3)], + "years": [i for i in range(2016, year + 6)], + } + + def test_custom_years_type(self): + """Test custom argument type for parsing years and returning a list.""" + cmd = bootstrap_devsite.Command() + year = timezone.now().year + expected = [year for year in range(year, (year + 3))] + + result = cmd._years(f"{year},{year + 2}") + + assert result == expected + + def test_custom_years_wrong_formatting(self): + """Test raising exception when wrongly formatted.""" + cmd = bootstrap_devsite.Command() + + with pytest.raises(ArgumentTypeError): + cmd._years("wrong format") + + with pytest.raises(ArgumentTypeError): + cmd._years("2020-2021") + + def test_validating_threads_argument(self, options): + """Test validating the `threads` arg.""" + cmd = bootstrap_devsite.Command() + options["threads"] = 0 + + with pytest.raises(CommandError): + cmd.validate(options) + + def test_validating_years_is_not_below_2016(self, options): + """Test validating `years` is not below 2016.""" + cmd = bootstrap_devsite.Command() + copy = deepcopy(options) + copy["years"][0] = 2015 + + with pytest.raises(CommandError): + cmd.validate(copy) + + with pytest.raises(CommandError): + call_command("bootstrap_devsite", years=copy["years"]) + + def test_validating_writable_years_is_within_range(self, options): + """Validate writable years is within range of camp years.""" + cmd = bootstrap_devsite.Command() + + # Test lower limit + lower = deepcopy(options) + lower["writable_years"][0] = options["years"][0] - 1 + + with pytest.raises(CommandError): + cmd.validate(lower) + + with pytest.raises(CommandError): + call_command( + "bootstrap_devsite", + writable_years=lower["writable_years"], + years=lower["years"], + ) + + # Test upper limit + upper = deepcopy(options) + upper["writable_years"] = [i for i in range(min(upper["years"]), max(upper["years"]) + 2)] + + with pytest.raises(CommandError): + cmd.validate(upper) + + with pytest.raises(CommandError): + call_command( + "bootstrap_devsite", + writable_years=upper["writable_years"], + years=upper["years"], + ) class BornhackTestBase(TestCase):