From 9fc5b700abfb5e2d7e459991a0e3ecf69326648e Mon Sep 17 00:00:00 2001 From: Christian Henriksen Date: Tue, 17 Feb 2026 01:47:05 +0100 Subject: [PATCH 1/7] Implement `bulk_create` to shave off ~23s --- src/utils/bootstrap/base.py | 947 ++++++++++++++++++++---------------- 1 file changed, 524 insertions(+), 423 deletions(-) diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py index e52db4551..d7ec26eba 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -9,7 +9,11 @@ from zoneinfo import ZoneInfo from datetime import datetime from datetime import timedelta +from collections import defaultdict +from itertools import chain +from psycopg2.extras import DateTimeTZRange +from django.utils.text import slugify import factory from allauth.account.models import EmailAddress from django.conf import settings @@ -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 @@ -127,20 +130,20 @@ def create_camps(self, camps: dict) -> None: read_only = camp["read_only"] camp_instances.append( ( - Camp.objects.create( + Camp( title=f"BornHack {year}", tagline=camp["tagline"], slug=f"bornhack-{year}", shortslug=f"bornhack-{year}", - buildup=( + buildup=DateTimeTZRange( datetime(year, 8, 25, 12, 0, tzinfo=tz), datetime(year, 8, 27, 12, 0, tzinfo=tz), ), - camp=( + camp=DateTimeTZRange( datetime(year, 8, 27, 12, 0, tzinfo=tz), datetime(year, 9, 3, 12, 0, tzinfo=tz), ), - teardown=( + teardown=DateTimeTZRange( datetime(year, 9, 3, 12, 0, tzinfo=tz), datetime(year, 9, 5, 12, 0, tzinfo=tz), ), @@ -150,7 +153,7 @@ def create_camps(self, camps: dict) -> None: read_only, ), ) - + Camp.objects.bulk_create((c[0] for c in camp_instances)) self.camps = camp_instances def create_event_routing_types(self) -> None: @@ -301,42 +304,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 +351,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 +405,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 +419,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 +432,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 +446,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 +459,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 +473,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 +486,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 +500,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 +582,79 @@ 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"] + return types def create_camp_products( @@ -655,7 +668,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 +682,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 +696,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 +710,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 +724,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 +738,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 +752,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 +766,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 +780,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 +794,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 +808,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 +822,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 +833,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 +946,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 +996,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 +1008,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 +1037,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 +1057,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 +1067,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,26 +1077,29 @@ 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), ), ) EventSession.objects.create( - camp=camp, - event_type=event_types["workshop"], - event_location=event_locations["workshop_room_3"], - when=( - datetime(start.year, start.month, start.day, 12, 0, tzinfo=tz), - datetime(start.year, start.month, start.day, 18, 0, tzinfo=tz), - ), - ) + 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), + ), + ) # create sessions for the keynotes for day in [days[1], days[3], days[5]]: EventSession.objects.create( camp=camp, event_type=event_types["keynote"], event_location=event_locations["speakers_tent"], + event_duration_minutes=90, when=( datetime( day.lower.year, @@ -1324,37 +1351,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 +1461,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 +1527,148 @@ 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"]["user3"] = TeamMember.objects.create( - team=teams["bar"], - user=users[3], - approved=True, - lead=True, + memberships["bar"].append( + TeamMember( + team=teams["bar"], + user=users[1], + 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[3], + approved=True, + lead=True, + ) ) - memberships["bar"]["user7"] = TeamMember.objects.create( - team=teams["bar"], - user=users[7], - approved=True, + memberships["bar"].append( + TeamMember( + team=teams["bar"], + user=users[2], + approved=True, + ) ) - memberships["bar"]["user8"] = TeamMember.objects.create( - team=teams["bar"], - user=users[8], + memberships["bar"].append( + TeamMember( + team=teams["bar"], + user=users[7], + approved=True, + ) ) + 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"]["user3"] = TeamMember.objects.create( - team=teams["shuttle"], - user=users[3], - approved=True, + memberships["shuttle"].append( + TeamMember( + team=teams["shuttle"], + user=users[7], + approved=True, + lead=True, + ) ) - memberships["shuttle"]["user9"] = TeamMember.objects.create( - team=teams["shuttle"], - user=users[9], + memberships["shuttle"].append( + TeamMember( + team=teams["shuttle"], + user=users[3], + approved=True, + ) ) + 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 +1680,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 +1688,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 +1696,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 +1704,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 +1881,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 +1890,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 +1898,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 +1906,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 +1914,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 +1922,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 +1931,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 +1940,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 +1949,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 +1958,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 +1966,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( @@ -1915,33 +2002,39 @@ def create_token_categories(self, camp: Camp) -> dict[str, TokenCategory]: """Create the camp tokens.""" self.output(f"Creating token categories for {camp.year}...") 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", ) + TokenCategory.objects.bulk_create( + categories.values(), + update_conflicts=True, + update_fields=["description"], + unique_fields=["name"], + ) return categories def create_camp_tokens(self, camp: Camp, categories: dict) -> 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"], @@ -1949,7 +2042,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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"], @@ -1957,7 +2050,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: description="Mastodon", active=True, ) - tokens[2] = Token.objects.create( + tokens[2] = Token( camp=camp, token=get_random_string(length=32), category=categories["website"], @@ -1965,7 +2058,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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"], @@ -1973,7 +2066,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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"], @@ -1981,7 +2074,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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"], @@ -1989,7 +2082,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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( @@ -2000,14 +2093,18 @@ def create_camp_token_finds( ) -> 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=users[4]), + TokenFind(token=tokens[5], user=users[4]), + TokenFind(token=tokens[2], user=users[7]), + TokenFind(token=tokens[1], user=users[3]), + TokenFind(token=tokens[4], user=users[2]), + TokenFind(token=tokens[5], user=users[6]), + ] for i in range(6): - TokenFind.objects.create(token=tokens[i], user=users[1]) + finds.append(TokenFind(token=tokens[i], user=users[1])) + + TokenFind.objects.bulk_create(finds) def create_prize_ticket(self, camp: Camp, ticket_types: dict) -> None: """Create prize tickets""" @@ -2126,9 +2223,10 @@ def create_camp_map_layer(self, camp: Camp) -> None: 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,16 +2258,19 @@ 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.""" From 7786626dc22df4c9f2f63033accafde642ea83d3 Mon Sep 17 00:00:00 2001 From: Christian Henriksen Date: Wed, 18 Feb 2026 01:21:45 +0100 Subject: [PATCH 2/7] Implement optional arguments and concurrency. - Allow to customize camp years created/read_only by specifying ranges. - Implement concurrency using threading with 4 default threads (best performance) [NOT TESTED ON WINDOWS]. - Restructure Bootstrap class methods for improving boundaries. - Write some tests for `bootstrap_devsite` command's new behaviour. --- src/utils/bootstrap/base.py | 571 +++++++----------- .../management/commands/bootstrap_devsite.py | 134 +++- src/utils/tests.py | 93 ++- 3 files changed, 444 insertions(+), 354 deletions(-) diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py index d7ec26eba..f041f51ce 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -106,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: @@ -119,43 +137,45 @@ 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]: + """Creates all camps from a list of camps.""" self.output("Creating camps...") camp_instances = [] - - for camp in camps: - year = camp["year"] - read_only = camp["read_only"] - camp_instances.append( - ( - Camp( - title=f"BornHack {year}", - tagline=camp["tagline"], - slug=f"bornhack-{year}", - shortslug=f"bornhack-{year}", - 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=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.objects.bulk_create((c[0] for c in camp_instances)) + + 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") @@ -1998,9 +2018,9 @@ 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(f"Creating token categories...") categories = {} categories["physical"] = TokenCategory( name="Physical", @@ -2028,16 +2048,16 @@ def create_token_categories(self, camp: Camp) -> dict[str, TokenCategory]: update_fields=["description"], unique_fields=["name"], ) - return categories + 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( 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, @@ -2045,7 +2065,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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, @@ -2053,7 +2073,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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, @@ -2061,7 +2081,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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, @@ -2069,7 +2089,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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, @@ -2077,7 +2097,7 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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, @@ -2085,24 +2105,19 @@ def create_camp_tokens(self, camp: Camp, categories: dict) -> dict[Token]: 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}...") finds = [ - TokenFind(token=tokens[3], user=users[4]), - TokenFind(token=tokens[5], user=users[4]), - TokenFind(token=tokens[2], user=users[7]), - TokenFind(token=tokens[1], user=users[3]), - TokenFind(token=tokens[4], user=users[2]), - TokenFind(token=tokens[5], user=users[6]), + 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): - finds.append(TokenFind(token=tokens[i], user=users[1])) + finds.append(TokenFind(token=tokens[i], user=self.users[1])) TokenFind.objects.bulk_create(finds) @@ -2276,347 +2291,174 @@ 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 = [] + default_camp = {'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 range(years_range[0], years_range[1] + 1): + camp = default_camp.copy() + camp["year"] = year + camp["read_only"] = False if year in writable_range else True - 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() + predefined = CAMP_MAP.get(year) + if predefined is not None: + camp.update(predefined) - 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() + dataset.append(camp) - 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, - ) - - self.create_orders(self.users, camp_products) - - self.create_camp_tracks(camp) + return dataset - locations = self.create_event_locations(camp) + def bootstrap_global_data(self) -> None: + """Bootstrap global data for the application.""" + self.output("----------[ Creating global data ]----------") - self.create_camp_news(camp) - - teams = self.create_camp_teams(camp) - self.teams[camp.year] = teams - - 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_event_routing_types() + self.create_event_types() - self.create_camp_team_tasks(camp, teams) + self.create_users(16) - team_memberships = self.create_camp_team_memberships( - camp, - teams, - self.users, - ) + self.create_news() - self.create_camp_team_shifts(camp, teams, team_memberships) + self.create_url_types() - self.create_camp_pos(teams) + self.create_product_categories() - self.create_camp_cfp(camp) + self.create_token_categories() - self.create_camp_proposals(camp, self.event_types) + self.create_quickfeedback_options() - self.create_proposal_urls(camp) + self.create_maps_layer_generic() - self.create_camp_event_sessions(camp, self.event_types, locations) + self.create_mobilepay_transactions() + self.create_clearhaus_settlements() + self.create_credebtors() + self.create_bank_stuff() + self.create_coinify_stuff() + self.create_epay_transactions() - self.generate_speaker_availability(camp) + self.output("----------[ Finished creating global data ]----------") - 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) + def bootstrap_camp(self, camp: Camp, autoschedule: bool=True) -> None: + """Bootstrap camp related entities.""" + permissions_added = False + self.teams = {} + self.output(f"----------[ Bornhack {camp.year} ]----------") - self.approve_event_proposals(camp) + if camp.year > timezone.now().year: + self.output("Not creating anything for this year yet") - self.create_camp_scheduling(camp, not options["skip_auto_scheduler"]) + ticket_types = self.create_camp_ticket_types(camp) - # shuffle it up - delete and create new random availability - self.generate_speaker_availability(camp) + camp_products = self.create_camp_products( + camp, + self.product_categories, + ticket_types, + ) - # and create some speaker<>event conflicts - self.create_camp_speaker_event_conflicts(camp) + self.create_orders(self.users, camp_products) - # recalculate the autoschedule - self.create_camp_rescheduling(camp, not options["skip_auto_scheduler"]) + self.create_camp_tracks(camp) - self.create_camp_villages(camp, self.users) + locations = self.create_event_locations(camp) - facility_types = self.create_facility_types( - teams, - self.quickfeedback_options, - ) + self.create_camp_news(camp) - facilities = self.create_facilities(facility_types) + teams = self.create_camp_teams(camp) + self.teams[camp.year] = teams - self.create_facility_feedbacks( - facilities, - self.quickfeedback_options, - self.users, - ) + 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 - info_categories = self.create_camp_info_categories(camp, teams) + self.create_camp_team_tasks(camp, teams) - self.create_camp_info_items(camp, info_categories) + team_memberships = self.create_camp_team_memberships( + camp, + teams, + self.users, + ) - self.create_camp_feedback(camp, self.users) + self.create_camp_team_shifts(camp, teams, team_memberships) - self.create_camp_rides(camp, self.users) + self.create_camp_pos(teams) - self.create_camp_cfs(camp) + self.create_camp_cfp(camp) - sponsor_tiers = self.create_camp_sponsor_tiers(camp) + self.create_camp_proposals(camp, self.event_types) - camp_sponsors = self.create_camp_sponsors(camp, sponsor_tiers) + self.create_proposal_urls(camp) - categories = self.create_token_categories(camp) + self.create_camp_event_sessions(camp, self.event_types, locations) - tokens = self.create_camp_tokens(camp, categories) + self.generate_speaker_availability(camp) - self.create_camp_token_finds(camp, tokens, self.users) + 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) - self.create_camp_expenses(camp) + self.approve_event_proposals(camp) - self.create_camp_reimbursements(camp) + self.create_camp_scheduling(camp, autoschedule) - self.create_camp_revenues(camp) + # shuffle it up - delete and create new random availability + self.generate_speaker_availability(camp) - self.create_camp_map_layer(camp) - else: - self.output("Not creating anything for this year yet") + # and create some speaker<>event conflicts + self.create_camp_speaker_event_conflicts(camp) - camp.read_only = read_only - camp.call_for_participation_open = not read_only - camp.call_for_sponsors_open = not read_only - camp.save() + # recalculate the autoschedule + self.create_camp_rescheduling(camp, autoschedule) - # Update team permissions. - if camp.year == settings.UPCOMING_CAMP_YEAR: - for member in TeamMember.objects.filter(team__camp=camp): - member.save() + self.create_camp_villages(camp, self.users) - def bootstrap_base(self, options: dict) -> None: - """Bootstrap the data for the application.""" - self.output( - "----------[ Running bootstrap_devsite ]----------", + facility_types = self.create_facility_types( + teams, + self.quickfeedback_options, ) - self.output("----------[ Global stuff ]----------") + facilities = self.create_facilities(facility_types) - self.create_event_routing_types() - self.create_users(16) + self.create_facility_feedbacks( + facilities, + self.quickfeedback_options, + self.users, + ) - self.create_news() + info_categories = self.create_camp_info_categories(camp, teams) - self.create_event_types() + self.create_camp_info_items(camp, info_categories) - self.create_url_types() + self.create_camp_feedback(camp, self.users) - self.create_product_categories() + self.create_camp_rides(camp, self.users) - self.create_quickfeedback_options() + self.create_camp_cfs(camp) - self.create_mobilepay_transactions() + sponsor_tiers = self.create_camp_sponsor_tiers(camp) - self.create_clearhaus_settlements() + self.create_camp_sponsors(camp, sponsor_tiers) - self.create_credebtors() + tokens = self.create_camp_tokens(camp) - self.create_bank_stuff() + self.create_camp_token_finds(camp, tokens) - self.create_coinify_stuff() + self.create_camp_expenses(camp) - self.create_epay_transactions() + self.create_camp_reimbursements(camp) - self.create_maps_layer_generic() + self.create_camp_revenues(camp) - self.bootstrap_camp(options) + self.create_camp_map_layer(camp) + def post_bootstrap(self, writable_range: list): + """Make the last changes after the bootstrapping is done.""" self.output("----------[ Finishing up ]----------") self.output("Adding event routing...") @@ -2630,4 +2472,61 @@ def bootstrap_base(self, options: dict) -> None: eventtype=Type.objects.get(name="ticket_created"), ) - self.output("done!") + # TODO (unicorn): Find out why it update team permissions. + # + # It now updates permissions for current year + # instead of using `UPCOMING_CAMP_YEAR` variable + + for camp in self.camps: + if camp.year not in writable_range: + self.output(f"Set {camp.title} to read-only...") + camp.read_only = True + if camp.year == timezone.now().year: + self.output("Update team permissions...") + for member in TeamMember.objects.filter(team__camp=camp): + member.save() + + Camp.objects.bulk_update(self.camps, fields=["read_only"]) + + 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(16) + 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..ef71ad97b 100644 --- a/src/utils/management/commands/bootstrap_devsite.py +++ b/src/utils/management/commands/bootstrap_devsite.py @@ -1,9 +1,14 @@ from __future__ import annotations import logging +from concurrent.futures import ThreadPoolExecutor +from argparse import ArgumentTypeError from django.core.management import call_command +from django.core.management import CommandError from django.core.management.base import BaseCommand +from django.db import connections +from django.db import transaction from django.utils import timezone from utils.bootstrap.base import Bootstrap @@ -12,32 +17,141 @@ 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=4, + help="Specify amount of threads to be used. Default: 4", + ) 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=[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: + return [int(year.strip()) for year in value.split(",")] + 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.validate(options) + self.decorated_output( + "Flush all data from database", + self.style.WARNING ) 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}!", + self.style.SUCCESS + ) + + 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 years is not None: + if years[0] < 2016: + raise CommandError("When specifying years the lower limit is 2016") + + writable_years = options["writable_years"] + if writable_years is not None: + if writable_years[0] < years[0] or writable_years[1] > years[1]: + raise CommandError( + "When specifying writable years stay within range of years" + ) + + def run(self, bootstrap:Bootstrap, options: dict): + """Bootstrap data using threading.""" + self.decorated_output(f"Running bootstrap_devsite", self.style.SUCCESS) + + years = options["years"] + writable_years = options["writable_years"] + prepared_camps = bootstrap.prepare_camp_list(years, writable_years) + camps = bootstrap.create_camps(prepared_camps) + bootstrap.bootstrap_global_data() + + threads = options["threads"] + self.decorated_output( + f"Bootstrap camp data using {threads} threads", + self.style.SUCCESS + ) + + # Don't bootstrap above last writable camp + BOOTSTRAP_LIMIT = writable_years[1] + limit_logs = [] + schedule = (not options["skip_auto_scheduler"]) + with ThreadPoolExecutor(max_workers=threads) as executor: + for camp in camps: + if camp.year > BOOTSTRAP_LIMIT: + limit_logs.append(f"Not bootstrapping {camp.title}") + else: + executor.submit(self.worker_job, bootstrap, camp, schedule) + + bootstrap.post_bootstrap(writable_years) + + for msg in limit_logs: + self.decorated_output(msg, self.style.WARNING) + + def worker_job(self, bootstrap: Bootstrap, camp, autoschedule: bool): + """Execute concurrent bootstrapping atomically + and always close the db connection. + """ + try: + with transaction.atomic(): + bootstrap.bootstrap_camp(camp, autoschedule) + finally: + connections.close() + + def output(self, message) -> None: + """Format the stdout format for command.""" + self.stdout.write( + "{}: {}".format(timezone.now().strftime("%Y-%m-%d %H:%M:%S"), message), + ) + + def decorated_output(self, msg, style=None): + """Decorate the stdout format with color and ascii art""" + msg = f"----------[ {msg} ]----------" + if style: + msg = style(msg) + self.output(msg) + diff --git a/src/utils/tests.py b/src/utils/tests.py index 45d9d38a5..9c81f0f8e 100644 --- a/src/utils/tests.py +++ b/src/utils/tests.py @@ -2,25 +2,102 @@ from __future__ import annotations +from copy import deepcopy import logging -from unittest import skip +import pytest +from argparse import ArgumentTypeError -from django.core.management import call_command from django.test import Client from django.test import TestCase +from django.core.management import CommandError, call_command +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": [year, (year + 1), (year + 2)], + "years": [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, (year + 1)] + + result = cmd._years(f"{year},{year + 1}") + + 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", years=lower["writable_years"]) + + # Test upper limit + upper = deepcopy(options) + upper["writable_years"][1] = (options["years"][1] + 1) + + with pytest.raises(CommandError): + cmd.validate(upper) + + with pytest.raises(CommandError): + call_command( + "bootstrap_devsite", + writable_years=upper["writable_years"] + ) class BornhackTestBase(TestCase): From 43315687846f95a56ac33b83d4947237d2dd5eae Mon Sep 17 00:00:00 2001 From: Christian Henriksen Date: Wed, 18 Feb 2026 16:01:59 +0100 Subject: [PATCH 3/7] fixup! Implement optional arguments and concurrency. --- src/bornhack/settings.py | 3 - src/utils/bootstrap/base.py | 63 +++++++++---------- .../management/commands/bootstrap_devsite.py | 60 +++++++++++------- src/utils/tests.py | 22 ++++--- 4 files changed, 82 insertions(+), 66 deletions(-) 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 f041f51ce..61516a1ff 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -181,7 +181,7 @@ def create_event_routing_types(self) -> None: 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...") @@ -2296,11 +2296,10 @@ def prepare_camp_list(self, years_range: list, writable_range: list) -> list: dataset = [] default_camp = {'colour': '#424242', 'tagline': 'Undecided'} - for year in range(years_range[0], years_range[1] + 1): + for year in years_range: camp = default_camp.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) @@ -2309,37 +2308,43 @@ def prepare_camp_list(self, years_range: list, writable_range: list) -> list: return dataset - def bootstrap_global_data(self) -> None: + def bootstrap_global_data(self, prepared_camps: list) -> None: """Bootstrap global data for the application.""" self.output("----------[ Creating global data ]----------") self.create_event_routing_types() self.create_event_types() - - self.create_users(16) - - self.create_news() - self.create_url_types() - self.create_product_categories() + self.create_users() + self.create_camps(prepared_camps) - self.create_token_categories() + self.create_news() self.create_quickfeedback_options() self.create_maps_layer_generic() + self.create_credebtors() + self.create_mobilepay_transactions() self.create_clearhaus_settlements() - self.create_credebtors() + self.create_epay_transactions() + self.create_bank_stuff() self.create_coinify_stuff() - self.create_epay_transactions() + + self.create_product_categories() + self.create_token_categories() self.output("----------[ Finished creating global data ]----------") - def bootstrap_camp(self, camp: Camp, autoschedule: bool=True) -> None: + def bootstrap_camp( + self, + camp: Camp, + autoschedule: bool = True, + read_only: bool = True + ) -> None: """Bootstrap camp related entities.""" permissions_added = False self.teams = {} @@ -2457,7 +2462,17 @@ def bootstrap_camp(self, camp: Camp, autoschedule: bool=True) -> None: self.create_camp_map_layer(camp) - def post_bootstrap(self, writable_range: list): + if read_only: + self.output(f"Set {camp.title} to read-only...") + camp.read_only = True + camp.save(update_fields=["read_only"]) + + if camp.year == timezone.now().year: + self.output("Update team permissions...") + for member in TeamMember.objects.filter(team__camp=camp): + member.save() + + def post_bootstrap(self): """Make the last changes after the bootstrapping is done.""" self.output("----------[ Finishing up ]----------") @@ -2472,29 +2487,13 @@ def post_bootstrap(self, writable_range: list): eventtype=Type.objects.get(name="ticket_created"), ) - # TODO (unicorn): Find out why it update team permissions. - # - # It now updates permissions for current year - # instead of using `UPCOMING_CAMP_YEAR` variable - - for camp in self.camps: - if camp.year not in writable_range: - self.output(f"Set {camp.title} to read-only...") - camp.read_only = True - if camp.year == timezone.now().year: - self.output("Update team permissions...") - for member in TeamMember.objects.filter(team__camp=camp): - member.save() - - Camp.objects.bulk_update(self.camps, fields=["read_only"]) - 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(16) + self.create_users() self.create_event_types() self.create_product_categories() teams = {} diff --git a/src/utils/management/commands/bootstrap_devsite.py b/src/utils/management/commands/bootstrap_devsite.py index ef71ad97b..73efb928e 100644 --- a/src/utils/management/commands/bootstrap_devsite.py +++ b/src/utils/management/commands/bootstrap_devsite.py @@ -11,6 +11,7 @@ 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__}") @@ -27,8 +28,8 @@ def add_arguments(self, parser) -> None: "-t", "--threads", type=int, - default=4, - help="Specify amount of threads to be used. Default: 4", + default=2, + help="Specify amount of threads to be used. Default: 2", ) parser.add_argument( "-s", @@ -54,7 +55,8 @@ def add_arguments(self, parser) -> None: def _years(self, value: str) -> list[int]: """Transform str argument to list of years or raise exception.""" try: - return [int(year.strip()) for year in value.split(",")] + years = [int(year.strip()) for year in value.split(",")] + return [year for year in range(years[0], years[-1] + 1)] except ValueError: raise ArgumentTypeError( "Years must be comma separated integers (e.g. 2026,2027)" @@ -90,25 +92,24 @@ def validate(self, options: dict): years = options["years"] if years is not None: - if years[0] < 2016: + if min(years) < 2016: raise CommandError("When specifying years the lower limit is 2016") - writable_years = options["writable_years"] - if writable_years is not None: - if writable_years[0] < years[0] or writable_years[1] > years[1]: + writables = options["writable_years"] + if writables is not None: + if min(writables) < min(years) or max(writables) > max(years): raise CommandError( "When specifying writable years stay within range of years" ) - def run(self, bootstrap:Bootstrap, options: dict): + def run(self, bootstrap: Bootstrap, options: dict): """Bootstrap data using threading.""" self.decorated_output(f"Running bootstrap_devsite", self.style.SUCCESS) years = options["years"] writable_years = options["writable_years"] prepared_camps = bootstrap.prepare_camp_list(years, writable_years) - camps = bootstrap.create_camps(prepared_camps) - bootstrap.bootstrap_global_data() + bootstrap.bootstrap_global_data(prepared_camps) threads = options["threads"] self.decorated_output( @@ -117,41 +118,52 @@ def run(self, bootstrap:Bootstrap, options: dict): ) # Don't bootstrap above last writable camp - BOOTSTRAP_LIMIT = writable_years[1] + BOOTSTRAP_LIMIT = writable_years[-1] limit_logs = [] - schedule = (not options["skip_auto_scheduler"]) with ThreadPoolExecutor(max_workers=threads) as executor: - for camp in camps: + for camp in bootstrap.camps: if camp.year > BOOTSTRAP_LIMIT: limit_logs.append(f"Not bootstrapping {camp.title}") else: - executor.submit(self.worker_job, bootstrap, camp, schedule) + executor.submit( + self.worker_job, + bootstrap, + camp, + (not options["skip_auto_scheduler"]), + False if camp.year in writable_years else True + ) - bootstrap.post_bootstrap(writable_years) + bootstrap.post_bootstrap() for msg in limit_logs: self.decorated_output(msg, self.style.WARNING) - def worker_job(self, bootstrap: Bootstrap, camp, autoschedule: bool): + def worker_job( + self, + bootstrap: Bootstrap, + camp: Camp, + schedule: bool, + read_only: bool + ): """Execute concurrent bootstrapping atomically and always close the db connection. """ try: with transaction.atomic(): - bootstrap.bootstrap_camp(camp, autoschedule) + bootstrap.bootstrap_camp(camp, schedule, read_only) finally: connections.close() - def output(self, message) -> None: - """Format the stdout format for command.""" - self.stdout.write( - "{}: {}".format(timezone.now().strftime("%Y-%m-%d %H:%M:%S"), message), - ) - def decorated_output(self, msg, style=None): - """Decorate the stdout format with color and ascii art""" + """Decorate the stdout format with color and ascii art.""" msg = f"----------[ {msg} ]----------" if style: msg = style(msg) self.output(msg) + def output(self, message) -> None: + """Format the stdout format for command.""" + self.stdout.write( + "{}: {}".format(timezone.now().strftime("%Y-%m-%d %H:%M:%S"), message), + ) + diff --git a/src/utils/tests.py b/src/utils/tests.py index 9c81f0f8e..c06dcae33 100644 --- a/src/utils/tests.py +++ b/src/utils/tests.py @@ -28,17 +28,17 @@ def options(self) -> dict: return { "threads": 4, "skip_auto_scheduler": False, - "writable_years": [year, (year + 1), (year + 2)], - "years": [2016, year + 6], + "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, (year + 1)] + expected = [year for year in range(year, (year + 3))] - result = cmd._years(f"{year},{year + 1}") + result = cmd._years(f"{year},{year + 2}") assert result == expected @@ -84,11 +84,18 @@ def test_validating_writable_years_is_within_range(self, options): cmd.validate(lower) with pytest.raises(CommandError): - call_command("bootstrap_devsite", years=lower["writable_years"]) + call_command( + "bootstrap_devsite", + writable_years=lower["writable_years"], + years=lower["years"] + ) # Test upper limit upper = deepcopy(options) - upper["writable_years"][1] = (options["years"][1] + 1) + upper["writable_years"] = [ + i for i in + range(min(upper["years"]), max(upper["years"]) + 2) + ] with pytest.raises(CommandError): cmd.validate(upper) @@ -96,7 +103,8 @@ def test_validating_writable_years_is_within_range(self, options): with pytest.raises(CommandError): call_command( "bootstrap_devsite", - writable_years=upper["writable_years"] + writable_years=upper["writable_years"], + years=upper["years"] ) From 2b693267500b43bfcf7a0f7dd5c521909516eb81 Mon Sep 17 00:00:00 2001 From: Christian Henriksen Date: Wed, 18 Feb 2026 19:54:15 +0100 Subject: [PATCH 4/7] fixup! Implement optional arguments and concurrency. --- src/utils/bootstrap/base.py | 6 +-- .../management/commands/bootstrap_devsite.py | 37 ++++++++++++------- 2 files changed, 26 insertions(+), 17 deletions(-) diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py index 61516a1ff..d4fb29719 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -138,7 +138,7 @@ class Bootstrap: quickfeedback_options: dict def create_camps(self, camps_list: list) -> list[Camp]: - """Creates all camps from a list of camps.""" + """Create camps from a list.""" self.output("Creating camps...") camp_instances = [] @@ -2294,10 +2294,10 @@ def output(self, message: str) -> None: def prepare_camp_list(self, years_range: list, writable_range: list) -> list: """Prepare camp dataset for bootstrapping.""" dataset = [] - default_camp = {'colour': '#424242', 'tagline': 'Undecided'} + template = {'colour': '#424242', 'tagline': 'Undecided'} for year in years_range: - camp = default_camp.copy() + camp = template.copy() camp["year"] = year camp["read_only"] = False if year in writable_range else True predefined = CAMP_MAP.get(year) diff --git a/src/utils/management/commands/bootstrap_devsite.py b/src/utils/management/commands/bootstrap_devsite.py index 73efb928e..2aaab9da6 100644 --- a/src/utils/management/commands/bootstrap_devsite.py +++ b/src/utils/management/commands/bootstrap_devsite.py @@ -48,7 +48,7 @@ def add_arguments(self, parser) -> None: "-y", "--years", type=self._years, - default=[2016, timezone.now().year + 6], + default=[i for i in range(2016, 2032)], help="Comma separated range of camp years. Example: 2016,2032", ) @@ -56,7 +56,7 @@ 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(years[0], years[-1] + 1)] + 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)" @@ -67,10 +67,7 @@ def handle(self, *args, **options) -> None: start = timezone.now() self.validate(options) - self.decorated_output( - "Flush all data from database", - self.style.WARNING - ) + self.decorated_output("Flush all data from database", "yellow") call_command("flush", "--noinput") bootstrap = Bootstrap() @@ -80,7 +77,7 @@ def handle(self, *args, **options) -> None: duration = timezone.now() - start self.decorated_output( f"Finished bootstrap_devsite in {duration}!", - self.style.SUCCESS + "green" ) def validate(self, options: dict): @@ -104,26 +101,29 @@ def validate(self, options: dict): def run(self, bootstrap: Bootstrap, options: dict): """Bootstrap data using threading.""" - self.decorated_output(f"Running bootstrap_devsite", self.style.SUCCESS) + self.decorated_output(f"Running bootstrap_devsite", "green") years = options["years"] writable_years = options["writable_years"] prepared_camps = bootstrap.prepare_camp_list(years, writable_years) bootstrap.bootstrap_global_data(prepared_camps) + self.decorated_output(years, "red") threads = options["threads"] self.decorated_output( f"Bootstrap camp data using {threads} threads", - self.style.SUCCESS + "green" ) # Don't bootstrap above last writable camp BOOTSTRAP_LIMIT = writable_years[-1] - limit_logs = [] + bootstrap_logs = [] with ThreadPoolExecutor(max_workers=threads) as executor: for camp in bootstrap.camps: if camp.year > BOOTSTRAP_LIMIT: - limit_logs.append(f"Not bootstrapping {camp.title}") + bootstrap_logs.append( + (f"Skipping bootstrap for {camp.title}", "yellow") + ) else: executor.submit( self.worker_job, @@ -132,11 +132,14 @@ def run(self, bootstrap: Bootstrap, options: dict): (not options["skip_auto_scheduler"]), False if camp.year in writable_years else True ) + bootstrap_logs.append( + (f"Completed bootstrapping of {camp.title}", "green") + ) bootstrap.post_bootstrap() - for msg in limit_logs: - self.decorated_output(msg, self.style.WARNING) + for msg, style in bootstrap_logs: + self.decorated_output(msg, style) def worker_job( self, @@ -158,7 +161,13 @@ def decorated_output(self, msg, style=None): """Decorate the stdout format with color and ascii art.""" msg = f"----------[ {msg} ]----------" if style: - msg = style(msg) + color_map = { + "red": self.style.ERROR, + "yellow": self.style.WARNING, + "green": self.style.SUCCESS, + } + color = color_map.get(style, self.style.NOTICE) + msg = color(msg) self.output(msg) def output(self, message) -> None: From 16b8220d8bc63ae44818addd8e935a9286a38b80 Mon Sep 17 00:00:00 2001 From: Christian Henriksen Date: Wed, 18 Feb 2026 22:10:44 +0100 Subject: [PATCH 5/7] Wire up verbosity arg and improve logging. - Verbosity arg is implemented in Django and can be used to silence noisy DEBUG/INFO logs - Improve logging methods, add and use more colors and move some logging from `bootstrap.base` to `bootstrap_devsite`. - Fix hardcoded default range ending. --- README.md | 3 +- src/utils/bootstrap/base.py | 13 +--- .../management/commands/bootstrap_devsite.py | 78 +++++++++++-------- 3 files changed, 52 insertions(+), 42 deletions(-) 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/utils/bootstrap/base.py b/src/utils/bootstrap/base.py index d4fb29719..abb8ce2fe 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -2310,8 +2310,6 @@ def prepare_camp_list(self, years_range: list, writable_range: list) -> list: def bootstrap_global_data(self, prepared_camps: list) -> None: """Bootstrap global data for the application.""" - self.output("----------[ Creating global data ]----------") - self.create_event_routing_types() self.create_event_types() self.create_url_types() @@ -2337,8 +2335,6 @@ def bootstrap_global_data(self, prepared_camps: list) -> None: self.create_product_categories() self.create_token_categories() - self.output("----------[ Finished creating global data ]----------") - def bootstrap_camp( self, camp: Camp, @@ -2348,7 +2344,6 @@ def bootstrap_camp( """Bootstrap camp related entities.""" permissions_added = False self.teams = {} - self.output(f"----------[ Bornhack {camp.year} ]----------") if camp.year > timezone.now().year: self.output("Not creating anything for this year yet") @@ -2463,20 +2458,18 @@ def bootstrap_camp( self.create_camp_map_layer(camp) if read_only: - self.output(f"Set {camp.title} to read-only...") + self.output(f"Updating {camp.title} to read-only...") camp.read_only = True camp.save(update_fields=["read_only"]) if camp.year == timezone.now().year: - self.output("Update team permissions...") + self.output("Updating team permissions...") for member in TeamMember.objects.filter(team__camp=camp): member.save() def post_bootstrap(self): """Make the last changes after the bootstrapping is done.""" - self.output("----------[ Finishing up ]----------") - - self.output("Adding event routing...") + self.output("Creating event routing...") teams = self.teams[next(reversed(self.teams.keys()))] Routing.objects.create( team=teams["orga"], diff --git a/src/utils/management/commands/bootstrap_devsite.py b/src/utils/management/commands/bootstrap_devsite.py index 2aaab9da6..d107339f1 100644 --- a/src/utils/management/commands/bootstrap_devsite.py +++ b/src/utils/management/commands/bootstrap_devsite.py @@ -15,7 +15,12 @@ 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.""" @@ -48,7 +53,7 @@ def add_arguments(self, parser) -> None: "-y", "--years", type=self._years, - default=[i for i in range(2016, 2032)], + default=[i for i in range(2016, timezone.now().year + 6)], help="Comma separated range of camp years. Example: 2016,2032", ) @@ -65,9 +70,15 @@ def _years(self, value: str) -> list[int]: def handle(self, *args, **options) -> None: """Flush database and run bootstrapper.""" start = timezone.now() + self.decorated_output(f"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", "yellow") + + self.decorated_output("Flush all data from database", "purple") call_command("flush", "--noinput") bootstrap = Bootstrap() @@ -77,7 +88,7 @@ def handle(self, *args, **options) -> None: duration = timezone.now() - start self.decorated_output( f"Finished bootstrap_devsite in {duration}!", - "green" + "cyan" ) def validate(self, options: dict): @@ -88,31 +99,30 @@ def validate(self, options: dict): raise CommandError("When specifying threads it must be above 0") years = options["years"] - if years is not None: - if min(years) < 2016: - raise CommandError("When specifying years the lower limit is 2016") + if min(years) < 2016: + raise CommandError("When specifying years the lower limit is 2016") writables = options["writable_years"] - if writables is not None: - if min(writables) < min(years) or max(writables) > max(years): - raise CommandError( - "When specifying writable years stay within range of 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.""" - self.decorated_output(f"Running bootstrap_devsite", "green") - 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(years, "red") + self.decorated_output("Finished creating global data", "green") threads = options["threads"] self.decorated_output( f"Bootstrap camp data using {threads} threads", - "green" + "purple" ) # Don't bootstrap above last writable camp @@ -136,7 +146,9 @@ def run(self, bootstrap: Bootstrap, options: dict): (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) @@ -151,28 +163,32 @@ def worker_job( """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, style=None): - """Decorate the stdout format with color and ascii art.""" + def decorated_output(self, msg, color="white"): + """Decorate stdout with colored text and ascii art.""" msg = f"----------[ {msg} ]----------" - if style: - color_map = { - "red": self.style.ERROR, - "yellow": self.style.WARNING, - "green": self.style.SUCCESS, - } - color = color_map.get(style, self.style.NOTICE) - msg = color(msg) - self.output(msg) - - def output(self, message) -> None: - """Format the stdout format for command.""" + 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"), message), + "{}: {}".format( + timezone.now().strftime("%Y-%m-%d %H:%M:%S"), color(msg) + ), ) From d44548dd2a52a92f08fde8ae8a1a9fe7bcc1a5ca Mon Sep 17 00:00:00 2001 From: Christian Henriksen Date: Wed, 18 Feb 2026 22:39:54 +0100 Subject: [PATCH 6/7] fixup! Implement `bulk_create` to shave off ~23s --- src/utils/bootstrap/base.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py index abb8ce2fe..185b24159 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -674,6 +674,14 @@ def create_camp_ticket_types(self, camp: Camp) -> dict: 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 @@ -2458,7 +2466,7 @@ def bootstrap_camp( self.create_camp_map_layer(camp) if read_only: - self.output(f"Updating {camp.title} to read-only...") + self.output(f"Updatin {camp.title} to read-only...") camp.read_only = True camp.save(update_fields=["read_only"]) From d24375469a0cdbe0885b31eacf533cc814257f36 Mon Sep 17 00:00:00 2001 From: Christian Henriksen Date: Wed, 18 Feb 2026 22:50:02 +0100 Subject: [PATCH 7/7] Manually run pre-commit on changed files --- src/utils/bootstrap/base.py | 144 +++++++++--------- .../management/commands/bootstrap_devsite.py | 32 ++-- src/utils/tests.py | 20 ++- 3 files changed, 97 insertions(+), 99 deletions(-) diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py index 185b24159..285e43839 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -6,14 +6,12 @@ import random import sys import uuid -from zoneinfo import ZoneInfo +from collections import defaultdict from datetime import datetime from datetime import timedelta -from collections import defaultdict from itertools import chain -from psycopg2.extras import DateTimeTZRange +from zoneinfo import ZoneInfo -from django.utils.text import slugify import factory from allauth.account.models import EmailAddress from django.conf import settings @@ -26,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 @@ -49,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 @@ -107,22 +107,22 @@ 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'} + 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"}, } @@ -403,7 +403,7 @@ def create_facility_feedbacks( quick_feedback=options["power"], comment="No power, please help", urgent=True, - ) + ), ] FacilityFeedback.objects.bulk_create(feedback) @@ -680,7 +680,7 @@ def create_camp_ticket_types(self, camp: Camp) -> dict: "ticket_type_one_day_adult", "ticket_type_full_week_child", "ticket_type_one_day_child", - ] + ], ) return types @@ -1046,7 +1046,7 @@ def create_camp_news(self, camp: Camp) -> None: 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) @@ -1112,15 +1112,15 @@ def create_camp_event_sessions( ), ) EventSession.objects.create( - 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), - ), - ) + 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), + ), + ) # create sessions for the keynotes for day in [days[1], days[3], days[5]]: EventSession.objects.create( @@ -1410,7 +1410,7 @@ def create_camp_villages(self, camp: Camp, users: dict) -> None: 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) @@ -1494,19 +1494,19 @@ def create_camp_team_tasks(self, camp: Camp, teams: dict) -> None: team=teams["noc"], name="Setup private networks", description="All the private networks need to be setup", - slug=slugify(f"{camp.year} Setup private networks") + 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") + 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") + slug=slugify(f"{camp.year} Deploy access points"), ), TeamTask( team=teams["noc"], @@ -1564,27 +1564,27 @@ def create_camp_team_memberships( user=users[4], approved=True, lead=True, - ) + ), ) memberships["noc"].append( TeamMember( team=teams["noc"], user=users[1], approved=True, - ) + ), ) memberships["noc"].append( TeamMember( team=teams["noc"], user=users[5], approved=True, - ) + ), ) memberships["noc"].append( TeamMember( team=teams["noc"], user=users[2], - ) + ), ) # bar team @@ -1594,7 +1594,7 @@ def create_camp_team_memberships( user=users[1], approved=True, lead=True, - ) + ), ) memberships["bar"].append( TeamMember( @@ -1602,28 +1602,27 @@ def create_camp_team_memberships( user=users[3], approved=True, lead=True, - ) + ), ) memberships["bar"].append( TeamMember( team=teams["bar"], user=users[2], approved=True, - ) + ), ) memberships["bar"].append( TeamMember( team=teams["bar"], user=users[7], approved=True, - ) + ), ) memberships["bar"].append( TeamMember( team=teams["bar"], user=users[8], - ) - + ), ) # orga team memberships["orga"].append( @@ -1632,7 +1631,7 @@ def create_camp_team_memberships( user=users[8], approved=True, lead=True, - ) + ), ) memberships["orga"].append( TeamMember( @@ -1640,7 +1639,7 @@ def create_camp_team_memberships( user=users[9], approved=True, lead=True, - ) + ), ) memberships["orga"].append( TeamMember( @@ -1648,7 +1647,7 @@ def create_camp_team_memberships( user=users[4], approved=True, lead=True, - ) + ), ) # shuttle team @@ -1658,21 +1657,20 @@ def create_camp_team_memberships( user=users[7], approved=True, lead=True, - ) + ), ) memberships["shuttle"].append( TeamMember( team=teams["shuttle"], user=users[3], approved=True, - ) + ), ) memberships["shuttle"].append( TeamMember( team=teams["shuttle"], user=users[9], - ) - + ), ) # economy team also gets a member memberships["economy"].append( @@ -1681,7 +1679,7 @@ def create_camp_team_memberships( user=users[0], lead=True, approved=True, - ) + ), ) # gis team also gets a member @@ -1691,7 +1689,7 @@ def create_camp_team_memberships( user=users[0], lead=True, approved=True, - ) + ), ) all_members = list(chain.from_iterable(memberships.values())) @@ -2028,7 +2026,7 @@ def create_camp_sponsor_tickets( def create_token_categories(self) -> None: """Create the camp tokens.""" - self.output(f"Creating token categories...") + self.output("Creating token categories...") categories = {} categories["physical"] = TokenCategory( name="Physical", @@ -2238,15 +2236,17 @@ 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", @@ -2291,7 +2291,7 @@ def create_camp_pos(self, teams: dict[Team]) -> None: name="Bar", team=teams["bar"], external_id="bTasxE2YYXZh35wtQ", - ) + ), ] Pos.objects.bulk_create(pos) @@ -2302,7 +2302,7 @@ def output(self, message: str) -> None: def prepare_camp_list(self, years_range: list, writable_range: list) -> list: """Prepare camp dataset for bootstrapping.""" dataset = [] - template = {'colour': '#424242', 'tagline': 'Undecided'} + template = {"colour": "#424242", "tagline": "Undecided"} for year in years_range: camp = template.copy() @@ -2347,7 +2347,7 @@ def bootstrap_camp( self, camp: Camp, autoschedule: bool = True, - read_only: bool = True + read_only: bool = True, ) -> None: """Bootstrap camp related entities.""" permissions_added = False @@ -2406,8 +2406,7 @@ def bootstrap_camp( 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 :)", + "Name collision, bad luck. Run the bootstrap script again! PRs to make this less annoying welcome :)", ) sys.exit(1) @@ -2491,7 +2490,7 @@ def post_bootstrap(self): def bootstrap_tests(self) -> None: """Method for bootstrapping the test database.""" year = timezone.now().year - year_range = [(year -1), year, (year + 1)] + year_range = [(year - 1), year, (year + 1)] camps = self.prepare_camp_list(year_range, [year]) self.create_camps(camps) self.create_users() @@ -2529,4 +2528,3 @@ def bootstrap_tests(self) -> None: 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 d107339f1..35b044605 100644 --- a/src/utils/management/commands/bootstrap_devsite.py +++ b/src/utils/management/commands/bootstrap_devsite.py @@ -1,11 +1,11 @@ from __future__ import annotations import logging -from concurrent.futures import ThreadPoolExecutor from argparse import ArgumentTypeError +from concurrent.futures import ThreadPoolExecutor -from django.core.management import call_command 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 @@ -22,8 +22,10 @@ 3: logging.DEBUG, } + class Command(BaseCommand): """Class for `bootstrap_devsite` command.""" + args = "none" help = "Create mock data for development instances" @@ -64,16 +66,16 @@ def _years(self, value: str) -> list[int]: 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)" + "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.decorated_output(f"Running bootstrap_devsite", "cyan") + self.decorated_output("Running bootstrap_devsite", "cyan") logging.getLogger( - "bornhack" + "bornhack", ).setLevel(VERBOSITY_LOG_LEVELS.get(options["verbosity"], 1)) self.validate(options) @@ -88,7 +90,7 @@ def handle(self, *args, **options) -> None: duration = timezone.now() - start self.decorated_output( f"Finished bootstrap_devsite in {duration}!", - "cyan" + "cyan", ) def validate(self, options: dict): @@ -106,7 +108,7 @@ def validate(self, options: dict): 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." + "\nUse (-w/--writable-years YYYY,YYYY) for manual override.", ) def run(self, bootstrap: Bootstrap, options: dict): @@ -122,7 +124,7 @@ def run(self, bootstrap: Bootstrap, options: dict): threads = options["threads"] self.decorated_output( f"Bootstrap camp data using {threads} threads", - "purple" + "purple", ) # Don't bootstrap above last writable camp @@ -132,7 +134,7 @@ def run(self, bootstrap: Bootstrap, options: dict): for camp in bootstrap.camps: if camp.year > BOOTSTRAP_LIMIT: bootstrap_logs.append( - (f"Skipping bootstrap for {camp.title}", "yellow") + (f"Skipping bootstrap for {camp.title}", "yellow"), ) else: executor.submit( @@ -140,10 +142,10 @@ def run(self, bootstrap: Bootstrap, options: dict): bootstrap, camp, (not options["skip_auto_scheduler"]), - False if camp.year in writable_years else True + False if camp.year in writable_years else True, ) bootstrap_logs.append( - (f"Completed bootstrapping of {camp.title}", "green") + (f"Completed bootstrapping of {camp.title}", "green"), ) self.decorated_output("Running post bootstrap tasks", "purple") @@ -158,7 +160,7 @@ def worker_job( bootstrap: Bootstrap, camp: Camp, schedule: bool, - read_only: bool + read_only: bool, ): """Execute concurrent bootstrapping atomically and always close the db connection. @@ -181,14 +183,14 @@ def output(self, msg, color="white") -> None: "red": self.style.ERROR, "yellow": self.style.WARNING, "green": self.style.SUCCESS, - "white": self.style.HTTP_INFO, # DEFAULT/FALLBACK + "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) + timezone.now().strftime("%Y-%m-%d %H:%M:%S"), + color(msg), ), ) - diff --git a/src/utils/tests.py b/src/utils/tests.py index c06dcae33..e47ebcc04 100644 --- a/src/utils/tests.py +++ b/src/utils/tests.py @@ -2,14 +2,15 @@ from __future__ import annotations -from copy import deepcopy import logging -import pytest 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.core.management import CommandError, call_command from django.utils import timezone from camps.models import Camp @@ -21,7 +22,7 @@ class TestBootstrapDevsiteCommand: """Test bootstrap_devsite command.""" - @pytest.fixture() + @pytest.fixture def options(self) -> dict: """Fixture for default options.""" year = timezone.now().year @@ -78,7 +79,7 @@ def test_validating_writable_years_is_within_range(self, options): # Test lower limit lower = deepcopy(options) - lower["writable_years"][0] = (options["years"][0] - 1) + lower["writable_years"][0] = options["years"][0] - 1 with pytest.raises(CommandError): cmd.validate(lower) @@ -87,15 +88,12 @@ def test_validating_writable_years_is_within_range(self, options): call_command( "bootstrap_devsite", writable_years=lower["writable_years"], - years=lower["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) - ] + upper["writable_years"] = [i for i in range(min(upper["years"]), max(upper["years"]) + 2)] with pytest.raises(CommandError): cmd.validate(upper) @@ -104,7 +102,7 @@ def test_validating_writable_years_is_within_range(self, options): call_command( "bootstrap_devsite", writable_years=upper["writable_years"], - years=upper["years"] + years=upper["years"], )