From 9e57946d569b1a01c5541cd578b3ae4c9801cbc2 Mon Sep 17 00:00:00 2001 From: "Rudy (zarya)" Date: Fri, 16 May 2025 12:38:54 +0200 Subject: [PATCH 01/10] WIP: initial linting and testing --- src/teams/templates/team_shift_list.html | 4 +- src/teams/templates/team_tasks.html | 2 +- src/teams/tests/__init__.py | 0 src/teams/tests/test_views.py | 32 ++++++ src/teams/views/guide.py | 8 +- src/teams/views/members.py | 4 +- src/teams/views/mixins.py | 23 ++++- src/teams/views/shifts.py | 119 ++++++++++++++++------- src/teams/views/tasks.py | 42 ++++++-- 9 files changed, 178 insertions(+), 56 deletions(-) create mode 100644 src/teams/tests/__init__.py create mode 100644 src/teams/tests/test_views.py diff --git a/src/teams/templates/team_shift_list.html b/src/teams/templates/team_shift_list.html index 6e435f904..680b7e4be 100644 --- a/src/teams/templates/team_shift_list.html +++ b/src/teams/templates/team_shift_list.html @@ -19,12 +19,12 @@ {% endif %} - +
{% for shift in shifts %} {% ifchanged shift.shift_range.lower|date:'d' %} -
+

{{ shift.shift_range.lower|date:'Y-m-d l' }}

diff --git a/src/teams/templates/team_tasks.html b/src/teams/templates/team_tasks.html index 5cb959455..7ec1a4cde 100644 --- a/src/teams/templates/team_tasks.html +++ b/src/teams/templates/team_tasks.html @@ -20,7 +20,7 @@

Tasks

Create Task {% endif %}

- +
diff --git a/src/teams/tests/__init__.py b/src/teams/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/teams/tests/test_views.py b/src/teams/tests/test_views.py new file mode 100644 index 000000000..851e0fc19 --- /dev/null +++ b/src/teams/tests/test_views.py @@ -0,0 +1,32 @@ +"""Test cases for the Maps application.""" + +from __future__ import annotations + +from bs4 import BeautifulSoup +from django.urls import reverse + +from teams.models import Team +from utils.tests import BornhackTestBase + +class TeamGuideViewTest(BornhackTestBase): + """Test Team Guide View""" + + @classmethod + def setUpTestData(cls) -> None: + """Setup test data.""" + # first add users and other basics + super().setUpTestData() + + + def test_team_guide_views_permission(self) -> None: + """Test the team guide view.""" + self.client.force_login(self.users[0]) + + url = reverse("teams:guide", kwargs={"team_slug": self.team.slug, "camp_slug": self.camp.slug}) + response = self.client.get(path=url, follow=True) + assert response.status_code == 403 + + url = reverse("teams:guide_print", kwargs={"team_slug": self.team.slug, "camp_slug": self.camp.slug}) + response = self.client.get(path=url, follow=True) + assert response.status_code == 403 + diff --git a/src/teams/views/guide.py b/src/teams/views/guide.py index 4a41d3afc..bf31b02b5 100644 --- a/src/teams/views/guide.py +++ b/src/teams/views/guide.py @@ -1,3 +1,4 @@ +"""Views for the guide of the teams application.""" from __future__ import annotations from django.contrib.auth.mixins import LoginRequiredMixin @@ -10,6 +11,7 @@ class TeamGuideView(LoginRequiredMixin, CampViewMixin, UserPassesTestMixin, DetailView): + """View for the team guide.""" template_name = "team_guide.html" context_object_name = "team" model = Team @@ -17,7 +19,7 @@ class TeamGuideView(LoginRequiredMixin, CampViewMixin, UserPassesTestMixin, Deta active_menu = "guide" def test_func(self) -> bool: - # Make sure that the user is an approved member of the team + """Method to test if the user is approved for this team.""" try: TeamMember.objects.get( user=self.request.user, @@ -31,4 +33,8 @@ def test_func(self) -> bool: class TeamGuidePrintView(TeamGuideView): + """View for printing the team guide. + + Includes permissions from TeamGuideView + """ template_name = "team_guide_print.html" diff --git a/src/teams/views/members.py b/src/teams/views/members.py index e5e5573b6..36327b12e 100644 --- a/src/teams/views/members.py +++ b/src/teams/views/members.py @@ -22,6 +22,7 @@ class TeamMembersView(CampViewMixin, DetailView): + """List view for team members.""" template_name = "team_members.html" context_object_name = "team" model = Team @@ -30,13 +31,14 @@ class TeamMembersView(CampViewMixin, DetailView): class TeamJoinView(LoginRequiredMixin, CampViewMixin, UpdateView): + """View displayed when joining a team.""" template_name = "team_join.html" model = Team fields = [] slug_url_kwarg = "team_slug" active_menu = "members" - def get(self, request, *args, **kwargs): + def get(self, request, *args, **kwargs) ->HttpResponse: if not Profile.objects.get(user=request.user).description: messages.warning( request, diff --git a/src/teams/views/mixins.py b/src/teams/views/mixins.py index 0babbb86d..2ffc3388f 100644 --- a/src/teams/views/mixins.py +++ b/src/teams/views/mixins.py @@ -1,5 +1,8 @@ +"""Mixins for the teams application views.""" from __future__ import annotations +from typing import TYPE_CHECKING + from django.contrib import messages from django.shortcuts import redirect from django.views.generic.detail import SingleObjectMixin @@ -9,11 +12,15 @@ from teams.models import TeamMember from utils.mixins import RaisePermissionRequiredMixin +if TYPE_CHECKING: + from django.http import HttpRequest + from django.http import HttpResponse class EnsureTeamLeadMixin: """Use to make sure request.user has team lead permission for the team specified by kwargs['team_slug'].""" - def dispatch(self, request, *args, **kwargs): + def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """Method to make sure request.user has team lead permission for the team specified by kwargs['team_slug'].""" self.team = Team.objects.get(slug=kwargs["team_slug"], camp=self.camp) if self.team.lead_permission_set not in request.user.get_all_permissions(): messages.error(request, "No thanks") @@ -31,7 +38,8 @@ class EnsureTeamMemberLeadMixin(SingleObjectMixin): model = TeamMember - def dispatch(self, request, *args, **kwargs): + def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """Method to make sure request.user has team lead permission for the team which TeamMember belongs to.""" if self.get_object().team.lead_permission_set not in request.user.get_all_permissions(): messages.error(request, "No thanks") return redirect( @@ -44,11 +52,14 @@ def dispatch(self, request, *args, **kwargs): class TeamViewMixin(CampViewMixin): + """View mixin for all Team views.""" def setup(self, *args, **kwargs) -> None: + """Method for setting team object.""" super().setup(*args, **kwargs) self.team = Team.objects.get(slug=kwargs["team_slug"], camp=self.camp) - def get_context_data(self, *args, **kwargs): + def get_context_data(self, *args, **kwargs) -> dict: + """Method for setting team object in the context.""" context = super().get_context_data(**kwargs) context["team"] = self.team return context @@ -57,12 +68,14 @@ def get_context_data(self, *args, **kwargs): class TeamInfopagerPermissionMixin(RaisePermissionRequiredMixin): """Permission mixin for views restricted to users with infopager permission for self.team.""" - def get_permission_required(self): + def get_permission_required(self) -> list: + """Method to restricted to users with infopager permission for self.team.""" return [self.team.infopager_permission_set] class TeamTaskerPermissionMixin(RaisePermissionRequiredMixin): """Permission mixin for views restricted to users with tasker permission for self.team.""" - def get_permission_required(self): + def get_permission_required(self) -> list: + """Method to restricted to users with tasker permission for self.team.""" return [self.team.tasker_permission_set] diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py index acfe0af69..a7e34b488 100644 --- a/src/teams/views/shifts.py +++ b/src/teams/views/shifts.py @@ -1,5 +1,8 @@ +"""View for shifts in teams application.""" from __future__ import annotations +from typing import TYPE_CHECKING + from django import forms from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin @@ -22,18 +25,28 @@ from teams.models import TeamMember from teams.models import TeamShift +if TYPE_CHECKING: + from django.db.models import QuerySet + from django.http import HttpRequest + from django.http import HttpResponse + + from camps.models import Camp + class ShiftListView(LoginRequiredMixin, CampViewMixin, ListView): + """Shift list view.""" model = TeamShift template_name = "team_shift_list.html" context_object_name = "shifts" active_menu = "shifts" - def get_queryset(self): + def get_queryset(self) -> QuerySet: + """Method to filter by team slug.""" queryset = super().get_queryset() return queryset.filter(team__slug=self.kwargs["team_slug"]) - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Method for setting team to context.""" context = super().get_context_data(**kwargs) context["team"] = Team.objects.get( camp=self.camp, @@ -42,17 +55,19 @@ def get_context_data(self, **kwargs): return context -def date_choices(camp): +def date_choices(camp: Camp) -> list: + """Method for making date/time choices.""" index = 0 minute_choices = [] # To begin with we assume a shift can not be shorter than an hour - SHIFT_MINIMUM_LENGTH = 60 - while index * SHIFT_MINIMUM_LENGTH < 60: - minutes = int(index * SHIFT_MINIMUM_LENGTH) + shift_minimum_length = 60 + while index * shift_minimum_length < 60: # noqa: PLR2004 + minutes = int(index * shift_minimum_length) minute_choices.append(minutes) index += 1 - def get_time_choices(date): + def get_time_choices(date) -> list: + """Method for making a list of time options.""" time_choices = [] for hour in range(24): for minute in minute_choices: @@ -73,11 +88,14 @@ def get_time_choices(date): class ShiftForm(forms.ModelForm): + """Form for shifts.""" class Meta: + """Meta.""" model = TeamShift - fields = ["from_datetime", "to_datetime", "people_required"] + fields = ("from_datetime", "to_datetime", "people_required") - def __init__(self, instance=None, **kwargs) -> None: + def __init__(self, instance: dict|None=None, **kwargs) -> None: + """Method for setting up the form.""" camp = kwargs.pop("camp") super().__init__(instance=instance, **kwargs) self.fields["from_datetime"].widget = forms.Select(choices=date_choices(camp)) @@ -96,46 +114,51 @@ def __init__(self, instance=None, **kwargs) -> None: from_datetime = forms.DateTimeField() to_datetime = forms.DateTimeField() - def _get_from_datetime(self): + def _get_from_datetime(self) -> dict: + """Method to convert from_datetime to current timezone.""" current_timezone = timezone.get_current_timezone() return self.cleaned_data["from_datetime"].astimezone(current_timezone) - def _get_to_datetime(self): + def _get_to_datetime(self) -> dict: + """Method to convert to_datetime to current timezone.""" current_timezone = timezone.get_current_timezone() return self.cleaned_data["to_datetime"].astimezone(current_timezone) def clean(self) -> None: + """Method for cleaning the form data and check lower is bigger then upper.""" self.lower = self._get_from_datetime() self.upper = self._get_to_datetime() if self.lower > self.upper: raise forms.ValidationError("Start can not be after end.") - def save(self, commit=True): + def save(self, commit=True) -> TeamShift: + """Method for saving shift_range from self.lower and self.upper.""" # self has .lower and .upper from .clean() self.instance.shift_range = DateTimeTZRange(self.lower, self.upper) return super().save(commit=commit) class ShiftCreateView(LoginRequiredMixin, CampViewMixin, CreateView): + """View for creating a single shift.""" model = TeamShift template_name = "team_shift_form.html" form_class = ShiftForm active_menu = "shifts" - def get_form_kwargs(self): + def get_form_kwargs(self) -> dict: + """Method for adding camp to kwargs.""" kwargs = super().get_form_kwargs() kwargs["camp"] = self.camp return kwargs - def form_valid(self, form): + def form_valid(self, form: ShiftForm) -> HttpResponse: + """Check if the form is valid add team to the data.""" shift = form.save(commit=False) shift.team = Team.objects.get(camp=self.camp, slug=self.kwargs["team_slug"]) return super().form_valid(form) - def get_success_url(self): - return reverse("teams:shifts", kwargs=self.kwargs) - - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Method for adding camp and team slug to context.""" context = super().get_context_data(**kwargs) context["team"] = Team.objects.get( camp=self.camp, @@ -143,34 +166,44 @@ def get_context_data(self, **kwargs): ) return context + def get_success_url(self) -> str: + """Method get success url.""" + return reverse("teams:shifts", kwargs=self.kwargs) + class ShiftUpdateView(LoginRequiredMixin, CampViewMixin, UpdateView): + """View for updating a single shift.""" model = TeamShift template_name = "team_shift_form.html" form_class = ShiftForm active_menu = "shifts" - def get_form_kwargs(self): + def get_form_kwargs(self) -> dict: + """Method for adding camp to kwargs.""" kwargs = super().get_form_kwargs() kwargs["camp"] = self.camp return kwargs - def get_success_url(self): - self.kwargs.pop("pk") - return reverse("teams:shifts", kwargs=self.kwargs) - - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Method for adding team to context.""" context = super().get_context_data(**kwargs) context["team"] = self.object.team return context + def get_success_url(self) -> str: + """Method get success url.""" + self.kwargs.pop("pk") + return reverse("teams:shifts", kwargs=self.kwargs) + class ShiftDeleteView(LoginRequiredMixin, CampViewMixin, DeleteView): + """View for deleting a shift.""" model = TeamShift template_name = "team_shift_confirm_delete.html" active_menu = "shifts" - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Method for adding camp and team slug to context.""" context = super().get_context_data(**kwargs) context["team"] = Team.objects.get( camp=self.camp, @@ -178,13 +211,16 @@ def get_context_data(self, **kwargs): ) return context - def get_success_url(self): + def get_success_url(self) -> str: + """Method get success url.""" self.kwargs.pop("pk") return reverse("teams:shifts", kwargs=self.kwargs) class MultipleShiftForm(forms.Form): - def __init__(self, instance=None, **kwargs) -> None: + """Form for creating multple shifts.""" + def __init__(self, instance: dict|None=None, **kwargs) -> None: + """Method for form init setting camp to kwargs.""" camp = kwargs.pop("camp") super().__init__(**kwargs) self.fields["from_datetime"].widget = forms.Select(choices=date_choices(camp)) @@ -201,16 +237,19 @@ def __init__(self, instance=None, **kwargs) -> None: class ShiftCreateMultipleView(LoginRequiredMixin, CampViewMixin, FormView): + """View for creating multiple shifts.""" template_name = "team_shift_form.html" form_class = MultipleShiftForm active_menu = "shifts" - def get_form_kwargs(self): + def get_form_kwargs(self) -> dict: + """Method for setting camp to the kwargs.""" kwargs = super().get_form_kwargs() kwargs["camp"] = self.camp return kwargs - def form_valid(self, form): + def form_valid(self, form: MultipleShiftForm) -> HttpResponse: + """Method for checking if the form data is valid.""" team = Team.objects.get(camp=self.camp, slug=self.kwargs["team_slug"]) current_timezone = timezone.get_current_timezone() @@ -238,10 +277,12 @@ def form_valid(self, form): return super().form_valid(form) - def get_success_url(self): + def get_success_url(self) -> str: + """Method for returning the success url.""" return reverse("teams:shifts", kwargs=self.kwargs) - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Method for adding team to the context.""" context = super().get_context_data(**kwargs) context["team"] = Team.objects.get( camp=self.camp, @@ -251,9 +292,11 @@ def get_context_data(self, **kwargs): class MemberTakesShift(LoginRequiredMixin, CampViewMixin, View): - http_methods = ["get"] + """View for adding a user to a shift.""" + http_methods = ("get",) - def get(self, request, **kwargs): + def get(self, request: HttpRequest, **kwargs) -> HttpResponseRedirect: + """Method for adding user to a shift.""" shift = TeamShift.objects.get(id=kwargs["pk"]) team = Team.objects.get(camp=self.camp, slug=kwargs["team_slug"]) @@ -287,9 +330,11 @@ def get(self, request, **kwargs): class MemberDropsShift(LoginRequiredMixin, CampViewMixin, View): - http_methods = ["get"] + """View for remove a user from a shift.""" + http_methods = ("get",) - def get(self, request, **kwargs): + def get(self, request: HttpRequest, **kwargs) -> HttpResponseRedirect: + """Method to remove user from shift.""" shift = TeamShift.objects.get(id=kwargs["pk"]) team = Team.objects.get(camp=self.camp, slug=kwargs["team_slug"]) @@ -303,9 +348,11 @@ def get(self, request, **kwargs): class UserShifts(CampViewMixin, TemplateView): + """View for showing shifts for current user.""" template_name = "team_user_shifts.html" - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Method for adding user_teams and user_shits to context.""" context = super().get_context_data(**kwargs) context["user_teams"] = self.request.user.teammember_set.filter( team__camp=self.camp, diff --git a/src/teams/views/tasks.py b/src/teams/views/tasks.py index afd938000..8d433c8e7 100644 --- a/src/teams/views/tasks.py +++ b/src/teams/views/tasks.py @@ -1,5 +1,8 @@ +"""All views for the teams task application.""" from __future__ import annotations +from typing import TYPE_CHECKING + from django import forms from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin @@ -18,8 +21,11 @@ from .mixins import TeamTaskerPermissionMixin from .mixins import TeamViewMixin +if TYPE_CHECKING: + from django.http import HttpRequest class TeamTasksView(CampViewMixin, DetailView): + """List view of the team tasks.""" template_name = "team_tasks.html" context_object_name = "team" model = Team @@ -28,23 +34,28 @@ class TeamTasksView(CampViewMixin, DetailView): class TaskCommentForm(forms.ModelForm): + """Form for commenting on a Task.""" class Meta: + """Meta.""" model = TaskComment - fields = ["comment"] + fields = ("comment",) class TaskDetailView(TeamViewMixin, DetailView): + """Task detail view.""" template_name = "task_detail.html" context_object_name = "task" model = TeamTask active_menu = "tasks" - def get_context_data(self, *args, **kwargs): + def get_context_data(self, *args, **kwargs) -> dict: + """Add the inline form for comments.""" context = super().get_context_data(*args, **kwargs) context["comment_form"] = TaskCommentForm() return context - def post(self, request, **kwargs): + def post(self, request: HttpRequest, **kwargs) -> HttpResponseRedirect: + """Post endpoint for the comment form.""" task = self.get_object() if request.user not in task.team.members.all(): return HttpResponseNotAllowed("Nope") @@ -62,11 +73,14 @@ def post(self, request, **kwargs): class TaskForm(forms.ModelForm): + """Form for creating or edditing Tasks.""" class Meta: + """Meta.""" model = TeamTask - fields = ["name", "description", "when", "completed"] + fields = ("name", "description", "when", "completed") def __init__(self, **kwargs) -> None: + """Method for setting up the form fields.""" super().__init__(**kwargs) self.fields["when"].widget.widgets = [ forms.DateTimeInput(attrs={"placeholder": "Start"}), @@ -80,18 +94,21 @@ class TaskCreateView( TeamTaskerPermissionMixin, CreateView, ): + """View for creating a team task.""" model = TeamTask template_name = "task_form.html" form_class = TaskForm active_menu = "tasks" - def get_team(self): + def get_team(self) -> Team: + """Method to get the team object.""" return Team.objects.get( camp__slug=self.kwargs["camp_slug"], slug=self.kwargs["team_slug"], ) - def form_valid(self, form): + def form_valid(self, form: TaskForm) -> HttpResponseRedirect: + """Method to set extra fields on save.""" task = form.save(commit=False) task.team = self.team if not task.name: @@ -99,7 +116,8 @@ def form_valid(self, form): task.save() return HttpResponseRedirect(task.get_absolute_url()) - def get_success_url(self): + def get_success_url(self) -> str: + """Method to get the success url.""" return self.get_object().get_absolute_url() @@ -109,17 +127,20 @@ class TaskUpdateView( TeamTaskerPermissionMixin, UpdateView, ): + """Update task view used for updating tasks.""" model = TeamTask template_name = "task_form.html" form_class = TaskForm active_menu = "tasks" - def get_context_data(self, *args, **kwargs): + def get_context_data(self, *args, **kwargs) -> dict: + """Method for adding context data for team.""" context = super().get_context_data(**kwargs) context["team"] = self.team return context - def form_valid(self, form): + def form_valid(self, form: TaskForm) -> HttpResponseRedirect: + """Method to set extra fields on save.""" task = form.save(commit=False) task.team = self.team if not task.name: @@ -127,5 +148,6 @@ def form_valid(self, form): task.save() return HttpResponseRedirect(task.get_absolute_url()) - def get_success_url(self): + def get_success_url(self) -> str: + """Method to get the success url.""" return self.get_object().get_absolute_url() From bf7ebe60415c11f44ef1d6f58cab7f165ce9aca6 Mon Sep 17 00:00:00 2001 From: "Rudy (zarya)" Date: Fri, 16 May 2025 17:20:35 +0200 Subject: [PATCH 02/10] Split up the bootstrap, add support for using bootstrap in tests --- src/maps/templates/user_location_list.html | 2 +- src/maps/tests/test_views.py | 6 +- .../templates/dectregistration_list.html | 2 +- src/phonebook/templates/phonebook.html | 2 +- src/phonebook/tests/test_views.py | 14 +- src/tokens/tests/test_views.py | 4 +- src/utils/bootstrap/__init__.py | 0 src/utils/bootstrap/base.py | 2242 ++++++++++++++++ src/utils/bootstrap/factories.py | 165 ++ src/utils/bootstrap/functions.py | 36 + .../management/commands/bootstrap_devsite.py | 2330 +---------------- src/utils/tests.py | 59 +- 12 files changed, 2470 insertions(+), 2392 deletions(-) create mode 100644 src/utils/bootstrap/__init__.py create mode 100644 src/utils/bootstrap/base.py create mode 100644 src/utils/bootstrap/factories.py create mode 100644 src/utils/bootstrap/functions.py diff --git a/src/maps/templates/user_location_list.html b/src/maps/templates/user_location_list.html index fb7e96025..b1bf237a7 100644 --- a/src/maps/templates/user_location_list.html +++ b/src/maps/templates/user_location_list.html @@ -116,7 +116,7 @@

DELETE /{{ camp.slug }}/map/userlocation/<uuid>/api/

{% if userlocation_list %} -
Name
+
diff --git a/src/maps/tests/test_views.py b/src/maps/tests/test_views.py index 4fc65476c..15a15a02a 100644 --- a/src/maps/tests/test_views.py +++ b/src/maps/tests/test_views.py @@ -117,7 +117,7 @@ def setUpTestData(cls) -> None: description="Test Layer", icon="fas fa-tractor", group=cls.group, - responsible_team=cls.team, + responsible_team=cls.teams['noc'], ) cls.layer.save() @@ -209,7 +209,7 @@ def test_user_location_view(self) -> None: content = response.content.decode() soup = BeautifulSoup(content, "html.parser") - rows = soup.select("div#main > table > tbody > tr") + rows = soup.select("table#main_table > tbody > tr") self.assertEqual(len(rows), 1, "user location list does not return 1 entries") def test_user_location_create(self) -> None: @@ -234,5 +234,5 @@ def test_user_location_create(self) -> None: content = response.content.decode() soup = BeautifulSoup(content, "html.parser") - rows = soup.select("div#main > table > tbody > tr") + rows = soup.select("table#main_table > tbody > tr") self.assertEqual(len(rows), 2, "user location list does not return 2 entries after create") diff --git a/src/phonebook/templates/dectregistration_list.html b/src/phonebook/templates/dectregistration_list.html index f1743da62..22d2df509 100644 --- a/src/phonebook/templates/dectregistration_list.html +++ b/src/phonebook/templates/dectregistration_list.html @@ -16,7 +16,7 @@ Phonebook Create DECT Registration

-
Name
+
diff --git a/src/phonebook/templates/phonebook.html b/src/phonebook/templates/phonebook.html index f86904965..442de8573 100644 --- a/src/phonebook/templates/phonebook.html +++ b/src/phonebook/templates/phonebook.html @@ -21,7 +21,7 @@

{{ camp.title }} Phonebook

{% endif %} {% if dectregistration_list %} -
Number
+
diff --git a/src/phonebook/tests/test_views.py b/src/phonebook/tests/test_views.py index 245c14e46..4ea67264a 100644 --- a/src/phonebook/tests/test_views.py +++ b/src/phonebook/tests/test_views.py @@ -53,23 +53,23 @@ def test_phonebook_list_view(self) -> None: response = self.client.get(url) content = response.content.decode() soup = BeautifulSoup(content, "html.parser") - rows = soup.select("div#main > table > tbody > tr") + rows = soup.select("table#main_table > tbody > tr") self.assertEqual(len(rows), 2, "phonebook list does not return 2 entries") def test_dect_registration_list_view(self) -> None: """Test the basics of the dect registrations list view.""" url = reverse("phonebook:dectregistration_list", kwargs={"camp_slug": self.camp.slug}) - self.client.login(username="user0", password="user0") + self.client.force_login(self.users[0]) response = self.client.get(url) content = response.content.decode() soup = BeautifulSoup(content, "html.parser") - rows = soup.select("div#main > div > div > table > tbody > tr") + rows = soup.select("table#main_table > tbody > tr") self.assertEqual(len(rows), 3, "dect registration list does not return 2 registrations") def test_dect_registration_create_view(self) -> None: """Test the basics of the dect registrations create view.""" - self.client.login(username="user0", password="user0") + self.client.force_login(self.users[0]) url = reverse("phonebook:dectregistration_create", kwargs={"camp_slug": self.camp.slug}) @@ -90,7 +90,7 @@ def test_dect_registration_create_view(self) -> None: # Test if the registration shows up content = response.content.decode() soup = BeautifulSoup(content, "html.parser") - rows = soup.select("div#main > div > div > table > tbody > tr") + rows = soup.select("table#main_table > tbody > tr") self.assertEqual(len(rows), 4, "dect registration create number failed") # Test Named number @@ -110,7 +110,7 @@ def test_dect_registration_create_view(self) -> None: # Test if the registration shows up content = response.content.decode() soup = BeautifulSoup(content, "html.parser") - rows = soup.select("div#main > div > div > table > tbody > tr") + rows = soup.select("table#main_table > tbody > tr") self.assertEqual(len(rows), 5, "dect registration create INFO failed") # Test duplicated number @@ -158,5 +158,5 @@ def test_dect_registration_create_view(self) -> None: response = self.client.get(url) content = response.content.decode() soup = BeautifulSoup(content, "html.parser") - rows = soup.select("div#main > table > tbody > tr") + rows = soup.select("table#main_table > tbody > tr") self.assertEqual(len(rows), 3, "phonebook list does not return 3 entries after create") diff --git a/src/tokens/tests/test_views.py b/src/tokens/tests/test_views.py index f96aeda6c..508b31091 100644 --- a/src/tokens/tests/test_views.py +++ b/src/tokens/tests/test_views.py @@ -83,7 +83,7 @@ def setUpTestData(cls) -> None: def test_token_list_view(self) -> None: """Test the basics of the token list view.""" - self.client.login(username="user0", password="user0") + self.client.force_login(self.users[0]) url = reverse("tokens:tokenfind_list") response = self.client.get(url) @@ -94,7 +94,7 @@ def test_token_list_view(self) -> None: def test_token_find_view(self) -> None: """Test the basics of the token find view.""" - self.client.login(username="user0", password="user0") + self.client.force_login(self.users[0]) url = reverse("tokens:details", kwargs={"token": self.token.token}) diff --git a/src/utils/bootstrap/__init__.py b/src/utils/bootstrap/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py new file mode 100644 index 000000000..1e8c5ad3e --- /dev/null +++ b/src/utils/bootstrap/base.py @@ -0,0 +1,2242 @@ +from __future__ import annotations + +import logging +import random +import sys +import uuid +from datetime import datetime +from datetime import timedelta + +import factory +import pytz +from allauth.account.models import EmailAddress +from django.conf import settings +from django.contrib.auth.models import Permission +from django.contrib.auth.models import User +from django.contrib.contenttypes.models import ContentType +from django.contrib.gis.geos import GeometryCollection +from django.contrib.gis.geos import Point +from django.contrib.gis.geos import Polygon +from django.core.exceptions import ValidationError +from django.utils import timezone +from django.utils.crypto import get_random_string +from faker import Faker + +from camps.models import Camp +from camps.models import Permission as CampPermission +from economy.factories import BankAccountFactory +from economy.factories import BankFactory +from economy.factories import BankTransactionFactory +from economy.factories import ClearhausSettlementFactory +from economy.factories import CoinifyBalanceFactory +from economy.factories import CoinifyInvoiceFactory +from economy.factories import CoinifyPaymentIntentFactory +from economy.factories import CoinifyPayoutFactory +from economy.factories import CoinifySettlementFactory +from economy.factories import EpayTransactionFactory +from economy.factories import MobilePayTransactionFactory +from economy.factories import ZettleBalanceFactory +from economy.factories import ZettleReceiptFactory +from economy.models import Chain +from economy.models import Credebtor +from economy.models import Expense +from economy.models import Pos +from economy.models import Reimbursement +from events.models import Routing +from events.models import Type +from facilities.models import Facility +from facilities.models import FacilityFeedback +from facilities.models import FacilityQuickFeedback +from facilities.models import FacilityType +from feedback.models import Feedback +from info.models import InfoCategory +from info.models import InfoItem +from maps.models import Feature +from maps.models import Group as MapGroup +from maps.models import Layer +from news.models import NewsItem +from program.autoscheduler import AutoScheduler +from program.models import Event +from program.models import EventLocation +from program.models import EventProposal +from program.models import EventSession +from program.models import EventSlot +from program.models import EventTrack +from program.models import EventType +from program.models import SpeakerProposal +from program.models import UrlType +from program.utils import get_speaker_availability_form_matrix +from program.utils import save_speaker_availability +from rideshare.models import Ride +from shop.models import Order +from shop.models import Product +from shop.models import ProductCategory +from sponsors.models import Sponsor +from sponsors.models import SponsorTier +from teams.models import Team +from teams.models import TeamMember +from teams.models import TeamShift +from teams.models import TeamTask +from tickets.models import TicketType +from tokens.models import Token +from tokens.models import TokenFind +from utils.slugs import unique_slugify +from villages.models import Village +from .functions import output_fake_md_description + + +from .factories import CredebtorFactory +from .factories import ExpenseFactory +from .factories import RevenueFactory +from .factories import UserFactory +from .factories import EmailAddressFactory +from .factories import SpeakerProposalFactory +from .factories import EventProposalFactory +from .factories import EventProposalUrlFactory +from .factories import SpeakerProposalUrlFactory + +fake = Faker() +# Faker.seed(0) +tz = pytz.timezone("Europe/Copenhagen") +logger = logging.getLogger(f"bornhack.{__name__}") + + + +class Bootstrap(): + + camps: list[Camp] + camp: Camp + users: dict + teams: dict + + def create_camps(self, camps): + self.output("Creating camps...") + + camp_instances = [] + + for camp in camps: + year = camp["year"] + read_only = camp["read_only"] + camp_instances.append( + ( + Camp.objects.create( + title=f"BornHack {year}", + tagline=camp["tagline"], + slug=f"bornhack-{year}", + shortslug=f"bornhack-{year}", + buildup=( + tz.localize(datetime(year, 8, 25, 12, 0)), + tz.localize(datetime(year, 8, 27, 12, 0)), + ), + camp=( + tz.localize(datetime(year, 8, 27, 12, 0)), + tz.localize(datetime(year, 9, 3, 12, 0)), + ), + teardown=( + tz.localize(datetime(year, 9, 3, 12, 0)), + tz.localize(datetime(year, 9, 5, 12, 0)), + ), + colour=camp["colour"], + light_text=camp.get("light_text", True), + ), + read_only, + ), + ) + + self.camps = camp_instances + + 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): + self.output("Creating users...") + + users = {} + + for i in range(amount): + username = f"user{i}" + user = UserFactory.create( + username=str(uuid.uuid4()), + email=f"{username}@example.com", + ) + user.set_password(username) + user.save() + users[i] = user + EmailAddressFactory.create( + user=user, + email=f"{username}@example.com", + primary=True, + ) + + admin = User.objects.create_superuser( + username="admin", + email="admin@example.com", + password="admin", + ) + users["admin"] = admin + admin.profile.name = "Administrator" + admin.profile.description = "Default adminstrative user" + admin.profile.public_credit_name = "Administrator" + admin.profile.public_credit_name_approved = True + admin.profile.save() + EmailAddress.objects.create( + user=admin, + email="admin@example.com", + verified=True, + primary=True, + ) + + self.users = users + + def create_news(self) -> None: + NewsItem.objects.create( + title="unpublished news item", + content="unpublished news body here", + ) + + def create_quickfeedback_options(self): + options = {} + self.output("Creating quickfeedback options") + options["na"] = FacilityQuickFeedback.objects.create( + feedback="N/A", + icon="fas fa-times", + ) + options["attention"] = FacilityQuickFeedback.objects.create( + feedback="Needs attention", + ) + options["toiletpaper"] = FacilityQuickFeedback.objects.create( + feedback="Needs more toiletpaper", + icon="fas fa-toilet-paper", + ) + options["cleaning"] = FacilityQuickFeedback.objects.create( + feedback="Needs cleaning", + icon="fas fa-broom", + ) + options["power"] = FacilityQuickFeedback.objects.create( + feedback="No power", + icon="fas fa-bolt", + ) + return options + + def create_mobilepay_transactions(self) -> None: + self.output("Creating MobilePay Transactions...") + MobilePayTransactionFactory.create_batch(50) + + def create_clearhaus_settlements(self) -> None: + self.output("Creating Clearhaus Settlements...") + ClearhausSettlementFactory.create_batch(50) + + def create_zettle_stuff(self) -> None: + self.output("Creating Zettle receipts and balances...") + ZettleBalanceFactory.create_batch(100) + ZettleReceiptFactory.create_batch(100) + + def create_bank_stuff(self) -> None: + self.output("Creating Banks, BankAccounts, and BankTransactions...") + BankFactory.create_batch(2) + BankAccountFactory.create_batch(16) + BankTransactionFactory.create_batch(300) + + def create_coinify_stuff(self) -> None: + self.output( + "Creating Coinify invoices, payment intents, payouts, settlements and balances...", + ) + CoinifyInvoiceFactory.create_batch(50) + CoinifyPaymentIntentFactory.create_batch(50) + CoinifyPayoutFactory.create_batch(10) + CoinifyBalanceFactory.create_batch(10) + CoinifySettlementFactory.create_batch(10) + + def create_epay_transactions(self) -> None: + self.output("Creating ePay Transactions...") + EpayTransactionFactory.create_batch(50) + + def create_facility_types(self, camp, teams, options): + types = {} + self.output("Creating facility types...") + types["toilet"] = FacilityType.objects.create( + name="Toilets", + description="All the toilets", + icon="fas fa-toilet", + marker="greyIcon", + responsible_team=teams["shit"], + ) + types["toilet"].quickfeedback_options.add(options["na"]) + types["toilet"].quickfeedback_options.add(options["attention"]) + types["toilet"].quickfeedback_options.add(options["toiletpaper"]) + types["toilet"].quickfeedback_options.add(options["cleaning"]) + + types["power"] = FacilityType.objects.create( + name="Power Infrastructure", + description="Power related infrastructure, distribution points, distribution cables, and so on.", + icon="fas fa-plug", + marker="goldIcon", + responsible_team=teams["power"], + ) + types["power"].quickfeedback_options.add(options["attention"]) + types["power"].quickfeedback_options.add(options["power"]) + return types + + def create_facilities(self, facility_types): + facilities = {} + self.output("Creating facilities...") + facilities["toilet1"] = Facility.objects.create( + 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( + 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( + facility_type=facility_types["power"], + name="PDP1", + description="In orga area", + location=Point(9.94079, 55.388022), + ) + facilities["pdp2"] = Facility.objects.create( + facility_type=facility_types["power"], + name="PDP2", + description="In bar area", + location=Point(9.942036, 55.387891), + ) + facilities["pdp3"] = Facility.objects.create( + facility_type=facility_types["power"], + name="PDP3", + description="In speaker tent", + location=Point(9.938416, 55.387109), + ) + facilities["pdp4"] = Facility.objects.create( + facility_type=facility_types["power"], + name="PDP4", + description="In food area", + location=Point(9.940146, 55.386983), + ) + return facilities + + def create_facility_feedbacks(self, facilities, options, users) -> None: + 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, + ) + + def create_event_types(self): + types = {} + self.output("Creating event types...") + types["workshop"] = EventType.objects.create( + name="Workshop", + slug="workshop", + color="#ff9900", + light_text=False, + public=True, + description="Workshops actively involve the participants in the learning experience", + icon="toolbox", + host_title="Host", + event_duration_minutes="180", + support_autoscheduling=True, + support_speaker_event_conflicts=True, + ) + + types["talk"] = EventType.objects.create( + name="Talk", + slug="talk", + color="#2D9595", + light_text=True, + public=True, + description="A presentation on a stage", + icon="chalkboard-teacher", + host_title="Speaker", + event_duration_minutes="60", + support_autoscheduling=True, + support_speaker_event_conflicts=True, + ) + + types["lightning"] = EventType.objects.create( + name="Lightning Talk", + slug="lightning-talk", + color="#ff0000", + light_text=True, + public=True, + description="A short 5-10 minute presentation", + icon="bolt", + host_title="Speaker", + event_duration_minutes="5", + support_speaker_event_conflicts=True, + ) + + types["music"] = EventType.objects.create( + name="Music Act", + slug="music", + color="#1D0095", + light_text=True, + public=True, + description="A musical performance", + icon="music", + host_title="Artist", + event_duration_minutes="180", + support_autoscheduling=True, + support_speaker_event_conflicts=True, + ) + + types["keynote"] = EventType.objects.create( + name="Keynote", + slug="keynote", + color="#FF3453", + light_text=True, + description="A keynote presentation", + icon="star", + host_title="Speaker", + event_duration_minutes="90", + support_autoscheduling=True, + support_speaker_event_conflicts=True, + ) + + types["debate"] = EventType.objects.create( + name="Debate", + slug="debate", + color="#F734C3", + light_text=True, + description="A panel debate with invited guests", + icon="users", + host_title="Guest", + public=True, + event_duration_minutes="120", + support_autoscheduling=True, + support_speaker_event_conflicts=True, + ) + + types["facility"] = EventType.objects.create( + name="Facilities", + slug="facilities", + color="#cccccc", + light_text=False, + include_in_event_list=False, + description="Events involving facilities like bathrooms, food area and so on", + icon="home", + host_title="Host", + event_duration_minutes="720", + support_speaker_event_conflicts=False, + ) + + types["recreational"] = EventType.objects.create( + name="Recreational Event", + slug="recreational-event", + color="#0000ff", + light_text=True, + public=True, + description="Events of a recreational nature", + icon="dice", + host_title="Host", + event_duration_minutes="600", + support_autoscheduling=False, + support_speaker_event_conflicts=True, + ) + + return types + + def create_url_types(self) -> None: + self.output("Creating UrlType objects...") + t, created = UrlType.objects.get_or_create( + name="Other", + defaults={"icon": "fas fa-link"}, + ) + t, created = UrlType.objects.get_or_create( + name="Homepage", + defaults={"icon": "fas fa-link"}, + ) + t, created = UrlType.objects.get_or_create( + name="Slides", + defaults={"icon": "fas fa-chalkboard-teacher"}, + ) + t, created = UrlType.objects.get_or_create( + name="Twitter", + defaults={"icon": "fab fa-twitter"}, + ) + t, created = UrlType.objects.get_or_create( + name="Mastodon", + defaults={"icon": "fab fa-mastodon"}, + ) + t, created = UrlType.objects.get_or_create( + name="Facebook", + defaults={"icon": "fab fa-facebook"}, + ) + t, created = UrlType.objects.get_or_create( + name="Project", + defaults={"icon": "fas fa-link"}, + ) + t, created = UrlType.objects.get_or_create( + name="Blog", + defaults={"icon": "fas fa-link"}, + ) + t, created = UrlType.objects.get_or_create( + name="Github", + defaults={"icon": "fab fa-github"}, + ) + t, created = UrlType.objects.get_or_create( + name="Keybase", + defaults={"icon": "fab fa-keybase"}, + ) + t, created = UrlType.objects.get_or_create( + name="Recording", + defaults={"icon": "fas fa-film"}, + ) + + def create_credebtors(self) -> None: + self.output("Creating Chains and Credebtors...") + try: + CredebtorFactory.create_batch(50) + except ValidationError: + self.output("Name conflict, retrying...") + CredebtorFactory.create_batch(50) + for _ in range(20): + # add 20 more credebtors to random existing chains + try: + CredebtorFactory.create(chain=Chain.objects.order_by("?").first()) + except ValidationError: + self.output("Name conflict, skipping...") + continue + # add a credebtor for reimbursements + reimbursement_chain = Chain.objects.create( + name="Reimbursement", + notes="This chain is only used for reimbursements", + ) + Credebtor.objects.create( + chain=reimbursement_chain, + name="Reimbursement", + address="Nowhere", + ) + + def create_product_categories(self): + categories = {} + self.output("Creating productcategories...") + categories["transportation"] = ProductCategory.objects.create( + name="Transportation", + slug="transportation", + ) + categories["merchandise"] = ProductCategory.objects.create( + name="Merchandise", + slug="merchandise", + ) + categories["tickets"] = ProductCategory.objects.create( + name="Tickets", + slug="tickets", + ) + categories["villages"] = ProductCategory.objects.create( + name="Villages", + slug="villages", + ) + categories["facilities"] = ProductCategory.objects.create( + name="Facilities", + slug="facilities", + ) + categories["packages"] = ProductCategory.objects.create( + name="Packages", + slug="packages", + ) + + return categories + + def create_camp_ticket_types(self, camp): + types = {} + self.output(f"Creating tickettypes for {camp.camp.lower.year}...") + types["adult_full_week"] = TicketType.objects.create( + name="Adult Full Week", + camp=camp, + ) + types["adult_one_day"] = TicketType.objects.create( + name="Adult One Day", + camp=camp, + ) + types["child_full_week"] = TicketType.objects.create( + name="Child Full Week", + camp=camp, + ) + types["child_one_day"] = TicketType.objects.create( + name="Child One Day", + camp=camp, + ) + types["village"] = TicketType.objects.create( + name="Village", + camp=camp, + ) + types["merchandise"] = TicketType.objects.create( + name="Merchandise", + camp=camp, + ) + types["facilities"] = TicketType.objects.create( + name="Facilities", + camp=camp, + single_ticket_per_product=True, + ) + types["transportation"] = TicketType.objects.create( + name="Transportation", + camp=camp, + ) + + return types + + def create_camp_products(self, camp, categories, ticket_types): + products = {} + year = camp.camp.lower.year + camp_prefix = f"BornHack {year}" + + name = f"{camp_prefix} Standard ticket" + products["ticket1"] = Product.objects.create( + name=name, + description="A ticket", + price=1200, + category=categories["tickets"], + available_in=( + tz.localize(datetime(year, 1, 1, 12, 0)), + tz.localize(datetime(year, 12, 20, 12, 0)), + ), + slug=unique_slugify( + name, + slugs_in_use=Product.objects.filter( + category=categories["tickets"], + ).values_list("slug", flat=True), + ), + ticket_type=ticket_types["adult_full_week"], + ) + + name = f"{camp_prefix} Hacker ticket" + products["ticket2"] = Product.objects.create( + name=name, + description="Another ticket", + price=1337, + category=categories["tickets"], + available_in=( + tz.localize(datetime(year, 1, 1, 12, 0)), + tz.localize(datetime(year, 12, 20, 12, 0)), + ), + slug=unique_slugify( + name, + slugs_in_use=Product.objects.filter( + category=categories["tickets"], + ).values_list("slug", flat=True), + ), + ticket_type=ticket_types["adult_full_week"], + ) + + name = f"{camp_prefix} One day ticket" + products["one_day_ticket"] = Product.objects.create( + name=name, + description="One day ticket", + price=300, + category=categories["tickets"], + available_in=( + tz.localize(datetime(year, 1, 1, 12, 0)), + tz.localize(datetime(year, 12, 20, 12, 0)), + ), + slug=unique_slugify( + name, + slugs_in_use=Product.objects.filter( + category=categories["tickets"], + ).values_list("slug", flat=True), + ), + ticket_type=ticket_types["adult_one_day"], + ) + + name = f"{camp_prefix} Village tent 3x3 meters, no floor" + products["tent1"] = Product.objects.create( + name=name, + description="A description of the tent goes here", + price=3325, + category=categories["villages"], + available_in=( + tz.localize(datetime(year, 1, 1, 12, 0)), + tz.localize(datetime(year, 12, 20, 12, 0)), + ), + slug=unique_slugify( + name, + slugs_in_use=Product.objects.filter( + category=categories["villages"], + ).values_list("slug", flat=True), + ), + ticket_type=ticket_types["village"], + ) + + name = f"{camp_prefix} Village tent 3x3 meters, with floor" + products["tent2"] = Product.objects.create( + name=name, + description="A description of the tent goes here", + price=3675, + category=categories["villages"], + available_in=( + tz.localize(datetime(year, 1, 1, 12, 0)), + tz.localize(datetime(year, 12, 20, 12, 0)), + ), + slug=unique_slugify( + name, + slugs_in_use=Product.objects.filter( + category=categories["villages"], + ).values_list("slug", flat=True), + ), + ticket_type=ticket_types["village"], + ) + + name = f"{camp_prefix} T-shirt Large" + products["t-shirt-large"] = Product.objects.create( + name=name, + description="A description of the t-shirt goes here", + price=150, + category=categories["merchandise"], + available_in=( + tz.localize(datetime(year, 1, 1, 12, 0)), + tz.localize(datetime(year, 12, 20, 12, 0)), + ), + slug=unique_slugify( + name, + slugs_in_use=Product.objects.filter( + category=categories["merchandise"], + ).values_list("slug", flat=True), + ), + ticket_type=ticket_types["merchandise"], + ) + + name = f"{camp_prefix} T-shirt Medium" + products["t-shirt-medium"] = Product.objects.create( + name=name, + description="A description of the t-shirt goes here", + price=150, + category=categories["merchandise"], + available_in=( + tz.localize(datetime(year, 1, 1, 12, 0)), + tz.localize(datetime(year, 12, 20, 12, 0)), + ), + slug=unique_slugify( + name, + slugs_in_use=Product.objects.filter( + category=categories["merchandise"], + ).values_list("slug", flat=True), + ), + ticket_type=ticket_types["merchandise"], + ) + + name = f"{camp_prefix} T-shirt Small" + products["t-shirt-small"] = Product.objects.create( + name=name, + description="A description of the t-shirt goes here", + price=150, + category=categories["merchandise"], + available_in=( + tz.localize(datetime(year, 1, 1, 12, 0)), + tz.localize(datetime(year, 12, 20, 12, 0)), + ), + slug=unique_slugify( + name, + slugs_in_use=Product.objects.filter( + category=categories["merchandise"], + ).values_list("slug", flat=True), + ), + ticket_type=ticket_types["merchandise"], + ) + + name = "100 HAX" + products["hax"] = Product.objects.create( + name=name, + description="100 HAX", + price=100, + category=categories["facilities"], + available_in=( + tz.localize(datetime(year, 1, 1, 12, 0)), + tz.localize(datetime(year, 12, 20, 12, 0)), + ), + slug=unique_slugify( + name, + slugs_in_use=Product.objects.filter( + category=categories["facilities"], + ).values_list("slug", flat=True), + ), + ticket_type=ticket_types["facilities"], + ) + + name = "Corporate Hackers Small" + products["corporate_hackers_small"] = Product.objects.create( + name=name, + description="Send your company to BornHack in style with one of our corporate packages!", + price=18000, + category=categories["packages"], + available_in=( + tz.localize(datetime(year, 1, 1, 12, 0)), + tz.localize(datetime(year, 12, 20, 12, 0)), + ), + slug=unique_slugify( + name, + slugs_in_use=Product.objects.filter( + category=categories["packages"], + ).values_list("slug", flat=True), + ), + ) + products["corporate_hackers_small"].sub_products.add( + products["ticket1"], + through_defaults={ + "number_of_tickets": 3, + }, + ) + products["corporate_hackers_small"].sub_products.add( + products["one_day_ticket"], + through_defaults={ + "number_of_tickets": 3, + }, + ) + products["corporate_hackers_small"].sub_products.add( + products["tent2"], + through_defaults={ + "number_of_tickets": 1, + }, + ) + products["corporate_hackers_small"].sub_products.add( + products["hax"], + through_defaults={ + "number_of_tickets": 25, + }, + ) + + return products + + def create_orders(self, users, camp_products): + orders = {} + self.output("Creating orders...") + orders[0] = Order.objects.create( + user=users[1], + payment_method="in_person", + open=None, + paid=True, + ) + orders[0].oprs.create(product=camp_products["ticket1"], quantity=1) + orders[0].oprs.create(product=camp_products["tent1"], quantity=1) + orders[0].mark_as_paid(request=None) + + orders[1] = Order.objects.create( + user=users[2], + payment_method="in_person", + open=None, + ) + orders[1].oprs.create(product=camp_products["ticket1"], quantity=1) + orders[1].oprs.create(product=camp_products["tent2"], quantity=1) + orders[1].oprs.create(product=camp_products["t-shirt-medium"], quantity=1) + orders[1].mark_as_paid(request=None) + + orders[2] = Order.objects.create( + user=users[3], + payment_method="in_person", + open=None, + ) + orders[2].oprs.create(product=camp_products["ticket2"], quantity=1) + orders[2].oprs.create(product=camp_products["ticket1"], quantity=1) + orders[2].oprs.create(product=camp_products["tent2"], quantity=1) + orders[2].oprs.create(product=camp_products["t-shirt-small"], quantity=1) + orders[2].oprs.create(product=camp_products["t-shirt-large"], quantity=1) + orders[2].mark_as_paid(request=None) + + orders[3] = Order.objects.create( + user=users[4], + payment_method="in_person", + open=None, + ) + orders[3].oprs.create(product=camp_products["ticket2"], quantity=1) + orders[3].oprs.create(product=camp_products["tent1"], quantity=1) + orders[3].oprs.create(product=camp_products["t-shirt-small"], quantity=1) + orders[3].oprs.create(product=camp_products["hax"], quantity=30) + orders[3].mark_as_paid(request=None) + + return orders + + def create_camp_tracks(self, camp): + tracks = {} + year = camp.camp.lower.year + self.output(f"Creating event_tracks for {year}...") + tracks[1] = EventTrack.objects.create( + camp=camp, + name=f"BornHack {year}", + slug=camp.slug, + ) + + return tracks + + def create_event_locations(self, camp): + locations = {} + year = camp.camp.lower.year + self.output(f"Creating event_locations for {year}...") + locations["speakers_tent"] = EventLocation.objects.create( + name="Speakers Tent", + slug="speakers-tent", + icon="comment", + camp=camp, + capacity=150, + ) + locations["workshop_room_1"] = EventLocation.objects.create( + name="Workshop room 1 (big)", + slug="workshop-room-1", + icon="briefcase", + camp=camp, + capacity=50, + ) + locations["workshop_room_2"] = EventLocation.objects.create( + name="Workshop room 2 (small)", + slug="workshop-room-2", + icon="briefcase", + camp=camp, + capacity=25, + ) + locations["workshop_room_3"] = EventLocation.objects.create( + name="Workshop room 3 (small)", + slug="workshop-room-3", + icon="briefcase", + camp=camp, + capacity=25, + ) + locations["bar_area"] = EventLocation.objects.create( + name="Bar Area", + slug="bar-area", + icon="glass-cheers", + camp=camp, + capacity=50, + ) + locations["food_area"] = EventLocation.objects.create( + name="Food Area", + slug="food-area", + icon="utensils", + camp=camp, + capacity=50, + ) + locations["infodesk"] = EventLocation.objects.create( + name="Infodesk", + slug="infodesk", + icon="info", + camp=camp, + capacity=20, + ) + + # 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"]) + locations["workshop_room_1"].conflicts.add(locations["workshop_room_3"]) + + return locations + + def create_camp_news(self, camp) -> None: + year = camp.camp.lower.year + self.output(f"Creating news for {year}...") + NewsItem.objects.create( + title=f"Welcome to {camp.title}", + content="news body here with html support", + published_at=tz.localize(datetime(year, 8, 27, 12, 0)), + ) + NewsItem.objects.create( + title=f"{camp.title} is over", + content="news body here", + published_at=tz.localize(datetime(year, 9, 4, 12, 0)), + ) + + def create_camp_event_sessions(self, camp, event_types, event_locations) -> None: + self.output(f"Creating EventSessions for {camp}...") + days = camp.get_days(camppart="camp")[1:-1] + for day in days: + start = day.lower + EventSession.objects.create( + camp=camp, + event_type=event_types["talk"], + event_location=event_locations["speakers_tent"], + when=( + tz.localize(datetime(start.year, start.month, start.day, 11, 0)), + tz.localize(datetime(start.year, start.month, start.day, 18, 0)), + ), + ) + EventSession.objects.create( + camp=camp, + event_type=event_types["recreational"], + event_location=event_locations["speakers_tent"], + event_duration_minutes=60, + when=( + tz.localize(datetime(start.year, start.month, start.day, 12, 0)), + tz.localize(datetime(start.year, start.month, start.day, 13, 0)), + ), + ) + EventSession.objects.create( + camp=camp, + event_type=event_types["music"], + event_location=event_locations["bar_area"], + when=( + tz.localize(datetime(start.year, start.month, start.day, 22, 0)), + tz.localize(datetime(start.year, start.month, start.day, 22, 0)) + timedelta(hours=3), + ), + ) + EventSession.objects.create( + camp=camp, + event_type=event_types["workshop"], + event_location=event_locations["workshop_room_1"], + when=( + tz.localize(datetime(start.year, start.month, start.day, 12, 0)), + tz.localize(datetime(start.year, start.month, start.day, 18, 0)), + ), + ) + EventSession.objects.create( + camp=camp, + event_type=event_types["workshop"], + event_location=event_locations["workshop_room_2"], + when=( + tz.localize(datetime(start.year, start.month, start.day, 12, 0)), + tz.localize(datetime(start.year, start.month, start.day, 18, 0)), + ), + ) + EventSession.objects.create( + camp=camp, + event_type=event_types["workshop"], + event_location=event_locations["workshop_room_3"], + when=( + tz.localize(datetime(start.year, start.month, start.day, 12, 0)), + tz.localize(datetime(start.year, start.month, start.day, 18, 0)), + ), + ) + # 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"], + when=( + tz.localize( + datetime(day.lower.year, day.lower.month, day.lower.day, 20, 0), + ), + tz.localize( + datetime( + day.lower.year, + day.lower.month, + day.lower.day, + 21, + 30, + ), + ), + ), + ) + + def create_camp_proposals(self, camp, event_types) -> None: + year = camp.camp.lower.year + self.output(f"Creating event- and speaker_proposals for {year}...") + + # add 45 talks + talkproposals = EventProposalFactory.create_batch( + 45, + track=factory.Iterator(camp.event_tracks.all()), + event_type=event_types["talk"], + ) + # and 15 workshops + workshopproposals = EventProposalFactory.create_batch( + 15, + track=factory.Iterator(camp.event_tracks.all()), + event_type=event_types["workshop"], + ) + # and 3 keynotes + # (in the real world these are submitted as talks + # and promoted to keynotes by the content team) + keynoteproposals = EventProposalFactory.create_batch( + 3, + track=factory.Iterator(camp.event_tracks.all()), + event_type=event_types["keynote"], + ) + + tags = [ + "infosec", + "hardware", + "politics", + "django", + "development", + "games", + "privacy", + "vampires", + "linux", + ] + + for ep in talkproposals + workshopproposals + keynoteproposals: + # create a speakerproposal for this EventProposal + sp = SpeakerProposalFactory(camp=camp, user=ep.user) + ep.speakers.add(sp) + # 20% chance we add an extra speaker + if random.randint(1, 10) > 8: + other_speakers = SpeakerProposal.objects.filter(camp=camp).exclude( + uuid=sp.uuid, + ) + # ... if we have any... + if other_speakers.exists(): + # add an extra speaker + ep.speakers.add(random.choice(other_speakers)) + + # add tags for 2 out of 3 events + if random.choice([True, True, False]): + # add 1-3 tags for this EP + ep.tags.add(*random.sample(tags, k=random.randint(1, 3))) + + EventProposal.objects.create( + user=random.choice(User.objects.all()), + title="Lunch break", + abstract="Daily lunch break. Remember to drink water.", + event_type=event_types["recreational"], + track=random.choice(camp.event_tracks.all()), + ).mark_as_approved() + + def create_proposal_urls(self, camp) -> None: + """Create URL objects for the proposals.""" + year = camp.camp.lower.year + self.output( + f"Creating URLs for Speaker- and EventProposals for {year}...", + ) + SpeakerProposalUrlFactory.create_batch( + 100, + speaker_proposal=factory.Iterator( + SpeakerProposal.objects.filter(camp=camp), + ), + ) + EventProposalUrlFactory.create_batch( + 100, + event_proposal=factory.Iterator( + EventProposal.objects.filter(track__camp=camp), + ), + ) + + def generate_speaker_availability(self, camp) -> None: + """Create SpeakerAvailability objects for the SpeakerProposals.""" + year = camp.camp.lower.year + self.output( + f"Generating random SpeakerProposalAvailability for {year}...", + ) + for sp in camp.speaker_proposals.all(): + # generate a matrix for this speaker_proposals event_types + matrix = get_speaker_availability_form_matrix( + sessions=sp.camp.event_sessions.filter( + event_type__in=sp.event_types.all(), + ), + ) + + # build a "form" object so we can reuse save_speaker_availability() + class FakeForm: + cleaned_data = {} + + form = FakeForm() + for daychunks in matrix.values(): + # 90% chance we have info for any given day + if random.randint(1, 100) > 90: + # no availability info for this entire day, sorry + continue + for data in daychunks.values(): + if not data: + continue + # 90% chance this speaker is available for any given chunk + form.cleaned_data[data["fieldname"]] = random.randint(1, 100) < 90 + # print(f"saving availability for speaker {sp}: {form.cleaned_data}") + save_speaker_availability(form, sp) + + def approve_speaker_proposals(self, camp) -> None: + """Approve all keynotes but reject 10% of other events.""" + for sp in camp.speaker_proposals.filter( + event_proposals__event_type__name="Keynote", + ): + sp.mark_as_approved() + + for sp in camp.speaker_proposals.filter(proposal_status="pending"): + # we do not approve all speakers + x = random.randint(1, 100) + if x < 90: + sp.mark_as_approved() + elif x < 95: + # leave this as pending + continue + else: + sp.mark_as_rejected() + + def approve_event_proposals(self, camp) -> None: + for ep in camp.event_proposals.filter(proposal_status="pending"): + # are all speakers for this event approved? + for sp in ep.speakers.all(): + if not hasattr(sp, "speaker"): + break + else: + # all speakers are approved, approve the event? always approve keynotes! + if random.randint(1, 100) < 90 or ep.event_type.name == "Keynote": + ep.mark_as_approved() + else: + ep.mark_as_rejected() + + # set demand for workshops to see the autoscheduler in action + for event in camp.events.filter(event_type__name="Workshop"): + # this should put about half the workshops in the big room + # (since the small rooms have max. 25 ppl capacity) + event.demand = random.randint(10, 40) + event.save() + + def create_camp_scheduling(self, camp, autoschedule) -> None: + year = camp.camp.lower.year + self.output(f"Creating scheduling for {year}...") + + # create a lunchbreak daily in speakers tent + lunch = Event.objects.get(track__camp=camp, title="Lunch break") + for day in camp.get_days(camppart="camp")[1:-1]: + date = day.lower.date() + start = tz.localize(datetime(date.year, date.month, date.day, 12, 0)) + lunchslot = EventSlot.objects.get( + event_session__event_location=camp.event_locations.get( + name="Speakers Tent", + ), + event_session__event_type=EventType.objects.get( + name="Recreational Event", + ), + when=(start, start + timedelta(hours=1)), + ) + lunchslot.event = lunch + lunchslot.autoscheduled = False + lunchslot.save() + + # exercise the autoscheduler a bit + if autoschedule: + scheduler = AutoScheduler(camp=camp) + schedulestart = timezone.now() + try: + autoschedule = scheduler.calculate_autoschedule() + if autoschedule: + scheduler.apply(autoschedule) + except ValueError as E: + self.output(f"Got exception while calculating autoschedule: {E}") + scheduleduration = timezone.now() - schedulestart + self.output( + f"Done running autoscheduler for {year}... It took {scheduleduration}", + ) + + def create_camp_speaker_event_conflicts(self, camp) -> None: + year = camp.camp.lower.year + self.output( + f"Generating event_conflicts for SpeakerProposals for {year}...", + ) + # loop over all + for sp in camp.speaker_proposals.all(): + # not all speakers add conflicts + if random.choice([True, True, False]): + # pick 0-10 events this speaker wants to attend + conflictcount = random.randint(0, 10) + sp.event_conflicts.set( + Event.objects.filter( + track__camp=camp, + event_type__support_speaker_event_conflicts=True, + ).order_by("?")[0:conflictcount], + ) + + def create_camp_rescheduling(self, camp, autoschedule) -> None: + year = camp.camp.lower.year + # reapprove all speaker_proposals so the new availability takes effect + for prop in camp.speaker_proposals.filter(proposal_status="approved"): + prop.mark_as_approved() + # exercise the autoscheduler a bit + self.output(f"Rescheduling {year}...") + if autoschedule: + scheduler = AutoScheduler(camp=camp) + schedulestart = timezone.now() + try: + autoschedule, diff = scheduler.calculate_similar_autoschedule() + scheduler.apply(autoschedule) + except ValueError as E: + self.output( + f"Got exception while calculating similar autoschedule: {E}", + ) + autoschedule = None + scheduleduration = timezone.now() - schedulestart + self.output(f"Done rescheduling for {year}... It took {scheduleduration}.") + + def create_camp_villages(self, camp, users) -> None: + year = camp.camp.lower.year + self.output(f"Creating villages for {year}...") + Village.objects.create( + contact=users[1], + camp=camp, + name="Baconsvin", + slug="baconsvin", + 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", + 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", + ) + + def create_camp_teams(self, camp) -> dict: + teams = {} + year = camp.camp.lower.year + self.output(f"Creating teams for {year}...") + teams["orga"] = Team.objects.create( + name="Orga", + description="The Orga team are the main organisers. All tasks are Orga responsibility until they are delegated to another team", + camp=camp, + needs_members=False, + ) + teams["info"] = Team.objects.create( + name="Info", + description="Info team manage the info pages and the info desk.", + camp=camp, + ) + teams["poc"] = Team.objects.create( + name="POC", + description="The POC team is in charge of establishing and running a phone network onsite.", + camp=camp, + ) + teams["noc"] = Team.objects.create( + name="NOC", + description="The NOC team is in charge of establishing and running a network onsite.", + camp=camp, + ) + teams["gis"] = Team.objects.create( + name="GIS", + description="The GIS team is in charge of managing the gis data.", + camp=camp, + ) + teams["bar"] = Team.objects.create( + name="Bar", + description="The Bar team plans, builds and run the IRL bar!", + camp=camp, + ) + teams["shuttle"] = Team.objects.create( + name="Shuttle", + description="The shuttle team drives people to and from the trainstation or the supermarket", + camp=camp, + ) + teams["power"] = Team.objects.create( + name="Power", + description="The power team makes sure we have power all over the venue", + camp=camp, + ) + teams["shit"] = Team.objects.create( + name="Sanitation", + description="Team shit takes care of the toilets", + camp=camp, + ) + teams["content"] = Team.objects.create( + name="Content", + description="The Content Team handles stuff on the program", + camp=camp, + mailing_list="content@example.com", + ) + teams["economy"] = Team.objects.create( + name="Economy", + description="The Economy Team handles the money and accounts.", + camp=camp, + mailing_list="economy@example.com", + ) + camp.economy_team = teams["economy"] + camp.save() + return teams + + def create_camp_team_tasks(self, camp, teams) -> None: + year = camp.camp.lower.year + self.output(f"Creating TeamTasks for {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", + ) + + def create_camp_team_memberships(self, camp, teams, users): + memberships = {} + year = camp.camp.lower.year + self.output(f"Creating team memberships for {year}...") + # noc team + memberships["noc"] = {} + memberships["noc"]["user4"] = TeamMember.objects.create( + team=teams["noc"], + user=users[4], + approved=True, + lead=True, + ) + memberships["noc"]["user1"] = TeamMember.objects.create( + team=teams["noc"], + user=users[1], + approved=True, + ) + memberships["noc"]["user5"] = TeamMember.objects.create( + team=teams["noc"], + user=users[5], + approved=True, + ) + memberships["noc"]["user2"] = TeamMember.objects.create( + 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"]["user2"] = TeamMember.objects.create( + team=teams["bar"], + user=users[2], + approved=True, + ) + memberships["bar"]["user7"] = TeamMember.objects.create( + team=teams["bar"], + user=users[7], + approved=True, + ) + memberships["bar"]["user8"] = TeamMember.objects.create( + 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"]["user9"] = TeamMember.objects.create( + 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, + ) + + # 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"]["user9"] = TeamMember.objects.create( + 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, + ) + + # gis team also gets a member + TeamMember.objects.create( + team=teams["gis"], + user=users[0], + lead=True, + approved=True, + ) + return memberships + + def create_camp_team_shifts(self, camp, teams, team_memberships) -> None: + shifts = {} + year = camp.camp.lower.year + self.output(f"Creating team shifts for {year}...") + shifts[0] = TeamShift.objects.create( + team=teams["shuttle"], + shift_range=( + tz.localize(datetime(year, 8, 27, 2, 0)), + tz.localize(datetime(year, 8, 27, 8, 0)), + ), + people_required=1, + ) + shifts[0].team_members.add(team_memberships["shuttle"]["user7"]) + shifts[1] = TeamShift.objects.create( + team=teams["shuttle"], + shift_range=( + tz.localize(datetime(year, 8, 27, 8, 0)), + tz.localize(datetime(year, 8, 27, 14, 0)), + ), + people_required=1, + ) + shifts[2] = TeamShift.objects.create( + team=teams["shuttle"], + shift_range=( + tz.localize(datetime(year, 8, 27, 14, 0)), + tz.localize(datetime(year, 8, 27, 20, 0)), + ), + people_required=1, + ) + + def create_camp_info_categories(self, camp, teams): + categories = {} + year = camp.camp.lower.year + self.output(f"Creating infocategories for {year}...") + categories["when"] = InfoCategory.objects.create( + team=teams["orga"], + headline="When is BornHack happening?", + anchor="when", + ) + categories["travel"] = InfoCategory.objects.create( + team=teams["orga"], + headline="Travel Information", + anchor="travel", + ) + categories["sleep"] = InfoCategory.objects.create( + team=teams["orga"], + headline="Where do I sleep?", + anchor="sleep", + ) + + return categories + + def create_camp_info_items(self, camp, categories) -> None: + year = camp.camp.lower.year + self.output(f"Creating infoitems for {year}...") + InfoItem.objects.create( + category=categories["when"], + headline="Opening", + anchor="opening", + body=f"BornHack {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 {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.", + ) + + def create_camp_feedback(self, camp, users) -> None: + year = camp.camp.lower.year + self.output(f"Creating feedback for {year}...") + Feedback.objects.create( + camp=camp, + user=users[1], + feedback="Awesome event, will be back next year", + ) + Feedback.objects.create( + camp=camp, + user=users[3], + feedback="Very nice, though a bit more hot water would be awesome", + ) + Feedback.objects.create( + camp=camp, + user=users[5], + feedback="Is there a token here?", + ) + Feedback.objects.create( + camp=camp, + user=users[9], + feedback="That was fun. Thanks!", + ) + + def create_camp_rides(self, camp, users) -> None: + year = camp.camp.lower.year + self.output(f"Creating rides for {year}...") + Ride.objects.create( + camp=camp, + user=users[1], + seats=2, + from_location="Copenhagen", + to_location="BornHack", + when=tz.localize(datetime(year, 8, 27, 12, 0)), + 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=tz.localize(datetime(year, 9, 4, 12, 0)), + 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=tz.localize(datetime(year, 8, 27, 12, 0)), + description="I need a ride and have a large backpack", + ) + + def create_camp_cfp(self, camp) -> None: + year = camp.camp.lower.year + self.output(f"Creating CFP for {year}...") + camp.call_for_participation_open = True + camp.call_for_participation = f"Please give a talk at Bornhack {year}..." + camp.save() + + def create_camp_cfs(self, camp) -> None: + year = camp.camp.lower.year + self.output(f"Creating CFS for {year}...") + camp.call_for_sponsors_open = True + camp.call_for_sponsors = f"Please give us ALL the money so that we can make Bornhack {year} the best ever!" + camp.save() + + def create_camp_sponsor_tiers(self, camp): + tiers = {} + year = camp.camp.lower.year + self.output(f"Creating sponsor tiers for {year}...") + tiers["platinum"] = SponsorTier.objects.create( + 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- promoted HackMe\n- sponsored social event", + camp=camp, + weight=0, + week_tickets=10, + ) + tiers["gold"] = SponsorTier.objects.create( + 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", + camp=camp, + weight=1, + week_tickets=10, + ) + tiers["silver"] = SponsorTier.objects.create( + 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", + camp=camp, + weight=2, + week_tickets=5, + ) + tiers["sponsor"] = SponsorTier.objects.create( + 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", + camp=camp, + weight=3, + week_tickets=2, + ) + + return tiers + + def create_camp_sponsors(self, camp, tiers) -> None: + year = camp.camp.lower.year + self.output(f"Creating sponsors for {year}...") + Sponsor.objects.create( + name="PROSA", + tier=tiers["platinum"], + description="Bus Trip", + logo_filename="PROSA-logo.png", + url="https://www.prosa.dk", + ) + Sponsor.objects.create( + name="DKUUG", + tier=tiers["platinum"], + description="Speakers tent", + logo_filename="DKUUGlogo.jpeg", + url="http://www.dkuug.dk/", + ) + Sponsor.objects.create( + name="LetsGo", + tier=tiers["silver"], + description="Shuttle", + logo_filename="letsgo.png", + url="https://letsgo.dk", + ) + Sponsor.objects.create( + name="Saxo Bank", + tier=tiers["gold"], + description="Cash Sponsorship", + logo_filename="saxobank.png", + url="https://home.saxo", + ) + Sponsor.objects.create( + name="CSIS", + tier=tiers["sponsor"], + description="Cash Sponsorship", + logo_filename="CSIS_PRI_LOGO_TURQUOISE_RGB.jpg", + url="https://csis.dk", + ) + + def create_camp_tokens(self, camp): + tokens = {} + year = camp.camp.lower.year + self.output(f"Creating tokens for {year}...") + tokens[0] = Token.objects.create( + camp=camp, + token=get_random_string(length=32), + category="Physical", + description="Token in the back of the speakers tent (in binary)", + active=True, + ) + tokens[1] = Token.objects.create( + camp=camp, + token=get_random_string(length=32), + category="Internet", + description="Twitter", + active=True, + ) + tokens[2] = Token.objects.create( + camp=camp, + token=get_random_string(length=32), + category="Website", + description="Token hidden in the X-Secret-Token HTTP header on the BornHack website", + active=True, + ) + tokens[3] = Token.objects.create( + camp=camp, + token=get_random_string(length=32), + category="Physical", + description="Token in infodesk (QR code)", + active=True, + ) + tokens[4] = Token.objects.create( + camp=camp, + token=get_random_string(length=32), + category="Physical", + description=f"Token on the back of the BornHack {year} badge", + active=True, + ) + tokens[5] = Token.objects.create( + camp=camp, + token=get_random_string(length=32), + category="Website", + description="Token hidden in EXIF data in the logo posted on the website sunday", + active=True, + ) + + return tokens + + def create_camp_token_finds(self, camp, tokens, users) -> None: + year = camp.camp.lower.year + self.output(f"Creating token finds for {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]) + for i in range(6): + TokenFind.objects.create(token=tokens[i], user=users[1]) + + def create_camp_expenses(self, camp) -> None: + self.output(f"Creating expenses for {camp}...") + for team in Team.objects.filter(camp=camp): + ExpenseFactory.create_batch(10, camp=camp, responsible_team=team) + + def create_camp_reimbursements(self, camp) -> None: + self.output(f"Creating reimbursements for {camp}...") + users = User.objects.filter( + id__in=Expense.objects.filter( + camp=camp, + reimbursement__isnull=True, + paid_by_bornhack=False, + approved=True, + ) + .values_list("user", flat=True) + .distinct(), + ) + for user in users: + expenses = Expense.objects.filter( + user=user, + approved=True, + reimbursement__isnull=True, + paid_by_bornhack=False, + ) + reimbursement = Reimbursement.objects.create( + camp=camp, + user=user, + reimbursement_user=user, + bank_account=random.randint(1000000000, 100000000000), + notes=f"bootstrap created reimbursement for user {user.username}", + paid=random.choice([True, True, False]), + ) + expenses.update(reimbursement=reimbursement) + reimbursement.create_payback_expense() + + def create_camp_revenues(self, camp) -> None: + self.output(f"Creating revenues for {camp}...") + RevenueFactory.create_batch(20, camp=camp) + + def add_team_permissions(self, camp) -> None: + """Assign member permissions to the team groups for this camp.""" + self.output(f"Assigning permissions to team groups for {camp}...") + permission_content_type = ContentType.objects.get_for_model(CampPermission) + for team in camp.teams.all(): + permission = Permission.objects.get( + content_type=permission_content_type, + codename=f"{team.slug}_team_member", + ) + team.group.permissions.add(permission) + + def create_maps_layer_generic(self) -> None: + group = MapGroup.objects.create(name="Generic") + layer = Layer.objects.create( + name="Areas", + slug="areas", + description="Venue areas", + icon="fa fa-list-ul", + group=group, + ) + Feature.objects.create( + layer=layer, + name="Orga", + description="Orga Area", + geom=GeometryCollection( + Polygon( + [ + [9.941073, 55.388305], + [9.940768, 55.388103], + [9.941146, 55.38796], + [9.941149, 55.388035], + [9.94132, 55.388201], + [9.941073, 55.388305], + ], + ), + ), + color="#ff00ffff", + icon="fa fa-hand-paper", + url="", + topic="", + processing="", + ) + + def create_camp_map_layer(self, camp) -> None: + group = MapGroup.objects.get(name="Generic") + team = Team.objects.get(name="Orga", camp=camp) + layer = Layer.objects.create( + name="Team Area", + description="Team areas", + icon="fa fa-list-ul", + group=group, + responsible_team=team, + ) + Feature.objects.create( + layer=layer, + name="Team Area", + description="Some Team Area", + geom=GeometryCollection( + Polygon( + [ + [9.940803, 55.38785], + [9.941136, 55.387826], + [9.941297, 55.387662], + [9.940943, 55.38754], + [9.940535, 55.387521], + [9.940803, 55.38785], + ], + ), + ), + color="#ff00ffff", + icon="fa fa-list", + url="", + topic="", + processing="", + ) + + def create_camp_pos(self, teams) -> None: + Pos.objects.create( + name="Infodesk", + team=teams["info"], + external_id="HHR9izotB6HLzgT6k", + ) + Pos.objects.create( + name="Bar", + team=teams["bar"], + external_id="bTasxE2YYXZh35wtQ", + ) + + def output(self, message) -> None: + logger.info(message) + + def bootstrap_full(self, options: dict) -> None: + 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": "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.run_bootstrap(options) + + def bootstrap_tests(self) -> None: + 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(2) + self.create_event_types() + teams = {} + for camp, read_only in self.camps: + year = camp.camp.lower.year + + teams[year] = self.create_camp_teams(camp) + + camp.read_only = read_only + camp.call_for_participation_open = not read_only + camp.call_for_sponsors_open = not read_only + camp.save() + + self.camp = self.camps[1][0] + self.teams = teams[self.camp.camp.lower.year] + + def run_bootstrap(self, options: dict) -> None: + self.output( + "----------[ Running bootstrap_devsite ]----------" + ) + + self.output("----------[ Global stuff ]----------") + + self.create_event_routing_types() + self.create_users(16) + + self.create_news() + + event_types = self.create_event_types() + + self.create_url_types() + + product_categories = self.create_product_categories() + + quickfeedback_options = self.create_quickfeedback_options() + + self.create_mobilepay_transactions() + + self.create_clearhaus_settlements() + + self.create_credebtors() + + self.create_bank_stuff() + + self.create_coinify_stuff() + + self.create_epay_transactions() + + self.create_maps_layer_generic() + + permissions_added = False + for camp, read_only in self.camps: + year = camp.camp.lower.year + + self.output( + f"----------[ Bornhack {year} ]----------" + ) + + if year <= settings.UPCOMING_CAMP_YEAR: + ticket_types = self.create_camp_ticket_types(camp) + + camp_products = self.create_camp_products( + camp, + product_categories, + ticket_types, + ) + + self.create_orders(self.users, camp_products) + + self.create_camp_tracks(camp) + + locations = self.create_event_locations(camp) + + self.create_camp_news(camp) + + teams = self.create_camp_teams(camp) + + 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_camp_team_tasks(camp, teams) + + team_memberships = self.create_camp_team_memberships(camp, teams, self.users) + + self.create_camp_team_shifts(camp, teams, team_memberships) + + self.create_camp_pos(teams) + + self.create_camp_cfp(camp) + + self.create_camp_proposals(camp, event_types) + + self.create_proposal_urls(camp) + + self.create_camp_event_sessions(camp, event_types, locations) + + self.generate_speaker_availability(camp) + + try: + self.approve_speaker_proposals(camp) + except ValidationError: + self.output( + "Name collision, bad luck. Run the bootstrap script again! PRs to make this less annoying welcome :)", + ) + sys.exit(1) + + self.approve_event_proposals(camp) + + self.create_camp_scheduling(camp, not options["skip_auto_scheduler"]) + + # shuffle it up - delete and create new random availability + self.generate_speaker_availability(camp) + + # and create some speaker<>event conflicts + self.create_camp_speaker_event_conflicts(camp) + + # recalculate the autoschedule + self.create_camp_rescheduling(camp, not options["skip_auto_scheduler"]) + + self.create_camp_villages(camp, self.users) + + facility_types = self.create_facility_types( + camp, + teams, + quickfeedback_options, + ) + + facilities = self.create_facilities(facility_types) + + self.create_facility_feedbacks(facilities, quickfeedback_options, self.users) + + info_categories = self.create_camp_info_categories(camp, teams) + + self.create_camp_info_items(camp, info_categories) + + self.create_camp_feedback(camp, self.users) + + self.create_camp_rides(camp, self.users) + + self.create_camp_cfs(camp) + + sponsor_tiers = self.create_camp_sponsor_tiers(camp) + + self.create_camp_sponsors(camp, sponsor_tiers) + + tokens = self.create_camp_tokens(camp) + + self.create_camp_token_finds(camp, tokens, self.users) + + self.create_camp_expenses(camp) + + self.create_camp_reimbursements(camp) + + self.create_camp_revenues(camp) + + self.create_camp_map_layer(camp) + else: + self.output("Not creating anything for this year yet") + + camp.read_only = read_only + camp.call_for_participation_open = not read_only + camp.call_for_sponsors_open = not read_only + camp.save() + + self.output("----------[ Finishing up ]----------") + + self.output("Adding event routing...") + Routing.objects.create( + team=teams["orga"], + eventtype=Type.objects.get(name="public_credit_name_changed"), + ) + Routing.objects.create( + team=teams["orga"], + eventtype=Type.objects.get(name="ticket_created"), + ) + + self.output("done!") + diff --git a/src/utils/bootstrap/factories.py b/src/utils/bootstrap/factories.py new file mode 100644 index 000000000..e26cb9377 --- /dev/null +++ b/src/utils/bootstrap/factories.py @@ -0,0 +1,165 @@ +from __future__ import annotations + +import logging +import random + +import factory +import pytz +from allauth.account.models import EmailAddress +from django.contrib.auth.models import User +from faker import Faker + +from camps.models import Camp +from economy.models import Chain +from economy.models import Credebtor +from economy.models import Expense +from economy.models import Revenue +from profiles.models import Profile +from program.models import EventProposal +from program.models import SpeakerProposal +from program.models import Url +from program.models import UrlType +from teams.models import Team +from utils.slugs import unique_slugify +from django.db.models.signals import post_save + +from .functions import output_fake_md_description +from .functions import output_fake_description + +fake = Faker() +tz = pytz.timezone("Europe/Copenhagen") +logger = logging.getLogger(f"bornhack.{__name__}") + + +class ChainFactory(factory.django.DjangoModelFactory): + class Meta: + model = Chain + + name = factory.Faker("company") + slug = factory.LazyAttribute( + lambda f: unique_slugify( + f.name, + Chain.objects.all().values_list("slug", flat=True), + ), + ) + + +class CredebtorFactory(factory.django.DjangoModelFactory): + class Meta: + model = Credebtor + + chain = factory.SubFactory(ChainFactory) + name = factory.Faker("company") + slug = factory.LazyAttribute( + lambda f: unique_slugify( + f.name, + Credebtor.objects.all().values_list("slug", flat=True), + ), + ) + address = factory.Faker("address", locale="dk_DK") + notes = factory.Faker("text") + + +class ExpenseFactory(factory.django.DjangoModelFactory): + class Meta: + model = Expense + + camp = factory.Faker("random_element", elements=Camp.objects.all()) + creditor = factory.Faker("random_element", elements=Credebtor.objects.all()) + user = factory.Faker("random_element", elements=User.objects.all()) + amount = factory.Faker("random_int", min=20, max=20000) + description = factory.Faker("text") + paid_by_bornhack = factory.Faker("random_element", elements=[True, True, False]) + invoice = factory.django.ImageField( + color=random.choice(["#ff0000", "#00ff00", "#0000ff"]), + ) + invoice_date = factory.Faker("date") + responsible_team = factory.Faker("random_element", elements=Team.objects.all()) + approved = factory.Faker("random_element", elements=[True, True, False]) + notes = factory.Faker("text") + + +class RevenueFactory(factory.django.DjangoModelFactory): + class Meta: + model = Revenue + + camp = factory.Faker("random_element", elements=Camp.objects.all()) + debtor = factory.Faker("random_element", elements=Credebtor.objects.all()) + user = factory.Faker("random_element", elements=User.objects.all()) + amount = factory.Faker("random_int", min=20, max=20000) + description = factory.Faker("text") + invoice = factory.django.ImageField( + color=random.choice(["#ff0000", "#00ff00", "#0000ff"]), + ) + invoice_date = factory.Faker("date") + responsible_team = factory.Faker("random_element", elements=Team.objects.all()) + approved = factory.Faker("random_element", elements=[True, True, False]) + notes = factory.Faker("text") + + +class ProfileFactory(factory.django.DjangoModelFactory): + class Meta: + model = Profile + + user = factory.SubFactory("self.UserFactory", profile=None) + name = factory.Faker("name") + description = factory.Faker("text") + public_credit_name = factory.Faker("name") + public_credit_name_approved = True + + +@factory.django.mute_signals(post_save) +class UserFactory(factory.django.DjangoModelFactory): + class Meta: + model = User + + profile = factory.RelatedFactory(ProfileFactory, "user") + + +class EmailAddressFactory(factory.django.DjangoModelFactory): + class Meta: + model = EmailAddress + + primary = False + verified = True + +class SpeakerProposalFactory(factory.django.DjangoModelFactory): + class Meta: + model = SpeakerProposal + + name = factory.Faker("name") + email = factory.Faker("email") + biography = output_fake_md_description() + submission_notes = factory.Iterator(["", output_fake_description()]) + needs_oneday_ticket = factory.Iterator([True, False]) + + +class EventProposalFactory(factory.django.DjangoModelFactory): + class Meta: + model = EventProposal + + user = factory.Iterator(User.objects.all()) + title = factory.Faker("sentence") + abstract = output_fake_md_description() + allow_video_recording = factory.Iterator([True, True, True, False]) + allow_video_streaming = factory.Iterator([True, True, True, False]) + submission_notes = factory.Iterator(["", output_fake_description()]) + use_provided_speaker_laptop = factory.Iterator([True, False]) + + +class EventProposalUrlFactory(factory.django.DjangoModelFactory): + class Meta: + model = Url + + url = factory.Faker("url") + url_type = factory.Iterator(UrlType.objects.all()) + + +class SpeakerProposalUrlFactory(factory.django.DjangoModelFactory): + class Meta: + model = Url + + url = factory.Faker("url") + url_type = factory.Iterator(UrlType.objects.all()) + + diff --git a/src/utils/bootstrap/functions.py b/src/utils/bootstrap/functions.py new file mode 100644 index 000000000..6b85ab335 --- /dev/null +++ b/src/utils/bootstrap/functions.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import logging + +import pytz +from faker import Faker + + +fake = Faker() +tz = pytz.timezone("Europe/Copenhagen") +logger = logging.getLogger(f"bornhack.{__name__}") + + + +def output_fake_md_description(): + fake_text = "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) + fake_text += "\n\n" + fake_text += "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) + fake_text += "\n\n" + fake_text += "## " + fake.sentence(nb_words=3) + "\n" + fake_text += "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) + fake_text += "\n\n" + fake_text += '![The image is not awailable](/static/img/na.jpg "not available")' + fake_text += "\n\n" + fake_text += "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) + fake_text += "\n\n" + fake_text += "* [" + fake.sentence(nb_words=3) + "](" + fake.uri() + ")\n" + fake_text += "* [" + fake.sentence(nb_words=3) + "](" + fake.uri() + ")\n" + return fake_text + + +def output_fake_description(): + fake_text = "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) + fake_text += "* [" + fake.sentence(nb_words=3) + "](" + fake.uri() + ")\n" + return fake_text + diff --git a/src/utils/management/commands/bootstrap_devsite.py b/src/utils/management/commands/bootstrap_devsite.py index 1aab63b2f..0c5b086df 100644 --- a/src/utils/management/commands/bootstrap_devsite.py +++ b/src/utils/management/commands/bootstrap_devsite.py @@ -1,256 +1,14 @@ from __future__ import annotations import logging -import random -import sys -import uuid -from datetime import datetime -from datetime import timedelta -import factory -import pytz -from allauth.account.models import EmailAddress -from django.conf import settings -from django.contrib.auth.models import Permission -from django.contrib.auth.models import User -from django.contrib.contenttypes.models import ContentType -from django.contrib.gis.geos import GeometryCollection -from django.contrib.gis.geos import Point -from django.contrib.gis.geos import Polygon -from django.core.exceptions import ValidationError +from utils.bootstrap.base import Bootstrap from django.core.management import call_command from django.core.management.base import BaseCommand -from django.db.models.signals import post_save from django.utils import timezone -from django.utils.crypto import get_random_string -from faker import Faker -from camps.models import Camp -from camps.models import Permission as CampPermission -from economy.factories import BankAccountFactory -from economy.factories import BankFactory -from economy.factories import BankTransactionFactory -from economy.factories import ClearhausSettlementFactory -from economy.factories import CoinifyBalanceFactory -from economy.factories import CoinifyInvoiceFactory -from economy.factories import CoinifyPaymentIntentFactory -from economy.factories import CoinifyPayoutFactory -from economy.factories import CoinifySettlementFactory -from economy.factories import EpayTransactionFactory -from economy.factories import MobilePayTransactionFactory -from economy.factories import ZettleBalanceFactory -from economy.factories import ZettleReceiptFactory -from economy.models import Chain -from economy.models import Credebtor -from economy.models import Expense -from economy.models import Pos -from economy.models import Reimbursement -from economy.models import Revenue -from events.models import Routing -from events.models import Type -from facilities.models import Facility -from facilities.models import FacilityFeedback -from facilities.models import FacilityQuickFeedback -from facilities.models import FacilityType -from feedback.models import Feedback -from info.models import InfoCategory -from info.models import InfoItem -from maps.models import Feature -from maps.models import Group as MapGroup -from maps.models import Layer -from news.models import NewsItem -from profiles.models import Profile -from program.autoscheduler import AutoScheduler -from program.models import Event -from program.models import EventLocation -from program.models import EventProposal -from program.models import EventSession -from program.models import EventSlot -from program.models import EventTrack -from program.models import EventType -from program.models import SpeakerProposal -from program.models import Url -from program.models import UrlType -from program.utils import get_speaker_availability_form_matrix -from program.utils import save_speaker_availability -from rideshare.models import Ride -from shop.models import Order -from shop.models import Product -from shop.models import ProductCategory -from sponsors.models import Sponsor -from sponsors.models import SponsorTier -from teams.models import Team -from teams.models import TeamMember -from teams.models import TeamShift -from teams.models import TeamTask -from tickets.models import TicketType -from tokens.models import Token -from tokens.models import TokenFind -from utils.slugs import unique_slugify -from villages.models import Village - -fake = Faker() -# Faker.seed(0) -tz = pytz.timezone("Europe/Copenhagen") logger = logging.getLogger(f"bornhack.{__name__}") - -class ChainFactory(factory.django.DjangoModelFactory): - class Meta: - model = Chain - - name = factory.Faker("company") - slug = factory.LazyAttribute( - lambda f: unique_slugify( - f.name, - Chain.objects.all().values_list("slug", flat=True), - ), - ) - - -class CredebtorFactory(factory.django.DjangoModelFactory): - class Meta: - model = Credebtor - - chain = factory.SubFactory(ChainFactory) - name = factory.Faker("company") - slug = factory.LazyAttribute( - lambda f: unique_slugify( - f.name, - Credebtor.objects.all().values_list("slug", flat=True), - ), - ) - address = factory.Faker("address", locale="dk_DK") - notes = factory.Faker("text") - - -class ExpenseFactory(factory.django.DjangoModelFactory): - class Meta: - model = Expense - - camp = factory.Faker("random_element", elements=Camp.objects.all()) - creditor = factory.Faker("random_element", elements=Credebtor.objects.all()) - user = factory.Faker("random_element", elements=User.objects.all()) - amount = factory.Faker("random_int", min=20, max=20000) - description = factory.Faker("text") - paid_by_bornhack = factory.Faker("random_element", elements=[True, True, False]) - invoice = factory.django.ImageField( - color=random.choice(["#ff0000", "#00ff00", "#0000ff"]), - ) - invoice_date = factory.Faker("date") - responsible_team = factory.Faker("random_element", elements=Team.objects.all()) - approved = factory.Faker("random_element", elements=[True, True, False]) - notes = factory.Faker("text") - - -class RevenueFactory(factory.django.DjangoModelFactory): - class Meta: - model = Revenue - - camp = factory.Faker("random_element", elements=Camp.objects.all()) - debtor = factory.Faker("random_element", elements=Credebtor.objects.all()) - user = factory.Faker("random_element", elements=User.objects.all()) - amount = factory.Faker("random_int", min=20, max=20000) - description = factory.Faker("text") - invoice = factory.django.ImageField( - color=random.choice(["#ff0000", "#00ff00", "#0000ff"]), - ) - invoice_date = factory.Faker("date") - responsible_team = factory.Faker("random_element", elements=Team.objects.all()) - approved = factory.Faker("random_element", elements=[True, True, False]) - notes = factory.Faker("text") - - -class ProfileFactory(factory.django.DjangoModelFactory): - class Meta: - model = Profile - - user = factory.SubFactory("self.UserFactory", profile=None) - name = factory.Faker("name") - description = factory.Faker("text") - public_credit_name = factory.Faker("name") - public_credit_name_approved = True - - -@factory.django.mute_signals(post_save) -class UserFactory(factory.django.DjangoModelFactory): - class Meta: - model = User - - profile = factory.RelatedFactory(ProfileFactory, "user") - - -class EmailAddressFactory(factory.django.DjangoModelFactory): - class Meta: - model = EmailAddress - - primary = False - verified = True - - -def output_fake_md_description(): - fake_text = "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) - fake_text += "\n\n" - fake_text += "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) - fake_text += "\n\n" - fake_text += "## " + fake.sentence(nb_words=3) + "\n" - fake_text += "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) - fake_text += "\n\n" - fake_text += '![The image is not awailable](/static/img/na.jpg "not available")' - fake_text += "\n\n" - fake_text += "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) - fake_text += "\n\n" - fake_text += "* [" + fake.sentence(nb_words=3) + "](" + fake.uri() + ")\n" - fake_text += "* [" + fake.sentence(nb_words=3) + "](" + fake.uri() + ")\n" - return fake_text - - -def output_fake_description(): - fake_text = "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) - fake_text += "* [" + fake.sentence(nb_words=3) + "](" + fake.uri() + ")\n" - return fake_text - - -class SpeakerProposalFactory(factory.django.DjangoModelFactory): - class Meta: - model = SpeakerProposal - - name = factory.Faker("name") - email = factory.Faker("email") - biography = output_fake_md_description() - submission_notes = factory.Iterator(["", output_fake_description()]) - needs_oneday_ticket = factory.Iterator([True, False]) - - -class EventProposalFactory(factory.django.DjangoModelFactory): - class Meta: - model = EventProposal - - user = factory.Iterator(User.objects.all()) - title = factory.Faker("sentence") - abstract = output_fake_md_description() - allow_video_recording = factory.Iterator([True, True, True, False]) - allow_video_streaming = factory.Iterator([True, True, True, False]) - submission_notes = factory.Iterator(["", output_fake_description()]) - use_provided_speaker_laptop = factory.Iterator([True, False]) - - -class EventProposalUrlFactory(factory.django.DjangoModelFactory): - class Meta: - model = Url - - url = factory.Faker("url") - url_type = factory.Iterator(UrlType.objects.all()) - - -class SpeakerProposalUrlFactory(factory.django.DjangoModelFactory): - class Meta: - model = Url - - url = factory.Faker("url") - url_type = factory.Iterator(UrlType.objects.all()) - - class Command(BaseCommand): args = "none" help = "Create mock data for development instances" @@ -263,1914 +21,6 @@ def add_arguments(self, parser) -> None: 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 create_camps(self): - self.output("Creating camps...") - 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, - "add_permissions": True, - }, - { - "year": 2025, - "tagline": "Undecided", - "colour": "#ffafc7", - "read_only": False, - "light_text": False, - }, - { - "year": 2026, - "tagline": "Undecided", - "colour": "#ffffff", - "read_only": False, - "light_text": False, - }, - ] - - camp_instances = [] - - for camp in camps: - year = camp["year"] - read_only = camp["read_only"] - camp_instances.append( - ( - Camp.objects.create( - title=f"BornHack {year}", - tagline=camp["tagline"], - slug=f"bornhack-{year}", - shortslug=f"bornhack-{year}", - buildup=( - tz.localize(datetime(year, 8, 25, 12, 0)), - tz.localize(datetime(year, 8, 27, 12, 0)), - ), - camp=( - tz.localize(datetime(year, 8, 27, 12, 0)), - tz.localize(datetime(year, 9, 3, 12, 0)), - ), - teardown=( - tz.localize(datetime(year, 9, 3, 12, 0)), - tz.localize(datetime(year, 9, 5, 12, 0)), - ), - colour=camp["colour"], - light_text=camp.get("light_text", True), - ), - read_only, - ), - ) - - return camp_instances - - 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): - self.output("Creating users...") - - users = {} - - for i in range(16): - username = f"user{i}" - user = UserFactory.create( - username=str(uuid.uuid4()), - email=f"{username}@example.com", - ) - user.set_password(username) - user.save() - users[i] = user - EmailAddressFactory.create( - user=user, - email=f"{username}@example.com", - primary=True, - ) - - admin = User.objects.create_superuser( - username="admin", - email="admin@example.com", - password="admin", - ) - users["admin"] = admin - admin.profile.name = "Administrator" - admin.profile.description = "Default adminstrative user" - admin.profile.public_credit_name = "Administrator" - admin.profile.public_credit_name_approved = True - admin.profile.save() - EmailAddress.objects.create( - user=admin, - email="admin@example.com", - verified=True, - primary=True, - ) - - return users - - def create_news(self) -> None: - NewsItem.objects.create( - title="unpublished news item", - content="unpublished news body here", - ) - - def create_quickfeedback_options(self): - options = {} - self.output("Creating quickfeedback options") - options["na"] = FacilityQuickFeedback.objects.create( - feedback="N/A", - icon="fas fa-times", - ) - options["attention"] = FacilityQuickFeedback.objects.create( - feedback="Needs attention", - ) - options["toiletpaper"] = FacilityQuickFeedback.objects.create( - feedback="Needs more toiletpaper", - icon="fas fa-toilet-paper", - ) - options["cleaning"] = FacilityQuickFeedback.objects.create( - feedback="Needs cleaning", - icon="fas fa-broom", - ) - options["power"] = FacilityQuickFeedback.objects.create( - feedback="No power", - icon="fas fa-bolt", - ) - return options - - def create_mobilepay_transactions(self) -> None: - self.output("Creating MobilePay Transactions...") - MobilePayTransactionFactory.create_batch(50) - - def create_clearhaus_settlements(self) -> None: - self.output("Creating Clearhaus Settlements...") - ClearhausSettlementFactory.create_batch(50) - - def create_zettle_stuff(self) -> None: - self.output("Creating Zettle receipts and balances...") - ZettleBalanceFactory.create_batch(100) - ZettleReceiptFactory.create_batch(100) - - def create_bank_stuff(self) -> None: - self.output("Creating Banks, BankAccounts, and BankTransactions...") - BankFactory.create_batch(2) - BankAccountFactory.create_batch(16) - BankTransactionFactory.create_batch(300) - - def create_coinify_stuff(self) -> None: - self.output( - "Creating Coinify invoices, payment intents, payouts, settlements and balances...", - ) - CoinifyInvoiceFactory.create_batch(50) - CoinifyPaymentIntentFactory.create_batch(50) - CoinifyPayoutFactory.create_batch(10) - CoinifyBalanceFactory.create_batch(10) - CoinifySettlementFactory.create_batch(10) - - def create_epay_transactions(self) -> None: - self.output("Creating ePay Transactions...") - EpayTransactionFactory.create_batch(50) - - def create_facility_types(self, camp, teams, options): - types = {} - self.output("Creating facility types...") - types["toilet"] = FacilityType.objects.create( - name="Toilets", - description="All the toilets", - icon="fas fa-toilet", - marker="greyIcon", - responsible_team=teams["shit"], - ) - types["toilet"].quickfeedback_options.add(options["na"]) - types["toilet"].quickfeedback_options.add(options["attention"]) - types["toilet"].quickfeedback_options.add(options["toiletpaper"]) - types["toilet"].quickfeedback_options.add(options["cleaning"]) - - types["power"] = FacilityType.objects.create( - name="Power Infrastructure", - description="Power related infrastructure, distribution points, distribution cables, and so on.", - icon="fas fa-plug", - marker="goldIcon", - responsible_team=teams["power"], - ) - types["power"].quickfeedback_options.add(options["attention"]) - types["power"].quickfeedback_options.add(options["power"]) - return types - - def create_facilities(self, facility_types): - facilities = {} - self.output("Creating facilities...") - facilities["toilet1"] = Facility.objects.create( - 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( - 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( - facility_type=facility_types["power"], - name="PDP1", - description="In orga area", - location=Point(9.94079, 55.388022), - ) - facilities["pdp2"] = Facility.objects.create( - facility_type=facility_types["power"], - name="PDP2", - description="In bar area", - location=Point(9.942036, 55.387891), - ) - facilities["pdp3"] = Facility.objects.create( - facility_type=facility_types["power"], - name="PDP3", - description="In speaker tent", - location=Point(9.938416, 55.387109), - ) - facilities["pdp4"] = Facility.objects.create( - facility_type=facility_types["power"], - name="PDP4", - description="In food area", - location=Point(9.940146, 55.386983), - ) - return facilities - - def create_facility_feedbacks(self, facilities, options, users) -> None: - 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, - ) - - def create_event_types(self): - types = {} - self.output("Creating event types...") - types["workshop"] = EventType.objects.create( - name="Workshop", - slug="workshop", - color="#ff9900", - light_text=False, - public=True, - description="Workshops actively involve the participants in the learning experience", - icon="toolbox", - host_title="Host", - event_duration_minutes="180", - support_autoscheduling=True, - support_speaker_event_conflicts=True, - ) - - types["talk"] = EventType.objects.create( - name="Talk", - slug="talk", - color="#2D9595", - light_text=True, - public=True, - description="A presentation on a stage", - icon="chalkboard-teacher", - host_title="Speaker", - event_duration_minutes="60", - support_autoscheduling=True, - support_speaker_event_conflicts=True, - ) - - types["lightning"] = EventType.objects.create( - name="Lightning Talk", - slug="lightning-talk", - color="#ff0000", - light_text=True, - public=True, - description="A short 5-10 minute presentation", - icon="bolt", - host_title="Speaker", - event_duration_minutes="5", - support_speaker_event_conflicts=True, - ) - - types["music"] = EventType.objects.create( - name="Music Act", - slug="music", - color="#1D0095", - light_text=True, - public=True, - description="A musical performance", - icon="music", - host_title="Artist", - event_duration_minutes="180", - support_autoscheduling=True, - support_speaker_event_conflicts=True, - ) - - types["keynote"] = EventType.objects.create( - name="Keynote", - slug="keynote", - color="#FF3453", - light_text=True, - description="A keynote presentation", - icon="star", - host_title="Speaker", - event_duration_minutes="90", - support_autoscheduling=True, - support_speaker_event_conflicts=True, - ) - - types["debate"] = EventType.objects.create( - name="Debate", - slug="debate", - color="#F734C3", - light_text=True, - description="A panel debate with invited guests", - icon="users", - host_title="Guest", - public=True, - event_duration_minutes="120", - support_autoscheduling=True, - support_speaker_event_conflicts=True, - ) - - types["facility"] = EventType.objects.create( - name="Facilities", - slug="facilities", - color="#cccccc", - light_text=False, - include_in_event_list=False, - description="Events involving facilities like bathrooms, food area and so on", - icon="home", - host_title="Host", - event_duration_minutes="720", - support_speaker_event_conflicts=False, - ) - - types["recreational"] = EventType.objects.create( - name="Recreational Event", - slug="recreational-event", - color="#0000ff", - light_text=True, - public=True, - description="Events of a recreational nature", - icon="dice", - host_title="Host", - event_duration_minutes="600", - support_autoscheduling=False, - support_speaker_event_conflicts=True, - ) - - return types - - def create_url_types(self) -> None: - self.output("Creating UrlType objects...") - t, created = UrlType.objects.get_or_create( - name="Other", - defaults={"icon": "fas fa-link"}, - ) - t, created = UrlType.objects.get_or_create( - name="Homepage", - defaults={"icon": "fas fa-link"}, - ) - t, created = UrlType.objects.get_or_create( - name="Slides", - defaults={"icon": "fas fa-chalkboard-teacher"}, - ) - t, created = UrlType.objects.get_or_create( - name="Twitter", - defaults={"icon": "fab fa-twitter"}, - ) - t, created = UrlType.objects.get_or_create( - name="Mastodon", - defaults={"icon": "fab fa-mastodon"}, - ) - t, created = UrlType.objects.get_or_create( - name="Facebook", - defaults={"icon": "fab fa-facebook"}, - ) - t, created = UrlType.objects.get_or_create( - name="Project", - defaults={"icon": "fas fa-link"}, - ) - t, created = UrlType.objects.get_or_create( - name="Blog", - defaults={"icon": "fas fa-link"}, - ) - t, created = UrlType.objects.get_or_create( - name="Github", - defaults={"icon": "fab fa-github"}, - ) - t, created = UrlType.objects.get_or_create( - name="Keybase", - defaults={"icon": "fab fa-keybase"}, - ) - t, created = UrlType.objects.get_or_create( - name="Recording", - defaults={"icon": "fas fa-film"}, - ) - - def create_credebtors(self) -> None: - self.output("Creating Chains and Credebtors...") - try: - CredebtorFactory.create_batch(50) - except ValidationError: - self.output("Name conflict, retrying...") - CredebtorFactory.create_batch(50) - for _ in range(20): - # add 20 more credebtors to random existing chains - try: - CredebtorFactory.create(chain=Chain.objects.order_by("?").first()) - except ValidationError: - self.output("Name conflict, skipping...") - continue - # add a credebtor for reimbursements - reimbursement_chain = Chain.objects.create( - name="Reimbursement", - notes="This chain is only used for reimbursements", - ) - Credebtor.objects.create( - chain=reimbursement_chain, - name="Reimbursement", - address="Nowhere", - ) - - def create_product_categories(self): - categories = {} - self.output("Creating productcategories...") - categories["transportation"] = ProductCategory.objects.create( - name="Transportation", - slug="transportation", - ) - categories["merchandise"] = ProductCategory.objects.create( - name="Merchandise", - slug="merchandise", - ) - categories["tickets"] = ProductCategory.objects.create( - name="Tickets", - slug="tickets", - ) - categories["villages"] = ProductCategory.objects.create( - name="Villages", - slug="villages", - ) - categories["facilities"] = ProductCategory.objects.create( - name="Facilities", - slug="facilities", - ) - categories["packages"] = ProductCategory.objects.create( - name="Packages", - slug="packages", - ) - - return categories - - def create_camp_ticket_types(self, camp): - types = {} - self.output(f"Creating tickettypes for {camp.camp.lower.year}...") - types["adult_full_week"] = TicketType.objects.create( - name="Adult Full Week", - camp=camp, - ) - types["adult_one_day"] = TicketType.objects.create( - name="Adult One Day", - camp=camp, - ) - types["child_full_week"] = TicketType.objects.create( - name="Child Full Week", - camp=camp, - ) - types["child_one_day"] = TicketType.objects.create( - name="Child One Day", - camp=camp, - ) - types["village"] = TicketType.objects.create( - name="Village", - camp=camp, - ) - types["merchandise"] = TicketType.objects.create( - name="Merchandise", - camp=camp, - ) - types["facilities"] = TicketType.objects.create( - name="Facilities", - camp=camp, - single_ticket_per_product=True, - ) - types["transportation"] = TicketType.objects.create( - name="Transportation", - camp=camp, - ) - - return types - - def create_camp_products(self, camp, categories, ticket_types): - products = {} - year = camp.camp.lower.year - camp_prefix = f"BornHack {year}" - - name = f"{camp_prefix} Standard ticket" - products["ticket1"] = Product.objects.create( - name=name, - description="A ticket", - price=1200, - category=categories["tickets"], - available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), - ), - slug=unique_slugify( - name, - slugs_in_use=Product.objects.filter( - category=categories["tickets"], - ).values_list("slug", flat=True), - ), - ticket_type=ticket_types["adult_full_week"], - ) - - name = f"{camp_prefix} Hacker ticket" - products["ticket2"] = Product.objects.create( - name=name, - description="Another ticket", - price=1337, - category=categories["tickets"], - available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), - ), - slug=unique_slugify( - name, - slugs_in_use=Product.objects.filter( - category=categories["tickets"], - ).values_list("slug", flat=True), - ), - ticket_type=ticket_types["adult_full_week"], - ) - - name = f"{camp_prefix} One day ticket" - products["one_day_ticket"] = Product.objects.create( - name=name, - description="One day ticket", - price=300, - category=categories["tickets"], - available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), - ), - slug=unique_slugify( - name, - slugs_in_use=Product.objects.filter( - category=categories["tickets"], - ).values_list("slug", flat=True), - ), - ticket_type=ticket_types["adult_one_day"], - ) - - name = f"{camp_prefix} Village tent 3x3 meters, no floor" - products["tent1"] = Product.objects.create( - name=name, - description="A description of the tent goes here", - price=3325, - category=categories["villages"], - available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), - ), - slug=unique_slugify( - name, - slugs_in_use=Product.objects.filter( - category=categories["villages"], - ).values_list("slug", flat=True), - ), - ticket_type=ticket_types["village"], - ) - - name = f"{camp_prefix} Village tent 3x3 meters, with floor" - products["tent2"] = Product.objects.create( - name=name, - description="A description of the tent goes here", - price=3675, - category=categories["villages"], - available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), - ), - slug=unique_slugify( - name, - slugs_in_use=Product.objects.filter( - category=categories["villages"], - ).values_list("slug", flat=True), - ), - ticket_type=ticket_types["village"], - ) - - name = f"{camp_prefix} T-shirt Large" - products["t-shirt-large"] = Product.objects.create( - name=name, - description="A description of the t-shirt goes here", - price=150, - category=categories["merchandise"], - available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), - ), - slug=unique_slugify( - name, - slugs_in_use=Product.objects.filter( - category=categories["merchandise"], - ).values_list("slug", flat=True), - ), - ticket_type=ticket_types["merchandise"], - ) - - name = f"{camp_prefix} T-shirt Medium" - products["t-shirt-medium"] = Product.objects.create( - name=name, - description="A description of the t-shirt goes here", - price=150, - category=categories["merchandise"], - available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), - ), - slug=unique_slugify( - name, - slugs_in_use=Product.objects.filter( - category=categories["merchandise"], - ).values_list("slug", flat=True), - ), - ticket_type=ticket_types["merchandise"], - ) - - name = f"{camp_prefix} T-shirt Small" - products["t-shirt-small"] = Product.objects.create( - name=name, - description="A description of the t-shirt goes here", - price=150, - category=categories["merchandise"], - available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), - ), - slug=unique_slugify( - name, - slugs_in_use=Product.objects.filter( - category=categories["merchandise"], - ).values_list("slug", flat=True), - ), - ticket_type=ticket_types["merchandise"], - ) - - name = "100 HAX" - products["hax"] = Product.objects.create( - name=name, - description="100 HAX", - price=100, - category=categories["facilities"], - available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), - ), - slug=unique_slugify( - name, - slugs_in_use=Product.objects.filter( - category=categories["facilities"], - ).values_list("slug", flat=True), - ), - ticket_type=ticket_types["facilities"], - ) - - name = "Corporate Hackers Small" - products["corporate_hackers_small"] = Product.objects.create( - name=name, - description="Send your company to BornHack in style with one of our corporate packages!", - price=18000, - category=categories["packages"], - available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), - ), - slug=unique_slugify( - name, - slugs_in_use=Product.objects.filter( - category=categories["packages"], - ).values_list("slug", flat=True), - ), - ) - products["corporate_hackers_small"].sub_products.add( - products["ticket1"], - through_defaults={ - "number_of_tickets": 3, - }, - ) - products["corporate_hackers_small"].sub_products.add( - products["one_day_ticket"], - through_defaults={ - "number_of_tickets": 3, - }, - ) - products["corporate_hackers_small"].sub_products.add( - products["tent2"], - through_defaults={ - "number_of_tickets": 1, - }, - ) - products["corporate_hackers_small"].sub_products.add( - products["hax"], - through_defaults={ - "number_of_tickets": 25, - }, - ) - - return products - - def create_orders(self, users, camp_products): - orders = {} - self.output("Creating orders...") - orders[0] = Order.objects.create( - user=users[1], - payment_method="in_person", - open=None, - paid=True, - ) - orders[0].oprs.create(product=camp_products["ticket1"], quantity=1) - orders[0].oprs.create(product=camp_products["tent1"], quantity=1) - orders[0].mark_as_paid(request=None) - - orders[1] = Order.objects.create( - user=users[2], - payment_method="in_person", - open=None, - ) - orders[1].oprs.create(product=camp_products["ticket1"], quantity=1) - orders[1].oprs.create(product=camp_products["tent2"], quantity=1) - orders[1].oprs.create(product=camp_products["t-shirt-medium"], quantity=1) - orders[1].mark_as_paid(request=None) - - orders[2] = Order.objects.create( - user=users[3], - payment_method="in_person", - open=None, - ) - orders[2].oprs.create(product=camp_products["ticket2"], quantity=1) - orders[2].oprs.create(product=camp_products["ticket1"], quantity=1) - orders[2].oprs.create(product=camp_products["tent2"], quantity=1) - orders[2].oprs.create(product=camp_products["t-shirt-small"], quantity=1) - orders[2].oprs.create(product=camp_products["t-shirt-large"], quantity=1) - orders[2].mark_as_paid(request=None) - - orders[3] = Order.objects.create( - user=users[4], - payment_method="in_person", - open=None, - ) - orders[3].oprs.create(product=camp_products["ticket2"], quantity=1) - orders[3].oprs.create(product=camp_products["tent1"], quantity=1) - orders[3].oprs.create(product=camp_products["t-shirt-small"], quantity=1) - orders[3].oprs.create(product=camp_products["hax"], quantity=30) - orders[3].mark_as_paid(request=None) - - return orders - - def create_camp_tracks(self, camp): - tracks = {} - year = camp.camp.lower.year - self.output(f"Creating event_tracks for {year}...") - tracks[1] = EventTrack.objects.create( - camp=camp, - name=f"BornHack {year}", - slug=camp.slug, - ) - - return tracks - - def create_event_locations(self, camp): - locations = {} - year = camp.camp.lower.year - self.output(f"Creating event_locations for {year}...") - locations["speakers_tent"] = EventLocation.objects.create( - name="Speakers Tent", - slug="speakers-tent", - icon="comment", - camp=camp, - capacity=150, - ) - locations["workshop_room_1"] = EventLocation.objects.create( - name="Workshop room 1 (big)", - slug="workshop-room-1", - icon="briefcase", - camp=camp, - capacity=50, - ) - locations["workshop_room_2"] = EventLocation.objects.create( - name="Workshop room 2 (small)", - slug="workshop-room-2", - icon="briefcase", - camp=camp, - capacity=25, - ) - locations["workshop_room_3"] = EventLocation.objects.create( - name="Workshop room 3 (small)", - slug="workshop-room-3", - icon="briefcase", - camp=camp, - capacity=25, - ) - locations["bar_area"] = EventLocation.objects.create( - name="Bar Area", - slug="bar-area", - icon="glass-cheers", - camp=camp, - capacity=50, - ) - locations["food_area"] = EventLocation.objects.create( - name="Food Area", - slug="food-area", - icon="utensils", - camp=camp, - capacity=50, - ) - locations["infodesk"] = EventLocation.objects.create( - name="Infodesk", - slug="infodesk", - icon="info", - camp=camp, - capacity=20, - ) - - # 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"]) - locations["workshop_room_1"].conflicts.add(locations["workshop_room_3"]) - - return locations - - def create_camp_news(self, camp) -> None: - year = camp.camp.lower.year - self.output(f"Creating news for {year}...") - NewsItem.objects.create( - title=f"Welcome to {camp.title}", - content="news body here with html support", - published_at=tz.localize(datetime(year, 8, 27, 12, 0)), - ) - NewsItem.objects.create( - title=f"{camp.title} is over", - content="news body here", - published_at=tz.localize(datetime(year, 9, 4, 12, 0)), - ) - - def create_camp_event_sessions(self, camp, event_types, event_locations) -> None: - self.output(f"Creating EventSessions for {camp}...") - days = camp.get_days(camppart="camp")[1:-1] - for day in days: - start = day.lower - EventSession.objects.create( - camp=camp, - event_type=event_types["talk"], - event_location=event_locations["speakers_tent"], - when=( - tz.localize(datetime(start.year, start.month, start.day, 11, 0)), - tz.localize(datetime(start.year, start.month, start.day, 18, 0)), - ), - ) - EventSession.objects.create( - camp=camp, - event_type=event_types["recreational"], - event_location=event_locations["speakers_tent"], - event_duration_minutes=60, - when=( - tz.localize(datetime(start.year, start.month, start.day, 12, 0)), - tz.localize(datetime(start.year, start.month, start.day, 13, 0)), - ), - ) - EventSession.objects.create( - camp=camp, - event_type=event_types["music"], - event_location=event_locations["bar_area"], - when=( - tz.localize(datetime(start.year, start.month, start.day, 22, 0)), - tz.localize(datetime(start.year, start.month, start.day, 22, 0)) + timedelta(hours=3), - ), - ) - EventSession.objects.create( - camp=camp, - event_type=event_types["workshop"], - event_location=event_locations["workshop_room_1"], - when=( - tz.localize(datetime(start.year, start.month, start.day, 12, 0)), - tz.localize(datetime(start.year, start.month, start.day, 18, 0)), - ), - ) - EventSession.objects.create( - camp=camp, - event_type=event_types["workshop"], - event_location=event_locations["workshop_room_2"], - when=( - tz.localize(datetime(start.year, start.month, start.day, 12, 0)), - tz.localize(datetime(start.year, start.month, start.day, 18, 0)), - ), - ) - EventSession.objects.create( - camp=camp, - event_type=event_types["workshop"], - event_location=event_locations["workshop_room_3"], - when=( - tz.localize(datetime(start.year, start.month, start.day, 12, 0)), - tz.localize(datetime(start.year, start.month, start.day, 18, 0)), - ), - ) - # 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"], - when=( - tz.localize( - datetime(day.lower.year, day.lower.month, day.lower.day, 20, 0), - ), - tz.localize( - datetime( - day.lower.year, - day.lower.month, - day.lower.day, - 21, - 30, - ), - ), - ), - ) - - def create_camp_proposals(self, camp, event_types) -> None: - year = camp.camp.lower.year - self.output(f"Creating event- and speaker_proposals for {year}...") - - # add 45 talks - talkproposals = EventProposalFactory.create_batch( - 45, - track=factory.Iterator(camp.event_tracks.all()), - event_type=event_types["talk"], - ) - # and 15 workshops - workshopproposals = EventProposalFactory.create_batch( - 15, - track=factory.Iterator(camp.event_tracks.all()), - event_type=event_types["workshop"], - ) - # and 3 keynotes - # (in the real world these are submitted as talks - # and promoted to keynotes by the content team) - keynoteproposals = EventProposalFactory.create_batch( - 3, - track=factory.Iterator(camp.event_tracks.all()), - event_type=event_types["keynote"], - ) - - tags = [ - "infosec", - "hardware", - "politics", - "django", - "development", - "games", - "privacy", - "vampires", - "linux", - ] - - for ep in talkproposals + workshopproposals + keynoteproposals: - # create a speakerproposal for this EventProposal - sp = SpeakerProposalFactory(camp=camp, user=ep.user) - ep.speakers.add(sp) - # 20% chance we add an extra speaker - if random.randint(1, 10) > 8: - other_speakers = SpeakerProposal.objects.filter(camp=camp).exclude( - uuid=sp.uuid, - ) - # ... if we have any... - if other_speakers.exists(): - # add an extra speaker - ep.speakers.add(random.choice(other_speakers)) - - # add tags for 2 out of 3 events - if random.choice([True, True, False]): - # add 1-3 tags for this EP - ep.tags.add(*random.sample(tags, k=random.randint(1, 3))) - - EventProposal.objects.create( - user=random.choice(User.objects.all()), - title="Lunch break", - abstract="Daily lunch break. Remember to drink water.", - event_type=event_types["recreational"], - track=random.choice(camp.event_tracks.all()), - ).mark_as_approved() - - def create_proposal_urls(self, camp) -> None: - """Create URL objects for the proposals.""" - year = camp.camp.lower.year - self.output( - f"Creating URLs for Speaker- and EventProposals for {year}...", - ) - SpeakerProposalUrlFactory.create_batch( - 100, - speaker_proposal=factory.Iterator( - SpeakerProposal.objects.filter(camp=camp), - ), - ) - EventProposalUrlFactory.create_batch( - 100, - event_proposal=factory.Iterator( - EventProposal.objects.filter(track__camp=camp), - ), - ) - - def generate_speaker_availability(self, camp) -> None: - """Create SpeakerAvailability objects for the SpeakerProposals.""" - year = camp.camp.lower.year - self.output( - f"Generating random SpeakerProposalAvailability for {year}...", - ) - for sp in camp.speaker_proposals.all(): - # generate a matrix for this speaker_proposals event_types - matrix = get_speaker_availability_form_matrix( - sessions=sp.camp.event_sessions.filter( - event_type__in=sp.event_types.all(), - ), - ) - - # build a "form" object so we can reuse save_speaker_availability() - class FakeForm: - cleaned_data = {} - - form = FakeForm() - for daychunks in matrix.values(): - # 90% chance we have info for any given day - if random.randint(1, 100) > 90: - # no availability info for this entire day, sorry - continue - for data in daychunks.values(): - if not data: - continue - # 90% chance this speaker is available for any given chunk - form.cleaned_data[data["fieldname"]] = random.randint(1, 100) < 90 - # print(f"saving availability for speaker {sp}: {form.cleaned_data}") - save_speaker_availability(form, sp) - - def approve_speaker_proposals(self, camp) -> None: - """Approve all keynotes but reject 10% of other events.""" - for sp in camp.speaker_proposals.filter( - event_proposals__event_type__name="Keynote", - ): - sp.mark_as_approved() - - for sp in camp.speaker_proposals.filter(proposal_status="pending"): - # we do not approve all speakers - x = random.randint(1, 100) - if x < 90: - sp.mark_as_approved() - elif x < 95: - # leave this as pending - continue - else: - sp.mark_as_rejected() - - def approve_event_proposals(self, camp) -> None: - for ep in camp.event_proposals.filter(proposal_status="pending"): - # are all speakers for this event approved? - for sp in ep.speakers.all(): - if not hasattr(sp, "speaker"): - break - else: - # all speakers are approved, approve the event? always approve keynotes! - if random.randint(1, 100) < 90 or ep.event_type.name == "Keynote": - ep.mark_as_approved() - else: - ep.mark_as_rejected() - - # set demand for workshops to see the autoscheduler in action - for event in camp.events.filter(event_type__name="Workshop"): - # this should put about half the workshops in the big room - # (since the small rooms have max. 25 ppl capacity) - event.demand = random.randint(10, 40) - event.save() - - def create_camp_scheduling(self, camp, autoschedule) -> None: - year = camp.camp.lower.year - self.output(f"Creating scheduling for {year}...") - - # create a lunchbreak daily in speakers tent - lunch = Event.objects.get(track__camp=camp, title="Lunch break") - for day in camp.get_days(camppart="camp")[1:-1]: - date = day.lower.date() - start = tz.localize(datetime(date.year, date.month, date.day, 12, 0)) - lunchslot = EventSlot.objects.get( - event_session__event_location=camp.event_locations.get( - name="Speakers Tent", - ), - event_session__event_type=EventType.objects.get( - name="Recreational Event", - ), - when=(start, start + timedelta(hours=1)), - ) - lunchslot.event = lunch - lunchslot.autoscheduled = False - lunchslot.save() - - # exercise the autoscheduler a bit - if autoschedule: - scheduler = AutoScheduler(camp=camp) - schedulestart = timezone.now() - try: - autoschedule = scheduler.calculate_autoschedule() - if autoschedule: - scheduler.apply(autoschedule) - except ValueError as E: - self.output(f"Got exception while calculating autoschedule: {E}") - scheduleduration = timezone.now() - schedulestart - self.output( - f"Done running autoscheduler for {year}... It took {scheduleduration}", - ) - - def create_camp_speaker_event_conflicts(self, camp) -> None: - year = camp.camp.lower.year - self.output( - f"Generating event_conflicts for SpeakerProposals for {year}...", - ) - # loop over all - for sp in camp.speaker_proposals.all(): - # not all speakers add conflicts - if random.choice([True, True, False]): - # pick 0-10 events this speaker wants to attend - conflictcount = random.randint(0, 10) - sp.event_conflicts.set( - Event.objects.filter( - track__camp=camp, - event_type__support_speaker_event_conflicts=True, - ).order_by("?")[0:conflictcount], - ) - - def create_camp_rescheduling(self, camp, autoschedule) -> None: - year = camp.camp.lower.year - # reapprove all speaker_proposals so the new availability takes effect - for prop in camp.speaker_proposals.filter(proposal_status="approved"): - prop.mark_as_approved() - # exercise the autoscheduler a bit - self.output(f"Rescheduling {year}...") - if autoschedule: - scheduler = AutoScheduler(camp=camp) - schedulestart = timezone.now() - try: - autoschedule, diff = scheduler.calculate_similar_autoschedule() - scheduler.apply(autoschedule) - except ValueError as E: - self.output( - f"Got exception while calculating similar autoschedule: {E}", - ) - autoschedule = None - scheduleduration = timezone.now() - schedulestart - self.output(f"Done rescheduling for {year}... It took {scheduleduration}.") - - def create_camp_villages(self, camp, users) -> None: - year = camp.camp.lower.year - self.output(f"Creating villages for {year}...") - Village.objects.create( - contact=users[1], - camp=camp, - name="Baconsvin", - slug="baconsvin", - 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", - 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", - ) - - def create_camp_teams(self, camp): - teams = {} - year = camp.camp.lower.year - self.output(f"Creating teams for {year}...") - teams["orga"] = Team.objects.create( - name="Orga", - description="The Orga team are the main organisers. All tasks are Orga responsibility until they are delegated to another team", - camp=camp, - needs_members=False, - ) - teams["info"] = Team.objects.create( - name="Info", - description="Info team manage the info pages and the info desk.", - camp=camp, - ) - teams["poc"] = Team.objects.create( - name="POC", - description="The POC team is in charge of establishing and running a phone network onsite.", - camp=camp, - ) - teams["noc"] = Team.objects.create( - name="NOC", - description="The NOC team is in charge of establishing and running a network onsite.", - camp=camp, - ) - teams["gis"] = Team.objects.create( - name="GIS", - description="The GIS team is in charge of managing the gis data.", - camp=camp, - ) - teams["bar"] = Team.objects.create( - name="Bar", - description="The Bar team plans, builds and run the IRL bar!", - camp=camp, - ) - teams["shuttle"] = Team.objects.create( - name="Shuttle", - description="The shuttle team drives people to and from the trainstation or the supermarket", - camp=camp, - ) - teams["power"] = Team.objects.create( - name="Power", - description="The power team makes sure we have power all over the venue", - camp=camp, - ) - teams["shit"] = Team.objects.create( - name="Sanitation", - description="Team shit takes care of the toilets", - camp=camp, - ) - teams["content"] = Team.objects.create( - name="Content", - description="The Content Team handles stuff on the program", - camp=camp, - mailing_list="content@example.com", - ) - teams["economy"] = Team.objects.create( - name="Economy", - description="The Economy Team handles the money and accounts.", - camp=camp, - mailing_list="economy@example.com", - ) - camp.economy_team = teams["economy"] - camp.save() - return teams - - def create_camp_team_tasks(self, camp, teams) -> None: - year = camp.camp.lower.year - self.output(f"Creating TeamTasks for {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", - ) - - def create_camp_team_memberships(self, camp, teams, users): - memberships = {} - year = camp.camp.lower.year - self.output(f"Creating team memberships for {year}...") - # noc team - memberships["noc"] = {} - memberships["noc"]["user4"] = TeamMember.objects.create( - team=teams["noc"], - user=users[4], - approved=True, - lead=True, - ) - memberships["noc"]["user1"] = TeamMember.objects.create( - team=teams["noc"], - user=users[1], - approved=True, - ) - memberships["noc"]["user5"] = TeamMember.objects.create( - team=teams["noc"], - user=users[5], - approved=True, - ) - memberships["noc"]["user2"] = TeamMember.objects.create( - 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"]["user2"] = TeamMember.objects.create( - team=teams["bar"], - user=users[2], - approved=True, - ) - memberships["bar"]["user7"] = TeamMember.objects.create( - team=teams["bar"], - user=users[7], - approved=True, - ) - memberships["bar"]["user8"] = TeamMember.objects.create( - 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"]["user9"] = TeamMember.objects.create( - 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, - ) - - # 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"]["user9"] = TeamMember.objects.create( - 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, - ) - - # gis team also gets a member - TeamMember.objects.create( - team=teams["gis"], - user=users[0], - lead=True, - approved=True, - ) - return memberships - - def create_camp_team_shifts(self, camp, teams, team_memberships) -> None: - shifts = {} - year = camp.camp.lower.year - self.output(f"Creating team shifts for {year}...") - shifts[0] = TeamShift.objects.create( - team=teams["shuttle"], - shift_range=( - tz.localize(datetime(year, 8, 27, 2, 0)), - tz.localize(datetime(year, 8, 27, 8, 0)), - ), - people_required=1, - ) - shifts[0].team_members.add(team_memberships["shuttle"]["user7"]) - shifts[1] = TeamShift.objects.create( - team=teams["shuttle"], - shift_range=( - tz.localize(datetime(year, 8, 27, 8, 0)), - tz.localize(datetime(year, 8, 27, 14, 0)), - ), - people_required=1, - ) - shifts[2] = TeamShift.objects.create( - team=teams["shuttle"], - shift_range=( - tz.localize(datetime(year, 8, 27, 14, 0)), - tz.localize(datetime(year, 8, 27, 20, 0)), - ), - people_required=1, - ) - - def create_camp_info_categories(self, camp, teams): - categories = {} - year = camp.camp.lower.year - self.output(f"Creating infocategories for {year}...") - categories["when"] = InfoCategory.objects.create( - team=teams["orga"], - headline="When is BornHack happening?", - anchor="when", - ) - categories["travel"] = InfoCategory.objects.create( - team=teams["orga"], - headline="Travel Information", - anchor="travel", - ) - categories["sleep"] = InfoCategory.objects.create( - team=teams["orga"], - headline="Where do I sleep?", - anchor="sleep", - ) - - return categories - - def create_camp_info_items(self, camp, categories) -> None: - year = camp.camp.lower.year - self.output(f"Creating infoitems for {year}...") - InfoItem.objects.create( - category=categories["when"], - headline="Opening", - anchor="opening", - body=f"BornHack {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 {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.", - ) - - def create_camp_feedback(self, camp, users) -> None: - year = camp.camp.lower.year - self.output(f"Creating feedback for {year}...") - Feedback.objects.create( - camp=camp, - user=users[1], - feedback="Awesome event, will be back next year", - ) - Feedback.objects.create( - camp=camp, - user=users[3], - feedback="Very nice, though a bit more hot water would be awesome", - ) - Feedback.objects.create( - camp=camp, - user=users[5], - feedback="Is there a token here?", - ) - Feedback.objects.create( - camp=camp, - user=users[9], - feedback="That was fun. Thanks!", - ) - - def create_camp_rides(self, camp, users) -> None: - year = camp.camp.lower.year - self.output(f"Creating rides for {year}...") - Ride.objects.create( - camp=camp, - user=users[1], - seats=2, - from_location="Copenhagen", - to_location="BornHack", - when=tz.localize(datetime(year, 8, 27, 12, 0)), - 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=tz.localize(datetime(year, 9, 4, 12, 0)), - 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=tz.localize(datetime(year, 8, 27, 12, 0)), - description="I need a ride and have a large backpack", - ) - - def create_camp_cfp(self, camp) -> None: - year = camp.camp.lower.year - self.output(f"Creating CFP for {year}...") - camp.call_for_participation_open = True - camp.call_for_participation = f"Please give a talk at Bornhack {year}..." - camp.save() - - def create_camp_cfs(self, camp) -> None: - year = camp.camp.lower.year - self.output(f"Creating CFS for {year}...") - camp.call_for_sponsors_open = True - camp.call_for_sponsors = f"Please give us ALL the money so that we can make Bornhack {year} the best ever!" - camp.save() - - def create_camp_sponsor_tiers(self, camp): - tiers = {} - year = camp.camp.lower.year - self.output(f"Creating sponsor tiers for {year}...") - tiers["platinum"] = SponsorTier.objects.create( - 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- promoted HackMe\n- sponsored social event", - camp=camp, - weight=0, - week_tickets=10, - ) - tiers["gold"] = SponsorTier.objects.create( - 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", - camp=camp, - weight=1, - week_tickets=10, - ) - tiers["silver"] = SponsorTier.objects.create( - 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", - camp=camp, - weight=2, - week_tickets=5, - ) - tiers["sponsor"] = SponsorTier.objects.create( - 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", - camp=camp, - weight=3, - week_tickets=2, - ) - - return tiers - - def create_camp_sponsors(self, camp, tiers) -> None: - year = camp.camp.lower.year - self.output(f"Creating sponsors for {year}...") - Sponsor.objects.create( - name="PROSA", - tier=tiers["platinum"], - description="Bus Trip", - logo_filename="PROSA-logo.png", - url="https://www.prosa.dk", - ) - Sponsor.objects.create( - name="DKUUG", - tier=tiers["platinum"], - description="Speakers tent", - logo_filename="DKUUGlogo.jpeg", - url="http://www.dkuug.dk/", - ) - Sponsor.objects.create( - name="LetsGo", - tier=tiers["silver"], - description="Shuttle", - logo_filename="letsgo.png", - url="https://letsgo.dk", - ) - Sponsor.objects.create( - name="Saxo Bank", - tier=tiers["gold"], - description="Cash Sponsorship", - logo_filename="saxobank.png", - url="https://home.saxo", - ) - Sponsor.objects.create( - name="CSIS", - tier=tiers["sponsor"], - description="Cash Sponsorship", - logo_filename="CSIS_PRI_LOGO_TURQUOISE_RGB.jpg", - url="https://csis.dk", - ) - - def create_camp_tokens(self, camp): - tokens = {} - year = camp.camp.lower.year - self.output(f"Creating tokens for {year}...") - tokens[0] = Token.objects.create( - camp=camp, - token=get_random_string(length=32), - category="Physical", - description="Token in the back of the speakers tent (in binary)", - active=True, - ) - tokens[1] = Token.objects.create( - camp=camp, - token=get_random_string(length=32), - category="Internet", - description="Twitter", - active=True, - ) - tokens[2] = Token.objects.create( - camp=camp, - token=get_random_string(length=32), - category="Website", - description="Token hidden in the X-Secret-Token HTTP header on the BornHack website", - active=True, - ) - tokens[3] = Token.objects.create( - camp=camp, - token=get_random_string(length=32), - category="Physical", - description="Token in infodesk (QR code)", - active=True, - ) - tokens[4] = Token.objects.create( - camp=camp, - token=get_random_string(length=32), - category="Physical", - description=f"Token on the back of the BornHack {year} badge", - active=True, - ) - tokens[5] = Token.objects.create( - camp=camp, - token=get_random_string(length=32), - category="Website", - description="Token hidden in EXIF data in the logo posted on the website sunday", - active=True, - ) - - return tokens - - def create_camp_token_finds(self, camp, tokens, users) -> None: - year = camp.camp.lower.year - self.output(f"Creating token finds for {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]) - for i in range(6): - TokenFind.objects.create(token=tokens[i], user=users[1]) - - def create_camp_expenses(self, camp) -> None: - self.output(f"Creating expenses for {camp}...") - for team in Team.objects.filter(camp=camp): - ExpenseFactory.create_batch(10, camp=camp, responsible_team=team) - - def create_camp_reimbursements(self, camp) -> None: - self.output(f"Creating reimbursements for {camp}...") - users = User.objects.filter( - id__in=Expense.objects.filter( - camp=camp, - reimbursement__isnull=True, - paid_by_bornhack=False, - approved=True, - ) - .values_list("user", flat=True) - .distinct(), - ) - for user in users: - expenses = Expense.objects.filter( - user=user, - approved=True, - reimbursement__isnull=True, - paid_by_bornhack=False, - ) - reimbursement = Reimbursement.objects.create( - camp=camp, - user=user, - reimbursement_user=user, - bank_account=random.randint(1000000000, 100000000000), - notes=f"bootstrap created reimbursement for user {user.username}", - paid=random.choice([True, True, False]), - ) - expenses.update(reimbursement=reimbursement) - reimbursement.create_payback_expense() - - def create_camp_revenues(self, camp) -> None: - self.output(f"Creating revenues for {camp}...") - RevenueFactory.create_batch(20, camp=camp) - - def add_team_permissions(self, camp) -> None: - """Assign member permissions to the team groups for this camp.""" - self.output(f"Assigning permissions to team groups for {camp}...") - permission_content_type = ContentType.objects.get_for_model(CampPermission) - for team in camp.teams.all(): - permission = Permission.objects.get( - content_type=permission_content_type, - codename=f"{team.slug}_team_member", - ) - team.group.permissions.add(permission) - - def create_maps_layer_generic(self) -> None: - group = MapGroup.objects.create(name="Generic") - layer = Layer.objects.create( - name="Areas", - slug="areas", - description="Venue areas", - icon="fa fa-list-ul", - group=group, - ) - Feature.objects.create( - layer=layer, - name="Orga", - description="Orga Area", - geom=GeometryCollection( - Polygon( - [ - [9.941073, 55.388305], - [9.940768, 55.388103], - [9.941146, 55.38796], - [9.941149, 55.388035], - [9.94132, 55.388201], - [9.941073, 55.388305], - ], - ), - ), - color="#ff00ffff", - icon="fa fa-hand-paper", - url="", - topic="", - processing="", - ) - - def create_camp_map_layer(self, camp) -> None: - group = MapGroup.objects.get(name="Generic") - team = Team.objects.get(name="Orga", camp=camp) - layer = Layer.objects.create( - name="Team Area", - description="Team areas", - icon="fa fa-list-ul", - group=group, - responsible_team=team, - ) - Feature.objects.create( - layer=layer, - name="Team Area", - description="Some Team Area", - geom=GeometryCollection( - Polygon( - [ - [9.940803, 55.38785], - [9.941136, 55.387826], - [9.941297, 55.387662], - [9.940943, 55.38754], - [9.940535, 55.387521], - [9.940803, 55.38785], - ], - ), - ), - color="#ff00ffff", - icon="fa fa-list", - url="", - topic="", - processing="", - ) - def output(self, message) -> None: self.stdout.write( "{}: {}".format(timezone.now().strftime("%Y-%m-%d %H:%M:%S"), message), @@ -2178,186 +28,14 @@ def output(self, message) -> None: def handle(self, *args, **options) -> None: start = timezone.now() - self.output( - self.style.SUCCESS("----------[ Running bootstrap_devsite ]----------"), - ) self.output( self.style.SUCCESS( "----------[ Deleting all data from database ]----------", ), ) call_command("flush", "--noinput") - - self.output(self.style.SUCCESS("----------[ Global stuff ]----------")) - - camps = self.create_camps() - self.create_event_routing_types() - users = self.create_users() - - self.create_news() - - event_types = self.create_event_types() - - self.create_url_types() - - product_categories = self.create_product_categories() - - quickfeedback_options = self.create_quickfeedback_options() - - self.create_mobilepay_transactions() - - self.create_clearhaus_settlements() - - self.create_credebtors() - - self.create_bank_stuff() - - self.create_coinify_stuff() - - self.create_epay_transactions() - - self.create_maps_layer_generic() - - permissions_added = False - for camp, read_only in camps: - year = camp.camp.lower.year - - self.output( - self.style.SUCCESS(f"----------[ Bornhack {year} ]----------"), - ) - - if year <= settings.UPCOMING_CAMP_YEAR: - ticket_types = self.create_camp_ticket_types(camp) - - camp_products = self.create_camp_products( - camp, - product_categories, - ticket_types, - ) - - self.create_orders(users, camp_products) - - self.create_camp_tracks(camp) - - locations = self.create_event_locations(camp) - - self.create_camp_news(camp) - - teams = self.create_camp_teams(camp) - - 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_camp_team_tasks(camp, teams) - - team_memberships = self.create_camp_team_memberships(camp, teams, users) - - self.create_camp_team_shifts(camp, teams, team_memberships) - - self.create_camp_pos(teams) - - self.create_camp_cfp(camp) - - self.create_camp_proposals(camp, event_types) - - self.create_proposal_urls(camp) - - self.create_camp_event_sessions(camp, event_types, locations) - - self.generate_speaker_availability(camp) - - try: - self.approve_speaker_proposals(camp) - except ValidationError: - self.output( - "Name collision, bad luck. Run the bootstrap script again! PRs to make this less annoying welcome :)", - ) - sys.exit(1) - - self.approve_event_proposals(camp) - - self.create_camp_scheduling(camp, not options["skip_auto_scheduler"]) - - # shuffle it up - delete and create new random availability - self.generate_speaker_availability(camp) - - # and create some speaker<>event conflicts - self.create_camp_speaker_event_conflicts(camp) - - # recalculate the autoschedule - self.create_camp_rescheduling(camp, not options["skip_auto_scheduler"]) - - self.create_camp_villages(camp, users) - - facility_types = self.create_facility_types( - camp, - teams, - quickfeedback_options, - ) - - facilities = self.create_facilities(facility_types) - - self.create_facility_feedbacks(facilities, quickfeedback_options, users) - - info_categories = self.create_camp_info_categories(camp, teams) - - self.create_camp_info_items(camp, info_categories) - - self.create_camp_feedback(camp, users) - - self.create_camp_rides(camp, users) - - self.create_camp_cfs(camp) - - sponsor_tiers = self.create_camp_sponsor_tiers(camp) - - self.create_camp_sponsors(camp, sponsor_tiers) - - tokens = self.create_camp_tokens(camp) - - self.create_camp_token_finds(camp, tokens, users) - - self.create_camp_expenses(camp) - - self.create_camp_reimbursements(camp) - - self.create_camp_revenues(camp) - - self.create_camp_map_layer(camp) - else: - self.output("Not creating anything for this year yet") - - camp.read_only = read_only - camp.call_for_participation_open = not read_only - camp.call_for_sponsors_open = not read_only - camp.save() - - self.output("----------[ Finishing up ]----------") - - self.output("Adding event routing...") - Routing.objects.create( - team=teams["orga"], - eventtype=Type.objects.get(name="public_credit_name_changed"), - ) - Routing.objects.create( - team=teams["orga"], - eventtype=Type.objects.get(name="ticket_created"), - ) - - self.output("done!") + bootstrap = Bootstrap() + bootstrap.output = self.output + bootstrap.bootstrap_full(options) duration = timezone.now() - start self.output(f"bootstrap_devsite took {duration}!") - - def create_camp_pos(self, teams) -> None: - Pos.objects.create( - name="Infodesk", - team=teams["info"], - external_id="HHR9izotB6HLzgT6k", - ) - Pos.objects.create( - name="Bar", - team=teams["bar"], - external_id="bTasxE2YYXZh35wtQ", - ) diff --git a/src/utils/tests.py b/src/utils/tests.py index ee8afc16c..16d177579 100644 --- a/src/utils/tests.py +++ b/src/utils/tests.py @@ -3,12 +3,8 @@ from __future__ import annotations import logging -from datetime import datetime from unittest import skip -import pytz -from django.contrib.auth.models import Group -from django.contrib.auth.models import User from django.core.management import call_command from django.test import Client from django.test import TestCase @@ -16,6 +12,7 @@ from camps.models import Camp from teams.models import Team +from utils.bootstrap.base import Bootstrap class TestBootstrapScript(TestCase): """Test bootstrap_devsite script (touching many codepaths)""" @@ -29,9 +26,10 @@ def test_bootstrap_script(self): class BornhackTestBase(TestCase): """Bornhack base TestCase.""" - users: list[User] + users: dict camp: Camp team: Team + bootstrap: Bootstrap @classmethod def setUpTestData(cls) -> None: @@ -41,49 +39,8 @@ def setUpTestData(cls) -> None: cls.client = Client(enforce_csrf_checks=False) - tz = pytz.timezone("Europe/Copenhagen") - year = datetime.now(tz).year - cls.camp = Camp( - title="Test Camp", - slug="test-camp", - tagline="Such test much wow", - shortslug="test-camp", - buildup=( - datetime(year, 8, 25, 12, 0, tzinfo=tz), - datetime(year, 8, 27, 12, 0, tzinfo=tz), - ), - camp=( - datetime(year, 8, 27, 12, 0, tzinfo=tz), - datetime(year, 9, 3, 12, 0, tzinfo=tz), - ), - teardown=( - datetime(year, 9, 3, 12, 0, tzinfo=tz), - datetime(year, 9, 5, 12, 0, tzinfo=tz), - ), - colour="#ffffff", - light_text=False, - ) - cls.camp.save() - - cls.users = [] - user = User.objects.create_user( - username="user0", - email="user0@example.com", - ) - user.set_password("user0") - user.save() - cls.users.append(user) - - # Create a team - team_group = Group(name="Test Team Group") - team_group.save() - cls.team = Team( - camp=cls.camp, - name="Test Team", - group=team_group, - slug="test", - shortslug="test", - description="Many test Such Team", - needs_members=True, - ) - cls.team.save() + cls.bootstrap = Bootstrap() + cls.bootstrap.bootstrap_tests() + cls.camp = cls.bootstrap.camp + cls.users = cls.bootstrap.users + cls.teams = cls.bootstrap.teams From 0dc8ab0d9be9a3c67ee2279b98233a8d485ed044 Mon Sep 17 00:00:00 2001 From: "Rudy (zarya)" Date: Fri, 16 May 2025 22:35:36 +0200 Subject: [PATCH 03/10] WIP: Linting --- src/utils/bootstrap/base.py | 476 ++++++++++++++++++------------- src/utils/bootstrap/factories.py | 57 +++- src/utils/bootstrap/functions.py | 11 +- 3 files changed, 337 insertions(+), 207 deletions(-) diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py index 1e8c5ad3e..82b7c8a4f 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -1,3 +1,5 @@ +"""Base class for bootstrapping the application.""" + from __future__ import annotations import logging @@ -82,34 +84,36 @@ from tokens.models import TokenFind from utils.slugs import unique_slugify from villages.models import Village -from .functions import output_fake_md_description - from .factories import CredebtorFactory -from .factories import ExpenseFactory -from .factories import RevenueFactory -from .factories import UserFactory from .factories import EmailAddressFactory -from .factories import SpeakerProposalFactory from .factories import EventProposalFactory from .factories import EventProposalUrlFactory +from .factories import ExpenseFactory +from .factories import RevenueFactory +from .factories import SpeakerProposalFactory from .factories import SpeakerProposalUrlFactory +from .factories import UserFactory +from .functions import output_fake_md_description fake = Faker() -# Faker.seed(0) tz = pytz.timezone("Europe/Copenhagen") logger = logging.getLogger(f"bornhack.{__name__}") - -class Bootstrap(): +class Bootstrap: + """Main bootstrap class.""" camps: list[Camp] camp: Camp users: dict teams: dict + event_types: dict + product_categories: dict + quickfeedback_options: dict - def create_camps(self, camps): + def create_camps(self, camps: dict) -> None: + """Creates all camps from a dict of camps.""" self.output("Creating camps...") camp_instances = [] @@ -125,16 +129,16 @@ def create_camps(self, camps): slug=f"bornhack-{year}", shortslug=f"bornhack-{year}", buildup=( - tz.localize(datetime(year, 8, 25, 12, 0)), - tz.localize(datetime(year, 8, 27, 12, 0)), + datetime(year, 8, 25, 12, 0, tzinfo=tz), + datetime(year, 8, 27, 12, 0, tzinfo=tz), ), camp=( - tz.localize(datetime(year, 8, 27, 12, 0)), - tz.localize(datetime(year, 9, 3, 12, 0)), + datetime(year, 8, 27, 12, 0, tzinfo=tz), + datetime(year, 9, 3, 12, 0, tzinfo=tz), ), teardown=( - tz.localize(datetime(year, 9, 3, 12, 0)), - tz.localize(datetime(year, 9, 5, 12, 0)), + 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), @@ -146,10 +150,12 @@ def create_camps(self, camps): self.camps = camp_instances def create_event_routing_types(self) -> None: + """Create event routing types.""" t, created = Type.objects.get_or_create(name="public_credit_name_changed") t, created = Type.objects.get_or_create(name="ticket_created") - def create_users(self, amount: int): + def create_users(self, amount: int) -> None: + """Create users.""" self.output("Creating users...") users = {} @@ -187,15 +193,17 @@ def create_users(self, amount: int): primary=True, ) - self.users = users + self.users = users def create_news(self) -> None: + """Create fake news.""" NewsItem.objects.create( title="unpublished news item", content="unpublished news body here", ) - def create_quickfeedback_options(self): + def create_quickfeedback_options(self) -> None: + """Create quick feedback options.""" options = {} self.output("Creating quickfeedback options") options["na"] = FacilityQuickFeedback.objects.create( @@ -217,28 +225,33 @@ def create_quickfeedback_options(self): feedback="No power", icon="fas fa-bolt", ) - return options + self.quickfeedback_options = options def create_mobilepay_transactions(self) -> None: + """Create MobilePay Transactions.""" self.output("Creating MobilePay Transactions...") MobilePayTransactionFactory.create_batch(50) def create_clearhaus_settlements(self) -> None: + """Create Clearhaus Settlements.""" self.output("Creating Clearhaus Settlements...") ClearhausSettlementFactory.create_batch(50) def create_zettle_stuff(self) -> None: + """Create Zettle receipts and balances.""" self.output("Creating Zettle receipts and balances...") ZettleBalanceFactory.create_batch(100) ZettleReceiptFactory.create_batch(100) def create_bank_stuff(self) -> None: + """Create Banks, BankAccounts, and BankTransactions.""" self.output("Creating Banks, BankAccounts, and BankTransactions...") BankFactory.create_batch(2) BankAccountFactory.create_batch(16) BankTransactionFactory.create_batch(300) def create_coinify_stuff(self) -> None: + """Create coinify invoices, payment intents, payouts, settlements and balances.""" self.output( "Creating Coinify invoices, payment intents, payouts, settlements and balances...", ) @@ -249,10 +262,12 @@ def create_coinify_stuff(self) -> None: CoinifySettlementFactory.create_batch(10) def create_epay_transactions(self) -> None: + """Create epay transactions.""" self.output("Creating ePay Transactions...") EpayTransactionFactory.create_batch(50) - def create_facility_types(self, camp, teams, options): + def create_facility_types(self, teams: dict, options: dict) -> dict: + """Create facility types.""" types = {} self.output("Creating facility types...") types["toilet"] = FacilityType.objects.create( @@ -278,7 +293,8 @@ def create_facility_types(self, camp, teams, options): types["power"].quickfeedback_options.add(options["power"]) return types - def create_facilities(self, facility_types): + def create_facilities(self, facility_types: dict) -> dict: + """Create facilities.""" facilities = {} self.output("Creating facilities...") facilities["toilet1"] = Facility.objects.create( @@ -319,7 +335,8 @@ def create_facilities(self, facility_types): ) return facilities - def create_facility_feedbacks(self, facilities, options, users) -> None: + def create_facility_feedbacks(self, facilities: dict, options: dict, users: dict) -> None: + """Create facility feedbacks.""" self.output("Creating facility feedbacks...") FacilityFeedback.objects.create( user=users[1], @@ -354,7 +371,8 @@ def create_facility_feedbacks(self, facilities, options, users) -> None: urgent=True, ) - def create_event_types(self): + def create_event_types(self) -> None: + """Create event types.""" types = {} self.output("Creating event types...") types["workshop"] = EventType.objects.create( @@ -466,9 +484,10 @@ def create_event_types(self): support_speaker_event_conflicts=True, ) - return types + self.event_types = types def create_url_types(self) -> None: + """Create UrlType objects.""" self.output("Creating UrlType objects...") t, created = UrlType.objects.get_or_create( name="Other", @@ -516,6 +535,7 @@ def create_url_types(self) -> None: ) def create_credebtors(self) -> None: + """Create Chain and Credebtors.""" self.output("Creating Chains and Credebtors...") try: CredebtorFactory.create_batch(50) @@ -540,7 +560,8 @@ def create_credebtors(self) -> None: address="Nowhere", ) - def create_product_categories(self): + def create_product_categories(self) -> None: + """Create product categories.""" categories = {} self.output("Creating productcategories...") categories["transportation"] = ProductCategory.objects.create( @@ -568,9 +589,10 @@ def create_product_categories(self): slug="packages", ) - return categories + self.product_categories = categories - def create_camp_ticket_types(self, camp): + def create_camp_ticket_types(self, camp: Camp) -> dict: + """Create camp ticket types.""" types = {} self.output(f"Creating tickettypes for {camp.camp.lower.year}...") types["adult_full_week"] = TicketType.objects.create( @@ -609,7 +631,8 @@ def create_camp_ticket_types(self, camp): return types - def create_camp_products(self, camp, categories, ticket_types): + def create_camp_products(self, camp: Camp, categories: dict, ticket_types: dict) -> dict: + """Create camp shop products.""" products = {} year = camp.camp.lower.year camp_prefix = f"BornHack {year}" @@ -621,8 +644,8 @@ def create_camp_products(self, camp, categories, ticket_types): price=1200, category=categories["tickets"], available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), + datetime(year, 1, 1, 12, 0, tzinfo=tz), + datetime(year, 12, 20, 12, 0, tzinfo=tz), ), slug=unique_slugify( name, @@ -640,8 +663,8 @@ def create_camp_products(self, camp, categories, ticket_types): price=1337, category=categories["tickets"], available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), + datetime(year, 1, 1, 12, 0, tzinfo=tz), + datetime(year, 12, 20, 12, 0, tzinfo=tz), ), slug=unique_slugify( name, @@ -659,8 +682,8 @@ def create_camp_products(self, camp, categories, ticket_types): price=300, category=categories["tickets"], available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), + datetime(year, 1, 1, 12, 0, tzinfo=tz), + datetime(year, 12, 20, 12, 0, tzinfo=tz), ), slug=unique_slugify( name, @@ -678,8 +701,8 @@ def create_camp_products(self, camp, categories, ticket_types): price=3325, category=categories["villages"], available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), + datetime(year, 1, 1, 12, 0, tzinfo=tz), + datetime(year, 12, 20, 12, 0, tzinfo=tz), ), slug=unique_slugify( name, @@ -697,8 +720,8 @@ def create_camp_products(self, camp, categories, ticket_types): price=3675, category=categories["villages"], available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), + datetime(year, 1, 1, 12, 0, tzinfo=tz), + datetime(year, 12, 20, 12, 0, tzinfo=tz), ), slug=unique_slugify( name, @@ -716,8 +739,8 @@ def create_camp_products(self, camp, categories, ticket_types): price=150, category=categories["merchandise"], available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), + datetime(year, 1, 1, 12, 0, tzinfo=tz), + datetime(year, 12, 20, 12, 0, tzinfo=tz), ), slug=unique_slugify( name, @@ -735,8 +758,8 @@ def create_camp_products(self, camp, categories, ticket_types): price=150, category=categories["merchandise"], available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), + datetime(year, 1, 1, 12, 0, tzinfo=tz), + datetime(year, 12, 20, 12, 0, tzinfo=tz), ), slug=unique_slugify( name, @@ -754,8 +777,8 @@ def create_camp_products(self, camp, categories, ticket_types): price=150, category=categories["merchandise"], available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), + datetime(year, 1, 1, 12, 0, tzinfo=tz), + datetime(year, 12, 20, 12, 0, tzinfo=tz), ), slug=unique_slugify( name, @@ -773,8 +796,8 @@ def create_camp_products(self, camp, categories, ticket_types): price=100, category=categories["facilities"], available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), + datetime(year, 1, 1, 12, 0, tzinfo=tz), + datetime(year, 12, 20, 12, 0, tzinfo=tz), ), slug=unique_slugify( name, @@ -792,8 +815,8 @@ def create_camp_products(self, camp, categories, ticket_types): price=18000, category=categories["packages"], available_in=( - tz.localize(datetime(year, 1, 1, 12, 0)), - tz.localize(datetime(year, 12, 20, 12, 0)), + datetime(year, 1, 1, 12, 0, tzinfo=tz), + datetime(year, 12, 20, 12, 0, tzinfo=tz), ), slug=unique_slugify( name, @@ -829,7 +852,8 @@ def create_camp_products(self, camp, categories, ticket_types): return products - def create_orders(self, users, camp_products): + def create_orders(self, users: dict, camp_products: dict) -> dict: + """Create camp orders for products in the shop.""" orders = {} self.output("Creating orders...") orders[0] = Order.objects.create( @@ -877,7 +901,8 @@ def create_orders(self, users, camp_products): return orders - def create_camp_tracks(self, camp): + def create_camp_tracks(self, camp: Camp) -> dict: + """Create camp event tracks.""" tracks = {} year = camp.camp.lower.year self.output(f"Creating event_tracks for {year}...") @@ -889,7 +914,8 @@ def create_camp_tracks(self, camp): return tracks - def create_event_locations(self, camp): + def create_event_locations(self, camp: Camp) -> dict: + """Create all event locations.""" locations = {} year = camp.camp.lower.year self.output(f"Creating event_locations for {year}...") @@ -950,21 +976,23 @@ def create_event_locations(self, camp): return locations - def create_camp_news(self, camp) -> None: + def create_camp_news(self, camp: Camp) -> None: + """Create camp news.""" year = camp.camp.lower.year self.output(f"Creating news for {year}...") NewsItem.objects.create( title=f"Welcome to {camp.title}", content="news body here with html support", - published_at=tz.localize(datetime(year, 8, 27, 12, 0)), + published_at=datetime(year, 8, 27, 12, 0, tzinfo=tz), ) NewsItem.objects.create( title=f"{camp.title} is over", content="news body here", - published_at=tz.localize(datetime(year, 9, 4, 12, 0)), + published_at=datetime(year, 9, 4, 12, 0, tzinfo=tz), ) - def create_camp_event_sessions(self, camp, event_types, event_locations) -> None: + def create_camp_event_sessions(self, camp: Camp, event_types: dict, event_locations: dict) -> None: + """Create camp event sessions.""" self.output(f"Creating EventSessions for {camp}...") days = camp.get_days(camppart="camp")[1:-1] for day in days: @@ -974,8 +1002,8 @@ def create_camp_event_sessions(self, camp, event_types, event_locations) -> None event_type=event_types["talk"], event_location=event_locations["speakers_tent"], when=( - tz.localize(datetime(start.year, start.month, start.day, 11, 0)), - tz.localize(datetime(start.year, start.month, start.day, 18, 0)), + datetime(start.year, start.month, start.day, 11, 0, tzinfo=tz), + datetime(start.year, start.month, start.day, 18, 0, tzinfo=tz), ), ) EventSession.objects.create( @@ -984,8 +1012,8 @@ def create_camp_event_sessions(self, camp, event_types, event_locations) -> None event_location=event_locations["speakers_tent"], event_duration_minutes=60, when=( - tz.localize(datetime(start.year, start.month, start.day, 12, 0)), - tz.localize(datetime(start.year, start.month, start.day, 13, 0)), + datetime(start.year, start.month, start.day, 12, 0, tzinfo=tz), + datetime(start.year, start.month, start.day, 13, 0, tzinfo=tz), ), ) EventSession.objects.create( @@ -993,8 +1021,8 @@ def create_camp_event_sessions(self, camp, event_types, event_locations) -> None event_type=event_types["music"], event_location=event_locations["bar_area"], when=( - tz.localize(datetime(start.year, start.month, start.day, 22, 0)), - tz.localize(datetime(start.year, start.month, start.day, 22, 0)) + timedelta(hours=3), + 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), ), ) EventSession.objects.create( @@ -1002,8 +1030,8 @@ def create_camp_event_sessions(self, camp, event_types, event_locations) -> None event_type=event_types["workshop"], event_location=event_locations["workshop_room_1"], when=( - tz.localize(datetime(start.year, start.month, start.day, 12, 0)), - tz.localize(datetime(start.year, start.month, start.day, 18, 0)), + 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( @@ -1011,8 +1039,8 @@ def create_camp_event_sessions(self, camp, event_types, event_locations) -> None event_type=event_types["workshop"], event_location=event_locations["workshop_room_2"], when=( - tz.localize(datetime(start.year, start.month, start.day, 12, 0)), - tz.localize(datetime(start.year, start.month, start.day, 18, 0)), + 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( @@ -1020,8 +1048,8 @@ def create_camp_event_sessions(self, camp, event_types, event_locations) -> None event_type=event_types["workshop"], event_location=event_locations["workshop_room_3"], when=( - tz.localize(datetime(start.year, start.month, start.day, 12, 0)), - tz.localize(datetime(start.year, start.month, start.day, 18, 0)), + 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 @@ -1031,22 +1059,20 @@ def create_camp_event_sessions(self, camp, event_types, event_locations) -> None event_type=event_types["keynote"], event_location=event_locations["speakers_tent"], when=( - tz.localize( - datetime(day.lower.year, day.lower.month, day.lower.day, 20, 0), - ), - tz.localize( - datetime( - day.lower.year, - day.lower.month, - day.lower.day, - 21, - 30, - ), + datetime(day.lower.year, day.lower.month, day.lower.day, 20, 0, tzinfo=tz), + datetime( + day.lower.year, + day.lower.month, + day.lower.day, + 21, + 30, + tzinfo=tz, ), ), ) - def create_camp_proposals(self, camp, event_types) -> None: + def create_camp_proposals(self, camp: Camp, event_types: dict) -> None: + """Create camp proposals: talks, workshops and keynotes.""" year = camp.camp.lower.year self.output(f"Creating event- and speaker_proposals for {year}...") @@ -1088,29 +1114,29 @@ def create_camp_proposals(self, camp, event_types) -> None: sp = SpeakerProposalFactory(camp=camp, user=ep.user) ep.speakers.add(sp) # 20% chance we add an extra speaker - if random.randint(1, 10) > 8: + if random.randint(1, 10) > 8: # noqa: PLR2004, S311 other_speakers = SpeakerProposal.objects.filter(camp=camp).exclude( uuid=sp.uuid, ) # ... if we have any... if other_speakers.exists(): # add an extra speaker - ep.speakers.add(random.choice(other_speakers)) + ep.speakers.add(random.choice(other_speakers)) # noqa: S311 # add tags for 2 out of 3 events - if random.choice([True, True, False]): + if random.choice([True, True, False]): # noqa: S311 # add 1-3 tags for this EP - ep.tags.add(*random.sample(tags, k=random.randint(1, 3))) + ep.tags.add(*random.sample(tags, k=random.randint(1, 3))) # noqa: S311 EventProposal.objects.create( - user=random.choice(User.objects.all()), + user=random.choice(User.objects.all()), # noqa: S311 title="Lunch break", abstract="Daily lunch break. Remember to drink water.", event_type=event_types["recreational"], - track=random.choice(camp.event_tracks.all()), + track=random.choice(camp.event_tracks.all()), # noqa: S311 ).mark_as_approved() - def create_proposal_urls(self, camp) -> None: + def create_proposal_urls(self, camp: Camp) -> None: """Create URL objects for the proposals.""" year = camp.camp.lower.year self.output( @@ -1129,7 +1155,7 @@ def create_proposal_urls(self, camp) -> None: ), ) - def generate_speaker_availability(self, camp) -> None: + def generate_speaker_availability(self, camp: Camp) -> None: """Create SpeakerAvailability objects for the SpeakerProposals.""" year = camp.camp.lower.year self.output( @@ -1150,18 +1176,18 @@ class FakeForm: form = FakeForm() for daychunks in matrix.values(): # 90% chance we have info for any given day - if random.randint(1, 100) > 90: + if random.randint(1, 100) > 90: # noqa: PLR2004, S311 # no availability info for this entire day, sorry continue for data in daychunks.values(): if not data: continue # 90% chance this speaker is available for any given chunk - form.cleaned_data[data["fieldname"]] = random.randint(1, 100) < 90 + form.cleaned_data[data["fieldname"]] = random.randint(1, 100) < 90 # noqa: PLR2004, S311 # print(f"saving availability for speaker {sp}: {form.cleaned_data}") save_speaker_availability(form, sp) - def approve_speaker_proposals(self, camp) -> None: + def approve_speaker_proposals(self, camp: Camp) -> None: """Approve all keynotes but reject 10% of other events.""" for sp in camp.speaker_proposals.filter( event_proposals__event_type__name="Keynote", @@ -1170,16 +1196,17 @@ def approve_speaker_proposals(self, camp) -> None: for sp in camp.speaker_proposals.filter(proposal_status="pending"): # we do not approve all speakers - x = random.randint(1, 100) - if x < 90: + x = random.randint(1, 100) # noqa: S311 + if x < 90: # noqa: PLR2004 sp.mark_as_approved() - elif x < 95: + elif x < 95: # noqa: PLR2004 # leave this as pending continue else: sp.mark_as_rejected() - def approve_event_proposals(self, camp) -> None: + def approve_event_proposals(self, camp: Camp) -> None: + """Approve event proposals but reject 10% if its not a keynote.""" for ep in camp.event_proposals.filter(proposal_status="pending"): # are all speakers for this event approved? for sp in ep.speakers.all(): @@ -1187,7 +1214,7 @@ def approve_event_proposals(self, camp) -> None: break else: # all speakers are approved, approve the event? always approve keynotes! - if random.randint(1, 100) < 90 or ep.event_type.name == "Keynote": + if random.randint(1, 100) < 90 or ep.event_type.name == "Keynote": # noqa: PLR2004, S311 ep.mark_as_approved() else: ep.mark_as_rejected() @@ -1196,10 +1223,11 @@ def approve_event_proposals(self, camp) -> None: for event in camp.events.filter(event_type__name="Workshop"): # this should put about half the workshops in the big room # (since the small rooms have max. 25 ppl capacity) - event.demand = random.randint(10, 40) + event.demand = random.randint(10, 40) # noqa: S311 event.save() - def create_camp_scheduling(self, camp, autoschedule) -> None: + def create_camp_scheduling(self, camp: Camp, autoschedule: bool) -> None: + """Create camp scheduling.""" year = camp.camp.lower.year self.output(f"Creating scheduling for {year}...") @@ -1207,7 +1235,7 @@ def create_camp_scheduling(self, camp, autoschedule) -> None: lunch = Event.objects.get(track__camp=camp, title="Lunch break") for day in camp.get_days(camppart="camp")[1:-1]: date = day.lower.date() - start = tz.localize(datetime(date.year, date.month, date.day, 12, 0)) + start = datetime(date.year, date.month, date.day, 12, 0, tzinfo=tz) lunchslot = EventSlot.objects.get( event_session__event_location=camp.event_locations.get( name="Speakers Tent", @@ -1236,7 +1264,8 @@ def create_camp_scheduling(self, camp, autoschedule) -> None: f"Done running autoscheduler for {year}... It took {scheduleduration}", ) - def create_camp_speaker_event_conflicts(self, camp) -> None: + def create_camp_speaker_event_conflicts(self, camp: Camp) -> None: + """Create speaker event conflicts.""" year = camp.camp.lower.year self.output( f"Generating event_conflicts for SpeakerProposals for {year}...", @@ -1244,9 +1273,9 @@ def create_camp_speaker_event_conflicts(self, camp) -> None: # loop over all for sp in camp.speaker_proposals.all(): # not all speakers add conflicts - if random.choice([True, True, False]): + if random.choice([True, True, False]): # noqa: S311 # pick 0-10 events this speaker wants to attend - conflictcount = random.randint(0, 10) + conflictcount = random.randint(0, 10) # noqa: S311 sp.event_conflicts.set( Event.objects.filter( track__camp=camp, @@ -1254,7 +1283,8 @@ def create_camp_speaker_event_conflicts(self, camp) -> None: ).order_by("?")[0:conflictcount], ) - def create_camp_rescheduling(self, camp, autoschedule) -> None: + def create_camp_rescheduling(self, camp: Camp, autoschedule: bool) -> None: + """Reschedule program for this camp.""" year = camp.camp.lower.year # reapprove all speaker_proposals so the new availability takes effect for prop in camp.speaker_proposals.filter(proposal_status="approved"): @@ -1275,7 +1305,8 @@ def create_camp_rescheduling(self, camp, autoschedule) -> None: scheduleduration = timezone.now() - schedulestart self.output(f"Done rescheduling for {year}... It took {scheduleduration}.") - def create_camp_villages(self, camp, users) -> None: + def create_camp_villages(self, camp: Camp, users: dict) -> None: + """Create camp villages.""" year = camp.camp.lower.year self.output(f"Creating villages for {year}...") Village.objects.create( @@ -1283,30 +1314,39 @@ def create_camp_villages(self, camp, users) -> None: camp=camp, name="Baconsvin", slug="baconsvin", - 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!", + 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", - 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.", + 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", + 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", ) - def create_camp_teams(self, camp) -> dict: + def create_camp_teams(self, camp: Camp) -> dict: + """Create camp teams.""" teams = {} year = camp.camp.lower.year self.output(f"Creating teams for {year}...") teams["orga"] = Team.objects.create( name="Orga", - description="The Orga team are the main organisers. All tasks are Orga responsibility until they are delegated to another team", + description="The Orga team are the main organisers. " + "All tasks are Orga responsibility until they are delegated to another team", camp=camp, needs_members=False, ) @@ -1366,7 +1406,8 @@ def create_camp_teams(self, camp) -> dict: camp.save() return teams - def create_camp_team_tasks(self, camp, teams) -> None: + def create_camp_team_tasks(self, camp: Camp, teams: dict) -> None: + """Create camp team tasks.""" year = camp.camp.lower.year self.output(f"Creating TeamTasks for {year}...") TeamTask.objects.create( @@ -1415,7 +1456,8 @@ def create_camp_team_tasks(self, camp, teams) -> None: description="We need ice cubes and crushed ice in the bar", ) - def create_camp_team_memberships(self, camp, teams, users): + def create_camp_team_memberships(self, camp: Camp, teams: dict, users: dict) -> dict: + """Create camp team memberships.""" memberships = {} year = camp.camp.lower.year self.output(f"Creating team memberships for {year}...") @@ -1527,15 +1569,16 @@ def create_camp_team_memberships(self, camp, teams, users): ) return memberships - def create_camp_team_shifts(self, camp, teams, team_memberships) -> None: + def create_camp_team_shifts(self, camp: Camp, teams: dict, team_memberships: dict) -> None: + """Create camp team shifts.""" shifts = {} year = camp.camp.lower.year self.output(f"Creating team shifts for {year}...") shifts[0] = TeamShift.objects.create( team=teams["shuttle"], shift_range=( - tz.localize(datetime(year, 8, 27, 2, 0)), - tz.localize(datetime(year, 8, 27, 8, 0)), + datetime(year, 8, 27, 2, 0, tzinfo=tz), + datetime(year, 8, 27, 8, 0, tzinfo=tz), ), people_required=1, ) @@ -1543,21 +1586,22 @@ def create_camp_team_shifts(self, camp, teams, team_memberships) -> None: shifts[1] = TeamShift.objects.create( team=teams["shuttle"], shift_range=( - tz.localize(datetime(year, 8, 27, 8, 0)), - tz.localize(datetime(year, 8, 27, 14, 0)), + datetime(year, 8, 27, 8, 0, tzinfo=tz), + datetime(year, 8, 27, 14, 0, tzinfo=tz), ), people_required=1, ) shifts[2] = TeamShift.objects.create( team=teams["shuttle"], shift_range=( - tz.localize(datetime(year, 8, 27, 14, 0)), - tz.localize(datetime(year, 8, 27, 20, 0)), + datetime(year, 8, 27, 14, 0, tzinfo=tz), + datetime(year, 8, 27, 20, 0, tzinfo=tz), ), people_required=1, ) - def create_camp_info_categories(self, camp, teams): + def create_camp_info_categories(self, camp: Camp, teams: dict) -> dict: + """Create camp info categories.""" categories = {} year = camp.camp.lower.year self.output(f"Creating infocategories for {year}...") @@ -1579,20 +1623,26 @@ def create_camp_info_categories(self, camp, teams): return categories - def create_camp_info_items(self, camp, categories) -> None: + def create_camp_info_items(self, camp: Camp, categories: dict) -> None: + """Create the camp info items.""" year = camp.camp.lower.year self.output(f"Creating infoitems for {year}...") InfoItem.objects.create( category=categories["when"], headline="Opening", anchor="opening", - body=f"BornHack {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 :)", + body=f"BornHack {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 {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).", + body=f"BornHack {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"], @@ -1604,7 +1654,10 @@ def create_camp_info_items(self, camp, categories) -> None: 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!", + 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"], @@ -1616,16 +1669,21 @@ def create_camp_info_items(self, camp, categories) -> None: 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.", + 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.", + 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.", ) - def create_camp_feedback(self, camp, users) -> None: + def create_camp_feedback(self, camp: Camp, users: dict[User]) -> None: + """Create camp feedback.""" year = camp.camp.lower.year self.output(f"Creating feedback for {year}...") Feedback.objects.create( @@ -1649,7 +1707,8 @@ def create_camp_feedback(self, camp, users) -> None: feedback="That was fun. Thanks!", ) - def create_camp_rides(self, camp, users) -> None: + def create_camp_rides(self, camp: Camp, users: dict) -> None: + """Create camp rides.""" year = camp.camp.lower.year self.output(f"Creating rides for {year}...") Ride.objects.create( @@ -1658,7 +1717,7 @@ def create_camp_rides(self, camp, users) -> None: seats=2, from_location="Copenhagen", to_location="BornHack", - when=tz.localize(datetime(year, 8, 27, 12, 0)), + when=datetime(year, 8, 27, 12, 0, tzinfo=tz), description="I have space for two people and a little bit of luggage", ) Ride.objects.create( @@ -1667,7 +1726,7 @@ def create_camp_rides(self, camp, users) -> None: seats=2, from_location="BornHack", to_location="Copenhagen", - when=tz.localize(datetime(year, 9, 4, 12, 0)), + when=datetime(year, 9, 4, 12, 0, tzinfo=tz), description="I have space for two people and a little bit of luggage", ) Ride.objects.create( @@ -1676,52 +1735,60 @@ def create_camp_rides(self, camp, users) -> None: seats=1, from_location="Aarhus", to_location="BornHack", - when=tz.localize(datetime(year, 8, 27, 12, 0)), + when=datetime(year, 8, 27, 12, 0, tzinfo=tz), description="I need a ride and have a large backpack", ) - def create_camp_cfp(self, camp) -> None: + def create_camp_cfp(self, camp: Camp) -> None: + """Create the camp call for participation.""" year = camp.camp.lower.year self.output(f"Creating CFP for {year}...") camp.call_for_participation_open = True camp.call_for_participation = f"Please give a talk at Bornhack {year}..." camp.save() - def create_camp_cfs(self, camp) -> None: + def create_camp_cfs(self, camp: Camp) -> None: + """Create the camp call for sponsors.""" year = camp.camp.lower.year self.output(f"Creating CFS for {year}...") camp.call_for_sponsors_open = True camp.call_for_sponsors = f"Please give us ALL the money so that we can make Bornhack {year} the best ever!" camp.save() - def create_camp_sponsor_tiers(self, camp): + def create_camp_sponsor_tiers(self, camp: Camp) -> dict: + """Create the camp sponsor tiers.""" tiers = {} year = camp.camp.lower.year self.output(f"Creating sponsor tiers for {year}...") tiers["platinum"] = SponsorTier.objects.create( 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- promoted HackMe\n- sponsored social event", + 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\n- sponsored social event", camp=camp, weight=0, week_tickets=10, ) tiers["gold"] = SponsorTier.objects.create( 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", + 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", camp=camp, weight=1, week_tickets=10, ) tiers["silver"] = SponsorTier.objects.create( 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", + 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", camp=camp, weight=2, week_tickets=5, ) tiers["sponsor"] = SponsorTier.objects.create( 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", + description="- 2 tickets\n- logo on website\n- physical banner in the speaker's tent\n- " + "thanks from the podium\n- recruitment area", camp=camp, weight=3, week_tickets=2, @@ -1729,7 +1796,8 @@ def create_camp_sponsor_tiers(self, camp): return tiers - def create_camp_sponsors(self, camp, tiers) -> None: + def create_camp_sponsors(self, camp: Camp, tiers: dict) -> None: + """Create the camp sponsors.""" year = camp.camp.lower.year self.output(f"Creating sponsors for {year}...") Sponsor.objects.create( @@ -1768,7 +1836,8 @@ def create_camp_sponsors(self, camp, tiers) -> None: url="https://csis.dk", ) - def create_camp_tokens(self, camp): + def create_camp_tokens(self, camp: Camp) -> dict[Token]: + """Create the camp tokens.""" tokens = {} year = camp.camp.lower.year self.output(f"Creating tokens for {year}...") @@ -1817,7 +1886,8 @@ def create_camp_tokens(self, camp): return tokens - def create_camp_token_finds(self, camp, tokens, users) -> None: + def create_camp_token_finds(self, camp: Camp, tokens: dict[Token], users: dict[User]) -> None: + """Create the camp token finds.""" year = camp.camp.lower.year self.output(f"Creating token finds for {year}...") TokenFind.objects.create(token=tokens[3], user=users[4]) @@ -1829,12 +1899,14 @@ def create_camp_token_finds(self, camp, tokens, users) -> None: for i in range(6): TokenFind.objects.create(token=tokens[i], user=users[1]) - def create_camp_expenses(self, camp) -> None: + def create_camp_expenses(self, camp: Camp) -> None: + """Create camp expenses.""" self.output(f"Creating expenses for {camp}...") for team in Team.objects.filter(camp=camp): ExpenseFactory.create_batch(10, camp=camp, responsible_team=team) - def create_camp_reimbursements(self, camp) -> None: + def create_camp_reimbursements(self, camp: Camp) -> None: + """Create camp reimbursements.""" self.output(f"Creating reimbursements for {camp}...") users = User.objects.filter( id__in=Expense.objects.filter( @@ -1857,18 +1929,19 @@ def create_camp_reimbursements(self, camp) -> None: camp=camp, user=user, reimbursement_user=user, - bank_account=random.randint(1000000000, 100000000000), + bank_account=random.randint(1000000000, 100000000000), # noqa: S311 notes=f"bootstrap created reimbursement for user {user.username}", - paid=random.choice([True, True, False]), + paid=random.choice([True, True, False]), # noqa: S311 ) expenses.update(reimbursement=reimbursement) reimbursement.create_payback_expense() - def create_camp_revenues(self, camp) -> None: + def create_camp_revenues(self, camp: Camp) -> None: + """Method for creating revenue.""" self.output(f"Creating revenues for {camp}...") RevenueFactory.create_batch(20, camp=camp) - def add_team_permissions(self, camp) -> None: + def add_team_permissions(self, camp: Camp) -> None: """Assign member permissions to the team groups for this camp.""" self.output(f"Assigning permissions to team groups for {camp}...") permission_content_type = ContentType.objects.get_for_model(CampPermission) @@ -1880,6 +1953,7 @@ def add_team_permissions(self, camp) -> None: team.group.permissions.add(permission) def create_maps_layer_generic(self) -> None: + """Create map layers that do not have a camp attached.""" group = MapGroup.objects.create(name="Generic") layer = Layer.objects.create( name="Areas", @@ -1911,7 +1985,8 @@ def create_maps_layer_generic(self) -> None: processing="", ) - def create_camp_map_layer(self, camp) -> None: + 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 = Layer.objects.create( @@ -1944,7 +2019,8 @@ def create_camp_map_layer(self, camp) -> None: processing="", ) - def create_camp_pos(self, teams) -> None: + def create_camp_pos(self, teams: dict[Team]) -> None: + """Create POS locations for camp.""" Pos.objects.create( name="Infodesk", team=teams["info"], @@ -1956,10 +2032,12 @@ def create_camp_pos(self, teams) -> None: external_id="bTasxE2YYXZh35wtQ", ) - def output(self, message) -> None: + 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, @@ -2033,9 +2111,10 @@ def bootstrap_full(self, options: dict) -> None: }, ] self.create_camps(camps) - self.run_bootstrap(options) + self.bootstrap_base(options) def bootstrap_tests(self) -> None: + """Method for bootstrapping the test database.""" camps = [ { "year": 2024, @@ -2076,54 +2155,21 @@ def bootstrap_tests(self) -> None: self.camp = self.camps[1][0] self.teams = teams[self.camp.camp.lower.year] - def run_bootstrap(self, options: dict) -> None: - self.output( - "----------[ Running bootstrap_devsite ]----------" - ) - - self.output("----------[ Global stuff ]----------") - - self.create_event_routing_types() - self.create_users(16) - - self.create_news() - - event_types = self.create_event_types() - - self.create_url_types() - - product_categories = self.create_product_categories() - - quickfeedback_options = self.create_quickfeedback_options() - - self.create_mobilepay_transactions() - - self.create_clearhaus_settlements() - - self.create_credebtors() - - self.create_bank_stuff() - - self.create_coinify_stuff() - - self.create_epay_transactions() - - self.create_maps_layer_generic() - + def bootstrap_camp(self, options: dict) -> None: + """Bootstrap camp related entities.""" permissions_added = False + self.teams = {} for camp, read_only in self.camps: - year = camp.camp.lower.year - self.output( - f"----------[ Bornhack {year} ]----------" + f"----------[ Bornhack {camp.camp.lower.year} ]----------", ) - if year <= settings.UPCOMING_CAMP_YEAR: + if camp.camp.lower.year <= settings.UPCOMING_CAMP_YEAR: ticket_types = self.create_camp_ticket_types(camp) camp_products = self.create_camp_products( camp, - product_categories, + self.product_categories, ticket_types, ) @@ -2136,6 +2182,7 @@ def run_bootstrap(self, options: dict) -> None: self.create_camp_news(camp) teams = self.create_camp_teams(camp) + self.teams[camp.camp.lower.year] = teams if not read_only and not permissions_added: # add permissions for the first camp that is not read_only @@ -2152,11 +2199,11 @@ def run_bootstrap(self, options: dict) -> None: self.create_camp_cfp(camp) - self.create_camp_proposals(camp, event_types) + self.create_camp_proposals(camp, self.event_types) self.create_proposal_urls(camp) - self.create_camp_event_sessions(camp, event_types, locations) + self.create_camp_event_sessions(camp, self.event_types, locations) self.generate_speaker_availability(camp) @@ -2164,7 +2211,8 @@ def run_bootstrap(self, options: dict) -> None: 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) @@ -2184,14 +2232,13 @@ def run_bootstrap(self, options: dict) -> None: self.create_camp_villages(camp, self.users) facility_types = self.create_facility_types( - camp, teams, - quickfeedback_options, + self.quickfeedback_options, ) facilities = self.create_facilities(facility_types) - self.create_facility_feedbacks(facilities, quickfeedback_options, self.users) + self.create_facility_feedbacks(facilities, self.quickfeedback_options, self.users) info_categories = self.create_camp_info_categories(camp, teams) @@ -2226,9 +2273,47 @@ def run_bootstrap(self, options: dict) -> None: camp.call_for_sponsors_open = not read_only camp.save() + def bootstrap_base(self, options: dict) -> None: + """Bootstrap the data for the application.""" + self.output( + "----------[ Running bootstrap_devsite ]----------", + ) + + self.output("----------[ Global stuff ]----------") + + self.create_event_routing_types() + self.create_users(16) + + self.create_news() + + self.create_event_types() + + self.create_url_types() + + self.create_product_categories() + + self.create_quickfeedback_options() + + self.create_mobilepay_transactions() + + self.create_clearhaus_settlements() + + self.create_credebtors() + + self.create_bank_stuff() + + self.create_coinify_stuff() + + self.create_epay_transactions() + + self.create_maps_layer_generic() + + self.bootstrap_camp(options) + self.output("----------[ Finishing up ]----------") self.output("Adding event routing...") + teams = self.teams[next(reversed(self.teams.keys()))] Routing.objects.create( team=teams["orga"], eventtype=Type.objects.get(name="public_credit_name_changed"), @@ -2239,4 +2324,3 @@ def run_bootstrap(self, options: dict) -> None: ) self.output("done!") - diff --git a/src/utils/bootstrap/factories.py b/src/utils/bootstrap/factories.py index e26cb9377..ca25ac93e 100644 --- a/src/utils/bootstrap/factories.py +++ b/src/utils/bootstrap/factories.py @@ -1,3 +1,5 @@ +"""Factories for bootstrapping the application.""" + from __future__ import annotations import logging @@ -7,6 +9,7 @@ import pytz from allauth.account.models import EmailAddress from django.contrib.auth.models import User +from django.db.models.signals import post_save from faker import Faker from camps.models import Camp @@ -21,10 +24,9 @@ from program.models import UrlType from teams.models import Team from utils.slugs import unique_slugify -from django.db.models.signals import post_save -from .functions import output_fake_md_description from .functions import output_fake_description +from .functions import output_fake_md_description fake = Faker() tz = pytz.timezone("Europe/Copenhagen") @@ -32,7 +34,11 @@ class ChainFactory(factory.django.DjangoModelFactory): + """Factory for creating chains.""" + class Meta: + """Meta.""" + model = Chain name = factory.Faker("company") @@ -45,7 +51,11 @@ class Meta: class CredebtorFactory(factory.django.DjangoModelFactory): + """Factory for creating Creditors and debitors.""" + class Meta: + """Meta.""" + model = Credebtor chain = factory.SubFactory(ChainFactory) @@ -61,7 +71,11 @@ class Meta: class ExpenseFactory(factory.django.DjangoModelFactory): + """Factory for creating expense data.""" + class Meta: + """Meta.""" + model = Expense camp = factory.Faker("random_element", elements=Camp.objects.all()) @@ -71,7 +85,7 @@ class Meta: description = factory.Faker("text") paid_by_bornhack = factory.Faker("random_element", elements=[True, True, False]) invoice = factory.django.ImageField( - color=random.choice(["#ff0000", "#00ff00", "#0000ff"]), + color=random.choice(["#ff0000", "#00ff00", "#0000ff"]), # noqa: S311 ) invoice_date = factory.Faker("date") responsible_team = factory.Faker("random_element", elements=Team.objects.all()) @@ -80,7 +94,11 @@ class Meta: class RevenueFactory(factory.django.DjangoModelFactory): + """Factory for creating revenue data.""" + class Meta: + """Meta.""" + model = Revenue camp = factory.Faker("random_element", elements=Camp.objects.all()) @@ -89,7 +107,7 @@ class Meta: amount = factory.Faker("random_int", min=20, max=20000) description = factory.Faker("text") invoice = factory.django.ImageField( - color=random.choice(["#ff0000", "#00ff00", "#0000ff"]), + color=random.choice(["#ff0000", "#00ff00", "#0000ff"]), # noqa: S311 ) invoice_date = factory.Faker("date") responsible_team = factory.Faker("random_element", elements=Team.objects.all()) @@ -98,7 +116,11 @@ class Meta: class ProfileFactory(factory.django.DjangoModelFactory): + """Factory for creating user profiles.""" + class Meta: + """Meta.""" + model = Profile user = factory.SubFactory("self.UserFactory", profile=None) @@ -110,21 +132,34 @@ class Meta: @factory.django.mute_signals(post_save) class UserFactory(factory.django.DjangoModelFactory): + """Factory for creating a User.""" + class Meta: + """Meta.""" + model = User profile = factory.RelatedFactory(ProfileFactory, "user") class EmailAddressFactory(factory.django.DjangoModelFactory): + """Factory for email address.""" + class Meta: + """Meta.""" + model = EmailAddress primary = False verified = True + class SpeakerProposalFactory(factory.django.DjangoModelFactory): + """Factory for speaker proposals.""" + class Meta: + """Meta.""" + model = SpeakerProposal name = factory.Faker("name") @@ -135,7 +170,11 @@ class Meta: class EventProposalFactory(factory.django.DjangoModelFactory): + """Factory for event proposals.""" + class Meta: + """Meta.""" + model = EventProposal user = factory.Iterator(User.objects.all()) @@ -148,7 +187,11 @@ class Meta: class EventProposalUrlFactory(factory.django.DjangoModelFactory): + """Factory for event proposal urls.""" + class Meta: + """Meta.""" + model = Url url = factory.Faker("url") @@ -156,10 +199,12 @@ class Meta: class SpeakerProposalUrlFactory(factory.django.DjangoModelFactory): + """Factory for speaker proposal urls.""" + class Meta: + """Meta.""" + model = Url url = factory.Faker("url") url_type = factory.Iterator(UrlType.objects.all()) - - diff --git a/src/utils/bootstrap/functions.py b/src/utils/bootstrap/functions.py index 6b85ab335..094bc5156 100644 --- a/src/utils/bootstrap/functions.py +++ b/src/utils/bootstrap/functions.py @@ -1,3 +1,5 @@ +"""Functions used to bootstrap/test the application.""" + from __future__ import annotations import logging @@ -5,14 +7,13 @@ import pytz from faker import Faker - fake = Faker() tz = pytz.timezone("Europe/Copenhagen") logger = logging.getLogger(f"bornhack.{__name__}") - -def output_fake_md_description(): +def output_fake_md_description() -> str: + """Method for creating a fake markup description using Faker().""" fake_text = "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) fake_text += "\n\n" fake_text += "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) @@ -29,8 +30,8 @@ def output_fake_md_description(): return fake_text -def output_fake_description(): +def output_fake_description() -> str: + """Method for creating a fake description using Faker().""" fake_text = "\n".join(fake.paragraphs(nb=3, ext_word_list=None)) fake_text += "* [" + fake.sentence(nb_words=3) + "](" + fake.uri() + ")\n" return fake_text - From 8e34d03d95b980c7e1bb31abbf64b86b18620c61 Mon Sep 17 00:00:00 2001 From: "Rudy (zarya)" Date: Sun, 18 May 2025 09:34:08 +0200 Subject: [PATCH 04/10] WIP: teams --- src/teams/templates/team_base.html | 2 +- src/teams/templates/team_shift_list.html | 99 ++++--- .../{test_views.py => test_guide_views.py} | 17 +- src/teams/tests/test_info_views.py | 117 ++++++++ src/teams/tests/test_shift_views.py | 259 ++++++++++++++++++ src/teams/tests/test_task_views.py | 137 +++++++++ src/teams/urls.py | 1 + src/teams/views/__init__.py | 1 + src/teams/views/base.py | 64 +++-- src/teams/views/guide.py | 3 +- src/teams/views/info.py | 51 +++- src/teams/views/members.py | 40 ++- src/teams/views/shifts.py | 23 +- src/teams/views/tasks.py | 7 +- src/utils/bootstrap/base.py | 23 +- src/utils/mixins.py | 19 ++ 16 files changed, 757 insertions(+), 106 deletions(-) rename src/teams/tests/{test_views.py => test_guide_views.py} (59%) create mode 100644 src/teams/tests/test_info_views.py create mode 100644 src/teams/tests/test_shift_views.py create mode 100644 src/teams/tests/test_task_views.py diff --git a/src/teams/templates/team_base.html b/src/teams/templates/team_base.html index a5187cfe2..557eac6d0 100644 --- a/src/teams/templates/team_base.html +++ b/src/teams/templates/team_base.html @@ -45,7 +45,7 @@

{{ team.name }} Team

{% endif %} - {% if request.user in team.responsible_members %} + {% if is_team_infopager %}
{% for shift in shifts %} {% ifchanged shift.shift_range.lower|date:'d' %} + - + + + + + + + + {% endifchanged %} + + + + + {% endfor %}
Number

{{ shift.shift_range.lower|date:'Y-m-d l' }}

-
- From - - To - - People required - - People - - Actions + + +
+ From + To + People required + People + Actions
{{ shift.shift_range.lower|date:'H:i' }} - - {{ shift.shift_range.upper|date:'H:i' }} - - {{ shift.people_required }} - - {% for member in shift.team_members.all %} - {{ member.user.profile.get_public_credit_name }}{% if not forloop.last %},{% endif %} - {% empty %} - None! - {% endfor %} - - - {% if request.user in team.leads.all %} - - Edit - - - Delete - - {% endif %} - {% if user in shift.users %} - - Unassign me - - {% elif shift.people_required > shift.team_members.count %} - - Assign me - - {% endif %} + + {{ shift.shift_range.upper|date:'H:i' }} + + {{ shift.people_required }} + + {% for member in shift.team_members.all %} + {{ member.user.profile.get_public_credit_name }}{% if not forloop.last %},{% endif %} + {% empty %} + None! + {% endfor %} + + {% if request.user in team.leads.all %} + + Edit + + + Delete + + {% endif %} + {% if user in shift.users %} + + Unassign me + + {% elif shift.people_required > shift.team_members.count %} + + Assign me + + {% endif %} +
{% endblock %} diff --git a/src/teams/tests/test_views.py b/src/teams/tests/test_guide_views.py similarity index 59% rename from src/teams/tests/test_views.py rename to src/teams/tests/test_guide_views.py index 851e0fc19..2fb8ffb3a 100644 --- a/src/teams/tests/test_views.py +++ b/src/teams/tests/test_guide_views.py @@ -2,12 +2,11 @@ from __future__ import annotations -from bs4 import BeautifulSoup from django.urls import reverse -from teams.models import Team from utils.tests import BornhackTestBase + class TeamGuideViewTest(BornhackTestBase): """Test Team Guide View""" @@ -22,11 +21,21 @@ def test_team_guide_views_permission(self) -> None: """Test the team guide view.""" self.client.force_login(self.users[0]) - url = reverse("teams:guide", kwargs={"team_slug": self.team.slug, "camp_slug": self.camp.slug}) + url = reverse("teams:guide", kwargs={"team_slug": self.teams["noc"].slug, "camp_slug": self.camp.slug}) response = self.client.get(path=url, follow=True) assert response.status_code == 403 - url = reverse("teams:guide_print", kwargs={"team_slug": self.team.slug, "camp_slug": self.camp.slug}) + url = reverse("teams:guide_print", kwargs={"team_slug": self.teams["noc"].slug, "camp_slug": self.camp.slug}) response = self.client.get(path=url, follow=True) assert response.status_code == 403 + self.client.force_login(self.users[5]) + + url = reverse("teams:guide", kwargs={"team_slug": self.teams["noc"].slug, "camp_slug": self.camp.slug}) + response = self.client.get(path=url, follow=True) + assert response.status_code == 200 + + url = reverse("teams:guide_print", kwargs={"team_slug": self.teams["noc"].slug, "camp_slug": self.camp.slug}) + response = self.client.get(path=url, follow=True) + assert response.status_code == 200 + diff --git a/src/teams/tests/test_info_views.py b/src/teams/tests/test_info_views.py new file mode 100644 index 000000000..f9f49083c --- /dev/null +++ b/src/teams/tests/test_info_views.py @@ -0,0 +1,117 @@ +"""Test cases for the info views of the teams application.""" + +from __future__ import annotations + +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse + +from camps.models import Permission as CampPermission +from utils.tests import BornhackTestBase + + +class TeamInfoViewTest(BornhackTestBase): + """Test Team Info Views.""" + + categories: dict + + @classmethod + def setUpTestData(cls) -> None: + """Setup test data.""" + # first add users and other basics + super().setUpTestData() + + cls.categories = cls.bootstrap.create_camp_info_categories(camp=cls.camp, teams=cls.teams) + cls.bootstrap.create_camp_info_items(camp=cls.camp,categories=cls.categories) + + permission_content_type = ContentType.objects.get_for_model(CampPermission) + cls.users[4].user_permissions.add( + Permission.objects.get( + content_type=permission_content_type, + codename="noc_team_infopager", + ), + ) + + def test_team_info_view_permissions(self) -> None: + """Test the team info view permissions.""" + self.client.force_login(self.users[0]) # Non noc team member + # Test access control to the views + url = reverse("teams:info_categories", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(path=url) + assert response.status_code == 403 + + def test_team_info_views(self) -> None: + """Test the team info views.""" + self.client.force_login(self.users[4]) # Noc teamlead + + # Test info categories page + url = reverse("teams:info_categories", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(url) + assert response.status_code == 200 + + # Test info categories create page + url = reverse("teams:info_item_create", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + "category_anchor": self.categories["noc"].anchor, + }) + response = self.client.get(url) + assert response.status_code == 200 + + response = self.client.post( + path=url, + data={ + "category": self.categories["noc"].anchor, + "headline": "Test info page", + "body": "Some test info", + "anchor": "test", + "weight": 100, + }, + follow=True, + ) + assert response.status_code == 200 + + # Test info categories edit page + url = reverse("teams:info_item_update", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + "category_anchor": self.categories["noc"].anchor, + "item_anchor": "test", + }) + response = self.client.get(url) + assert response.status_code == 200 + + response = self.client.post( + path=url, + data={ + "category": self.categories["noc"].anchor, + "headline": "Test info page", + "body": "Some test info", + "anchor": "test", + "weight": 101, + }, + follow=True, + ) + assert response.status_code == 200 + + # Test info categories delete page + url = reverse("teams:info_item_delete", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + "category_anchor": self.categories["noc"].anchor, + "item_anchor": "test", + }) + response = self.client.get(url) + assert response.status_code == 200 + + response = self.client.post( + path=url, + follow=True, + ) + assert response.status_code == 200 diff --git a/src/teams/tests/test_shift_views.py b/src/teams/tests/test_shift_views.py new file mode 100644 index 000000000..a7fb212af --- /dev/null +++ b/src/teams/tests/test_shift_views.py @@ -0,0 +1,259 @@ +"""Test cases for the shift views of the teams application.""" + +from __future__ import annotations + +from bs4 import BeautifulSoup +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse +from django.utils import timezone + +from camps.models import Permission as CampPermission +from teams.models import TeamShift +from utils.tests import BornhackTestBase + + +class TeamShiftViewTest(BornhackTestBase): + """Test Team Shift View""" + + @classmethod + def setUpTestData(cls) -> None: + """Setup test data.""" + # first add users and other basics + super().setUpTestData() + permission_content_type = ContentType.objects.get_for_model(CampPermission) + cls.users[4].user_permissions.add( + Permission.objects.get( + content_type=permission_content_type, + codename="noc_team_lead", + ), + ) + + def test_team_shift_view_permissions(self) -> None: + """Test the team shift view permissions.""" + self.client.force_login(self.users[0]) # Non noc team member + # Test access control to the views + url = reverse("teams:shift_create", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(path=url) + assert response.status_code == 302 + + def test_team_user_shift_view(self) -> None: + """Test the user shift view.""" + self.client.force_login(self.users[4]) # Noc teamlead + url = reverse("teams:user_shifts", kwargs={ + "camp_slug": self.camp.slug, + }) + response = self.client.get(path=url) + assert response.status_code == 200 + + def test_team_shift_views(self) -> None: + """Test the team shift views.""" + self.client.force_login(self.users[4]) # Noc teamlead + + # Test creating a shift + url = reverse("teams:shift_create", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(url) + assert response.status_code == 200 + + response = self.client.post( + path=url, + data={ + "from_datetime": self.camp.buildup.lower.date(), + "to_datetime": self.camp.buildup.lower + timezone.timedelta(hours=1), + "people_required": 1, + }, + follow=True, + ) + assert response.status_code == 200 + content = response.content.decode() + + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("table#main_table > tbody > tr") + self.assertEqual(len(rows), 1, "team shift list does not return 1 entries after create") + + # Test same start and end. + response = self.client.post( + path=url, + data={ + "from_datetime": self.camp.buildup.lower, + "to_datetime": self.camp.buildup.lower, + "people_required": 1, + }, + follow=True, + ) + assert response.status_code == 200 + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("ul.list-unstyled.text-danger > li") + matches = [s for s in rows if "Start can not be the same as end." in str(s)] + self.assertEqual(len(matches), 1, "team shift Start can not be equal to end") + + # Test same end before start. + response = self.client.post( + path=url, + data={ + "from_datetime": self.camp.buildup.lower + timezone.timedelta(hours=1), + "to_datetime": self.camp.buildup.lower, + "people_required": 1, + }, + follow=True, + ) + assert response.status_code == 200 + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("ul.list-unstyled.text-danger > li") + matches = [s for s in rows if "Start can not be after end." in str(s)] + self.assertEqual(len(matches), 1, "team shift Start can not be before to end") + + # Test Creating multiple shifts + url = reverse("teams:shift_create_multiple", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + + response = self.client.get(url) + assert response.status_code == 200 + + response = self.client.post( + path=url, + data={ + "from_datetime": self.camp.camp.lower.date(), + "shift_length": 60, + "number_of_shifts": 10, + "people_required": 5, + }, + follow=True, + ) + assert response.status_code == 200 + + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("table#main_table > tbody > tr") + self.assertEqual(len(rows), 11, "team shift list does not return 11 entries after create multiple") + + # Lookup the id of one of the shifts. + shift_row = soup.select_one("table#main_table > tbody > tr:nth-of-type(1) td:nth-of-type(5)") + shift_link = shift_row.find("a") + shift_id = int(shift_link["href"].split("/")[5]) + + # Test the update view + url = reverse("teams:shift_update", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + "pk": shift_id, + }) + from_datetime = self.camp.buildup.lower + to_datetime = from_datetime + timezone.timedelta(hours=2) + + response = self.client.get(url) + assert response.status_code == 200 + + response = self.client.post( + path=url, + data={ + "from_datetime": from_datetime, + "to_datetime": to_datetime, + "people_required": 2, + }, + follow=True, + ) + assert response.status_code == 200 + + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + row = soup.select_one("table#main_table > tbody > tr:nth-of-type(1) td:nth-of-type(3)").get_text(strip=True) + self.assertEqual(row, "2", "team shift people required count does not return 2 entries after update") + + # Test the delete view + url = reverse("teams:shift_delete", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + "pk": shift_id, + }) + response = self.client.get(url) + assert response.status_code == 200 + + response = self.client.post(path=url, follow=True) + assert response.status_code == 200 + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("table#main_table > tbody > tr") + self.assertEqual(len(rows), 10, "team shift list does not return 10 entries after delete") + + def test_team_shift_actions(self) -> None: + """Test the team shift actions.""" + self.client.force_login(self.users[4]) # Noc teamlead + + team_shift_1 = TeamShift( + team=self.teams["noc"], + shift_range=( + self.camp.buildup.lower, + self.camp.buildup.lower + timezone.timedelta(hours=1), + ), + people_required=1, + ) + team_shift_1.save() + team_shift_2 = TeamShift( + team=self.teams["noc"], + shift_range=( + self.camp.buildup.lower, + self.camp.buildup.lower + timezone.timedelta(hours=1), + ), + people_required=1, + ) + team_shift_2.save() + + url = reverse("teams:shift_member_take", kwargs={ + "team_slug": team_shift_1.team.slug, + "camp_slug": self.camp.slug, + "pk": team_shift_1.pk, + }) + response = self.client.get( + path=url, + follow=True, + ) + assert response.status_code == 200 + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select_one("table#main_table > tbody > tr:nth-of-type(1) td:nth-of-type(5)") + matches = [s for s in rows if "Unassign me" in str(s)] + self.assertEqual(len(matches), 1, "team shift assign failed") + + url = reverse("teams:shift_member_take", kwargs={ + "team_slug": team_shift_1.team.slug, + "camp_slug": self.camp.slug, + "pk": team_shift_2.pk, + }) + response = self.client.get( + path=url, + follow=True, + ) + assert response.status_code == 200 + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("div.alert.alert-danger") + matches = [s for s in rows if "overlapping" in str(s)] + self.assertEqual(len(matches), 1, "team shift double assign failed to fail") + + url = reverse("teams:shift_member_drop", kwargs={ + "team_slug": team_shift_1.team.slug, + "camp_slug": self.camp.slug, + "pk": team_shift_1.pk, + }) + + response = self.client.get( + path=url, + follow=True, + ) + assert response.status_code == 200 + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select_one("table#main_table > tbody > tr:nth-of-type(1) td:nth-of-type(5)") + matches = [s for s in rows if "Assign me" in str(s)] + self.assertEqual(len(matches), 1, "team shift unassign failed") diff --git a/src/teams/tests/test_task_views.py b/src/teams/tests/test_task_views.py new file mode 100644 index 000000000..80ca34690 --- /dev/null +++ b/src/teams/tests/test_task_views.py @@ -0,0 +1,137 @@ +"""Test cases for the task views of the teams application.""" + +from __future__ import annotations + +from bs4 import BeautifulSoup +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse + +from camps.models import Permission as CampPermission +from utils.tests import BornhackTestBase + + +class TeamTaskViewTest(BornhackTestBase): + """Test Team Task View""" + + @classmethod + def setUpTestData(cls) -> None: + """Setup test data.""" + # first add users and other basics + super().setUpTestData() + permission_content_type = ContentType.objects.get_for_model(CampPermission) + cls.users[4].user_permissions.add( + Permission.objects.get( + content_type=permission_content_type, + codename="noc_team_tasker", + ), + ) + + def test_team_task_view_permissions(self) -> None: + """Test the team shift view permissions.""" + self.client.force_login(self.users[0]) # Non noc team member + # Test access control to the views + url = reverse("teams:task_create", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(path=url) + assert response.status_code == 403 + + def test_team_task_views(self) -> None: + """Test the team task views.""" + self.client.force_login(self.users[4]) # Noc team tasker + url = reverse("teams:tasks", kwargs={ + "camp_slug": self.camp.slug, + "team_slug": self.teams["noc"].slug, + }) + response = self.client.get(path=url) + assert response.status_code == 200 + + # Test creating a shift + url = reverse("teams:task_create", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(url) + assert response.status_code == 200 + + response = self.client.post( + path=url, + data={ + "name": "Test task", + "description": "Test task description", + "when_0": self.camp.buildup.lower, + "when_1": self.camp.buildup.upper, + }, + follow=True, + ) + assert response.status_code == 200 + + task = response.context["task"] + + # Test if the task got to the list. + url = reverse("teams:tasks", kwargs={ + "camp_slug": self.camp.slug, + "team_slug": self.teams["noc"].slug, + }) + response = self.client.get(path=url) + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("table#main_table > tbody > tr") + self.assertEqual(len(rows), 1, "team task list does not return 1 entries after create") + + # Test updating a task. + url = reverse("teams:task_update", kwargs={ + "camp_slug": self.camp.slug, + "team_slug": self.teams["noc"].slug, + "slug": task.slug, + }) + + response = self.client.get(path=url) + assert response.status_code == 200 + + response = self.client.post( + path=url, + data={ + "name": "Test task", + "description": "Test updating a task description", + "when_0": self.camp.buildup.lower, + "when_1": self.camp.buildup.upper, + "completed": True, + }, + follow=True, + ) + assert response.status_code == 200 + + # Test submitting a comment + url = reverse("teams:task_detail", kwargs={ + "camp_slug": self.camp.slug, + "team_slug": self.teams["noc"].slug, + "slug": task.slug, + }) + + response = self.client.post( + path=url, + data={ + "comment": "Some test comment", + }, + follow=True, + ) + assert response.status_code == 200 + + response = self.client.post( + path=url, + data={ + "comment": "", + }, + follow=True, + ) + assert response.status_code == 200 + + # Test if the page returned a failure + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("div.alert.alert-danger") + matches = [s for s in rows if "Something went wrong." in str(s)] + self.assertEqual(len(matches), 1, "comment does not return a error msg.") diff --git a/src/teams/urls.py b/src/teams/urls.py index b4040b583..39deab476 100644 --- a/src/teams/urls.py +++ b/src/teams/urls.py @@ -1,3 +1,4 @@ +"""Django URLs for application Teams.""" from __future__ import annotations from django.urls import include diff --git a/src/teams/views/__init__.py b/src/teams/views/__init__.py index e69de29bb..ac0945129 100644 --- a/src/teams/views/__init__.py +++ b/src/teams/views/__init__.py @@ -0,0 +1 @@ +"""All views for application Teams.""" diff --git a/src/teams/views/base.py b/src/teams/views/base.py index af4b735a8..3d808760d 100644 --- a/src/teams/views/base.py +++ b/src/teams/views/base.py @@ -1,6 +1,8 @@ +"""Base view for teams.""" from __future__ import annotations import logging +from typing import TYPE_CHECKING from django.conf import settings from django.contrib import messages @@ -14,19 +16,29 @@ from camps.mixins import CampViewMixin from teams.models import Team from teams.models import TeamMember +from utils.mixins import IsPermissionMixin from utils.widgets import MarkdownWidget from .mixins import EnsureTeamLeadMixin +if TYPE_CHECKING: + from django.db.models import QuerySet + from django.forms import Form + from django.http import HttpRequest + from django.http import HttpResponse + from django.http import HttpResponseRedirect + logger = logging.getLogger(f"bornhack.{__name__}") class TeamListView(CampViewMixin, ListView): + """View for the list of teams.""" template_name = "team_list.html" model = Team context_object_name = "teams" - def get_queryset(self, *args, **kwargs): + def get_queryset(self, *args, **kwargs) -> QuerySet: + """Method for prefetching team members.""" qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related("members") return qs.prefetch_related("members__profile") @@ -35,7 +47,8 @@ def get_queryset(self, *args, **kwargs): # also the getting of team leads and their profiles do not use the prefetching # :( /tyk - def get_context_data(self, *, object_list=None, **kwargs): + def get_context_data(self, *, object_list=None, **kwargs) -> dict: + """Method for adding user_teams to the context.""" context = super().get_context_data(object_list=object_list, **kwargs) if self.request.user.is_authenticated: context["user_teams"] = self.request.user.teammember_set.filter( @@ -44,24 +57,27 @@ def get_context_data(self, *, object_list=None, **kwargs): return context -class TeamGeneralView(CampViewMixin, DetailView): +class TeamGeneralView(CampViewMixin, IsPermissionMixin, DetailView): + """General view for a team.""" template_name = "team_general.html" context_object_name = "team" model = Team slug_url_kwarg = "team_slug" active_menu = "general" - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Method for adding ircbot info to context.""" context = super().get_context_data(**kwargs) context["IRCBOT_SERVER_HOSTNAME"] = settings.IRCBOT_SERVER_HOSTNAME context["IRCBOT_PUBLIC_CHANNEL"] = settings.IRCBOT_PUBLIC_CHANNEL return context -class TeamManageView(CampViewMixin, EnsureTeamLeadMixin, UpdateView): +class TeamManageView(CampViewMixin, EnsureTeamLeadMixin, IsPermissionMixin, UpdateView): + """View for mananaging team members.""" model = Team template_name = "team_manage.html" - fields = [ + fields = ( "description", "needs_members", "public_irc_channel_name", @@ -73,32 +89,37 @@ class TeamManageView(CampViewMixin, EnsureTeamLeadMixin, UpdateView): "public_signal_channel_link", "private_signal_channel_link", "guide", - ] + ) slug_url_kwarg = "team_slug" - def get_form(self, *args, **kwargs): + def get_form(self, *args, **kwargs) -> Form: + """Method for updating form widgets.""" form = super().get_form(*args, **kwargs) form.fields["guide"].widget = MarkdownWidget() return form - def get_success_url(self): + def get_success_url(self) -> str: + """Method for returning the success url.""" return reverse_lazy( "teams:general", kwargs={"camp_slug": self.camp.slug, "team_slug": self.get_object().slug}, ) - def form_valid(self, form): + def form_valid(self, form: Form) -> HttpResponseRedirect: + """Method for sending success message if form is valid.""" messages.success(self.request, "Team has been saved") return super().form_valid(form) -class FixIrcAclView(LoginRequiredMixin, CampViewMixin, UpdateView): +class FixIrcAclView(LoginRequiredMixin, CampViewMixin, IsPermissionMixin, UpdateView): + """View for fixing IRC ACL's.""" template_name = "fix_irc_acl.html" model = Team - fields = [] + fields = () slug_url_kwarg = "team_slug" - def dispatch(self, request, *args, **kwargs): + def dispatch(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """Method dispatch.""" # we need to call the super().dispatch() method early so self.camp gets populated by CampViewMixin, # because the lookups below depend on self.camp being set :) response = super().dispatch(request, *args, **kwargs) @@ -128,7 +149,8 @@ def dispatch(self, request, *args, **kwargs): if not request.user.profile.nickserv_username: messages.error( request, - "Please go to your profile and set your NickServ username first. Make sure the account is registered with NickServ first!", + "Please go to your profile and set your NickServ username first. " + "Make sure the account is registered with NickServ first!", ) return redirect( "teams:general", @@ -138,8 +160,8 @@ def dispatch(self, request, *args, **kwargs): return response - def get(self, request, *args, **kwargs): - # get membership + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """Get membership.""" try: TeamMember.objects.get( user=request.user, @@ -148,10 +170,10 @@ def get(self, request, *args, **kwargs): irc_channel_acl_ok=True, ) except TeamMember.DoesNotExist: - # this membership is already marked as membership.irc_channel_acl_ok=False, no need to do anything messages.error( request, - "No need, this membership is already marked as irc_channel_acl_ok=False, so the bot will fix the ACL soon", + "No need, this membership is already marked as irc_channel_acl_ok=False, " + "so the bot will fix the ACL soon", ) return redirect( "teams:general", @@ -161,7 +183,8 @@ def get(self, request, *args, **kwargs): return super().get(request, *args, **kwargs) - def form_valid(self, form): + def form_valid(self, form: Form) -> HttpResponseRedirect: + """Method for adding membership and returning a message.""" membership = TeamMember.objects.get( user=self.request.user, team=self.get_object(), @@ -173,7 +196,8 @@ def form_valid(self, form): membership.save() messages.success( self.request, - f"OK, hang on while we fix the permissions for your NickServ user '{self.request.user.profile.nickserv_username}' for IRC channel '{form.instance.irc_channel_name}'", + f"OK, hang on while we fix the permissions for your NickServ user " + f"'{self.request.user.profile.nickserv_username}' for IRC channel '{form.instance.irc_channel_name}'", ) return redirect( "teams:general", diff --git a/src/teams/views/guide.py b/src/teams/views/guide.py index bf31b02b5..ae86e53fb 100644 --- a/src/teams/views/guide.py +++ b/src/teams/views/guide.py @@ -8,9 +8,10 @@ from camps.mixins import CampViewMixin from teams.models import Team from teams.models import TeamMember +from utils.mixins import IsPermissionMixin -class TeamGuideView(LoginRequiredMixin, CampViewMixin, UserPassesTestMixin, DetailView): +class TeamGuideView(LoginRequiredMixin, CampViewMixin, UserPassesTestMixin, IsPermissionMixin, DetailView): """View for the team guide.""" template_name = "team_guide.html" context_object_name = "team" diff --git a/src/teams/views/info.py b/src/teams/views/info.py index 42e9819cc..52eb075df 100644 --- a/src/teams/views/info.py +++ b/src/teams/views/info.py @@ -1,5 +1,8 @@ +"""View for managing the team info pages.""" from __future__ import annotations +from typing import TYPE_CHECKING + from django.http import HttpResponseRedirect from django.views.generic import CreateView from django.views.generic import DeleteView @@ -10,16 +13,21 @@ from info.models import InfoCategory from info.models import InfoItem from teams.views.mixins import TeamInfopagerPermissionMixin +from utils.mixins import IsPermissionMixin from utils.widgets import MarkdownWidget from .mixins import TeamViewMixin +if TYPE_CHECKING: + from django.forms import Form class InfoCategoriesListView( TeamViewMixin, TeamInfopagerPermissionMixin, + IsPermissionMixin, ListView, ): + """Info Categories list view.""" model = InfoCategory template_name = "team_info_categories.html" slug_field = "anchor" @@ -29,20 +37,24 @@ class InfoCategoriesListView( class InfoItemCreateView( TeamViewMixin, TeamInfopagerPermissionMixin, + IsPermissionMixin, CreateView, ): + """Info item create view.""" model = InfoItem template_name = "team_info_item_form.html" - fields = ["headline", "body", "anchor", "weight"] + fields = ("headline", "body", "anchor", "weight") slug_field = "anchor" active_menu = "info_categories" - def get_form(self, *args, **kwargs): + def get_form(self, *args, **kwargs) -> Form: + """Method to update widget for body form element.""" form = super().get_form(*args, **kwargs) form.fields["body"].widget = MarkdownWidget() return form - def form_valid(self, form): + def form_valid(self, form: Form) -> HttpResponseRedirect: + """Method for updating the category field by the category_anchor.""" info_item = form.save(commit=False) category = InfoCategory.objects.get( team__camp=self.camp, @@ -52,10 +64,12 @@ def form_valid(self, form): info_item.save() return HttpResponseRedirect(self.get_success_url()) - def get_success_url(self): + def get_success_url(self) -> str: + """Method for creating the success/redirect url.""" return self.team.get_absolute_url() - def get_context_data(self, **kwargs): + def get_context_data(self, **kwargs) -> dict: + """Method for adding category to the context.""" context = super().get_context_data(**kwargs) context["category"] = InfoCategory.objects.get( team__camp__slug=self.kwargs["camp_slug"], @@ -68,24 +82,28 @@ class InfoItemUpdateView( TeamViewMixin, TeamInfopagerPermissionMixin, RevisionMixin, + IsPermissionMixin, UpdateView, ): + """Info item update view.""" model = InfoItem template_name = "team_info_item_form.html" - fields = ["headline", "body", "anchor", "weight"] + fields = ("headline", "body", "anchor", "weight") slug_field = "anchor" slug_url_kwarg = "item_anchor" active_menu = "info_categories" - def get_form(self, *args, **kwargs): + def get_form(self, *args, **kwargs) -> Form: + """Method to update widget for body form element.""" form = super().get_form(*args, **kwargs) form.fields["body"].widget = MarkdownWidget() return form - def get_success_url(self): - next = self.request.GET.get("next") - if next: - return next + def get_success_url(self) -> str: + """Method for creating the success/redirect url.""" + next_url = self.request.GET.get("next") + if next_url: + return next_url return self.team.get_absolute_url() @@ -93,16 +111,19 @@ class InfoItemDeleteView( TeamViewMixin, TeamInfopagerPermissionMixin, RevisionMixin, + IsPermissionMixin, DeleteView, ): + """View for deleting a info item.""" model = InfoItem template_name = "team_info_item_delete_confirm.html" slug_field = "anchor" slug_url_kwarg = "item_anchor" active_menu = "info_categories" - def get_success_url(self): - next = self.request.GET.get("next") - if next: - return next + def get_success_url(self) -> str: + """Method for creating the success/redirect url.""" + next_url = self.request.GET.get("next") + if next_url: + return next_url return self.team.get_absolute_url() diff --git a/src/teams/views/members.py b/src/teams/views/members.py index 36327b12e..2ddac48f9 100644 --- a/src/teams/views/members.py +++ b/src/teams/views/members.py @@ -1,6 +1,8 @@ +"""Views for team members.""" from __future__ import annotations import logging +from typing import TYPE_CHECKING from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin @@ -14,14 +16,21 @@ from teams.email import add_removed_membership_email from teams.models import Team from teams.models import TeamMember +from utils.mixins import IsPermissionMixin from .mixins import EnsureTeamMemberLeadMixin from .mixins import TeamViewMixin +if TYPE_CHECKING: + from django.forms import Form + from django.http import HttpRequest + from django.http import HttpResponse + from django.http import HttpResponseRedirect + logger = logging.getLogger(f"bornhack.{__name__}") -class TeamMembersView(CampViewMixin, DetailView): +class TeamMembersView(CampViewMixin, IsPermissionMixin, DetailView): """List view for team members.""" template_name = "team_members.html" context_object_name = "team" @@ -34,11 +43,12 @@ class TeamJoinView(LoginRequiredMixin, CampViewMixin, UpdateView): """View displayed when joining a team.""" template_name = "team_join.html" model = Team - fields = [] + fields = () slug_url_kwarg = "team_slug" active_menu = "members" - def get(self, request, *args, **kwargs) ->HttpResponse: + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """Get method view.""" if not Profile.objects.get(user=request.user).description: messages.warning( request, @@ -56,7 +66,8 @@ def get(self, request, *args, **kwargs) ->HttpResponse: return super().get(request, *args, **kwargs) - def form_valid(self, form): + def form_valid(self, form: Form) -> HttpResponseRedirect: + """Method to create team member and show message.""" TeamMember.objects.create(team=self.get_object(), user=self.request.user) messages.success( self.request, @@ -66,20 +77,23 @@ def form_valid(self, form): class TeamLeaveView(LoginRequiredMixin, CampViewMixin, UpdateView): + """View for leaving a team.""" template_name = "team_leave.html" model = Team - fields = [] + fields = () slug_url_kwarg = "team_slug" active_menu = "members" - def get(self, request, *args, **kwargs): + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """Get method for leaving a team.""" if request.user not in self.get_object().members.all(): messages.warning(request, "You are not a member of this team") return redirect("teams:list", camp_slug=self.get_object().camp.slug) return super().get(request, *args, **kwargs) - def form_valid(self, form): + def form_valid(self, form: Form) -> HttpResponseRedirect: + """Method deletes team member.""" TeamMember.objects.filter( team=self.get_object(), user=self.request.user, @@ -97,12 +111,14 @@ class TeamMemberRemoveView( EnsureTeamMemberLeadMixin, UpdateView, ): + """View for removing a team member.""" template_name = "teammember_remove.html" model = TeamMember - fields = [] + fields = () active_menu = "members" - def form_valid(self, form): + def form_valid(self, form: Form) -> HttpResponseRedirect: + """Method to delete instance and show message..""" form.instance.delete() if add_removed_membership_email(form.instance): messages.success(self.request, "Team member removed") @@ -127,12 +143,14 @@ class TeamMemberApproveView( EnsureTeamMemberLeadMixin, UpdateView, ): + """View to approve team member.""" template_name = "teammember_approve.html" model = TeamMember - fields = [] + fields = () active_menu = "members" - def form_valid(self, form): + def form_valid(self, form: Form) -> HttpResponseRedirect: + """Method to set approve true and show message..""" form.instance.approved = True form.instance.save() if add_added_membership_email(form.instance): diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py index a7e34b488..c5ddeeab6 100644 --- a/src/teams/views/shifts.py +++ b/src/teams/views/shifts.py @@ -24,6 +24,9 @@ from teams.models import Team from teams.models import TeamMember from teams.models import TeamShift +from utils.mixins import IsPermissionMixin + +from .mixins import EnsureTeamLeadMixin if TYPE_CHECKING: from django.db.models import QuerySet @@ -33,7 +36,7 @@ from camps.models import Camp -class ShiftListView(LoginRequiredMixin, CampViewMixin, ListView): +class ShiftListView(LoginRequiredMixin, CampViewMixin, IsPermissionMixin, ListView): """Shift list view.""" model = TeamShift template_name = "team_shift_list.html" @@ -94,7 +97,7 @@ class Meta: model = TeamShift fields = ("from_datetime", "to_datetime", "people_required") - def __init__(self, instance: dict|None=None, **kwargs) -> None: + def __init__(self, instance: TeamShift|None=None, **kwargs) -> None: """Method for setting up the form.""" camp = kwargs.pop("camp") super().__init__(instance=instance, **kwargs) @@ -125,11 +128,17 @@ def _get_to_datetime(self) -> dict: return self.cleaned_data["to_datetime"].astimezone(current_timezone) def clean(self) -> None: - """Method for cleaning the form data and check lower is bigger then upper.""" + """Method for cleaning the form data. + + Check lower is bigger then upper + Check lower and upper are not the same + """ self.lower = self._get_from_datetime() self.upper = self._get_to_datetime() if self.lower > self.upper: raise forms.ValidationError("Start can not be after end.") + if self.lower == self.upper: + raise forms.ValidationError("Start can not be the same as end.") def save(self, commit=True) -> TeamShift: """Method for saving shift_range from self.lower and self.upper.""" @@ -138,7 +147,7 @@ def save(self, commit=True) -> TeamShift: return super().save(commit=commit) -class ShiftCreateView(LoginRequiredMixin, CampViewMixin, CreateView): +class ShiftCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsPermissionMixin, CreateView): """View for creating a single shift.""" model = TeamShift template_name = "team_shift_form.html" @@ -171,7 +180,7 @@ def get_success_url(self) -> str: return reverse("teams:shifts", kwargs=self.kwargs) -class ShiftUpdateView(LoginRequiredMixin, CampViewMixin, UpdateView): +class ShiftUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsPermissionMixin, UpdateView): """View for updating a single shift.""" model = TeamShift template_name = "team_shift_form.html" @@ -196,7 +205,7 @@ def get_success_url(self) -> str: return reverse("teams:shifts", kwargs=self.kwargs) -class ShiftDeleteView(LoginRequiredMixin, CampViewMixin, DeleteView): +class ShiftDeleteView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsPermissionMixin, DeleteView): """View for deleting a shift.""" model = TeamShift template_name = "team_shift_confirm_delete.html" @@ -236,7 +245,7 @@ def __init__(self, instance: dict|None=None, **kwargs) -> None: people_required = forms.IntegerField() -class ShiftCreateMultipleView(LoginRequiredMixin, CampViewMixin, FormView): +class ShiftCreateMultipleView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsPermissionMixin, FormView): """View for creating multiple shifts.""" template_name = "team_shift_form.html" form_class = MultipleShiftForm diff --git a/src/teams/views/tasks.py b/src/teams/views/tasks.py index 8d433c8e7..9546f832a 100644 --- a/src/teams/views/tasks.py +++ b/src/teams/views/tasks.py @@ -17,6 +17,7 @@ from teams.models import Team from teams.models import TeamMember from teams.models import TeamTask +from utils.mixins import IsPermissionMixin from .mixins import TeamTaskerPermissionMixin from .mixins import TeamViewMixin @@ -24,7 +25,7 @@ if TYPE_CHECKING: from django.http import HttpRequest -class TeamTasksView(CampViewMixin, DetailView): +class TeamTasksView(CampViewMixin, IsPermissionMixin, DetailView): """List view of the team tasks.""" template_name = "team_tasks.html" context_object_name = "team" @@ -41,7 +42,7 @@ class Meta: fields = ("comment",) -class TaskDetailView(TeamViewMixin, DetailView): +class TaskDetailView(TeamViewMixin, IsPermissionMixin, DetailView): """Task detail view.""" template_name = "task_detail.html" context_object_name = "task" @@ -92,6 +93,7 @@ class TaskCreateView( LoginRequiredMixin, TeamViewMixin, TeamTaskerPermissionMixin, + IsPermissionMixin, CreateView, ): """View for creating a team task.""" @@ -125,6 +127,7 @@ class TaskUpdateView( LoginRequiredMixin, TeamViewMixin, TeamTaskerPermissionMixin, + IsPermissionMixin, UpdateView, ): """Update task view used for updating tasks.""" diff --git a/src/utils/bootstrap/base.py b/src/utils/bootstrap/base.py index 82b7c8a4f..d2c5bfcc7 100644 --- a/src/utils/bootstrap/base.py +++ b/src/utils/bootstrap/base.py @@ -1469,6 +1469,7 @@ def create_camp_team_memberships(self, camp: Camp, teams: dict, users: dict) -> approved=True, lead=True, ) + memberships["noc"]["user4"].save() memberships["noc"]["user1"] = TeamMember.objects.create( team=teams["noc"], user=users[1], @@ -1620,6 +1621,11 @@ def create_camp_info_categories(self, camp: Camp, teams: dict) -> dict: headline="Where do I sleep?", anchor="sleep", ) + categories["noc"] = InfoCategory.objects.create( + team=teams["noc"], + headline="Where do I plug in?", + anchor="noc", + ) return categories @@ -1681,6 +1687,12 @@ def create_camp_info_items(self, camp: Camp, categories: dict) -> None: "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" + ) def create_camp_feedback(self, camp: Camp, users: dict[User]) -> None: """Create camp feedback.""" @@ -2139,13 +2151,14 @@ def bootstrap_tests(self) -> None: }, ] self.create_camps(camps) - self.create_users(2) + self.create_users(16) self.create_event_types() teams = {} for camp, read_only in self.camps: year = camp.camp.lower.year teams[year] = self.create_camp_teams(camp) + self.create_camp_team_memberships(camp, teams[year], self.users) camp.read_only = read_only camp.call_for_participation_open = not read_only @@ -2154,6 +2167,8 @@ def bootstrap_tests(self) -> None: self.camp = self.camps[1][0] self.teams = teams[self.camp.camp.lower.year] + for member in TeamMember.objects.filter(team__camp=self.camp): + member.save() def bootstrap_camp(self, options: dict) -> None: """Bootstrap camp related entities.""" @@ -2272,6 +2287,12 @@ def bootstrap_camp(self, options: dict) -> None: camp.call_for_participation_open = not read_only camp.call_for_sponsors_open = not read_only camp.save() + + # Update team permissions. + if camp.camp.lower.year == settings.UPCOMING_CAMP_YEAR: + for member in TeamMember.objects.filter(team__camp=camp): + member.save() + def bootstrap_base(self, options: dict) -> None: """Bootstrap the data for the application.""" diff --git a/src/utils/mixins.py b/src/utils/mixins.py index cd6c80468..8184c5912 100644 --- a/src/utils/mixins.py +++ b/src/utils/mixins.py @@ -3,6 +3,8 @@ import logging from typing import TYPE_CHECKING +from django.conf import settings + from django.contrib import messages from django.contrib.auth.mixins import PermissionRequiredMixin from django.contrib.auth.mixins import UserPassesTestMixin @@ -37,6 +39,23 @@ class RaisePermissionRequiredMixin(PermissionRequiredMixin): raise_exception = True +class IsPermissionMixin: + """Mixing for adding is_team_{perm} to context""" + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + perms = self.request.user.get_all_permissions() + # add bools for each of settings.BORNHACK_TEAM_PERMISSIONS + for perm in settings.BORNHACK_TEAM_PERMISSIONS: + # loop over user permissions and set context + for user_perm in perms: + if user_perm.startswith("camps.") and user_perm.endswith( + f"_team_{perm}", + ): + context[f"is_team_{perm}"] = True + break + else: + context[f"is_team_{perm}"] = False + return context class BaseTeamPermRequiredMixin: """Base class for TeamRequiredMixins.""" From 0e4bae809c1d9e73c19cdd50730cbec5952b7bd0 Mon Sep 17 00:00:00 2001 From: "Rudy (zarya)" Date: Sun, 18 May 2025 12:33:38 +0200 Subject: [PATCH 05/10] WIP: more linting and tests --- src/teams/apps.py | 3 + src/teams/factories.py | 3 + src/teams/models.py | 110 +++++++++++++++++++-------- src/teams/templatetags/__init__.py | 1 + src/teams/templatetags/teams_tags.py | 16 +++- src/teams/tests/test_base_views.py | 57 ++++++++++++++ src/teams/utils.py | 3 +- 7 files changed, 157 insertions(+), 36 deletions(-) create mode 100644 src/teams/tests/test_base_views.py diff --git a/src/teams/apps.py b/src/teams/apps.py index b5b2f0bc3..807fd1171 100644 --- a/src/teams/apps.py +++ b/src/teams/apps.py @@ -1,3 +1,4 @@ +"""App config for the teams application.""" from __future__ import annotations from django.apps import AppConfig @@ -10,9 +11,11 @@ class TeamsConfig(AppConfig): + """App config for the signals connected to the teams application.""" name = "teams" def ready(self) -> None: + """Method to connect the signals.""" # connect the post_save signal, always including a dispatch_uid to prevent it being called multiple times in corner cases post_save.connect( teammember_saved, diff --git a/src/teams/factories.py b/src/teams/factories.py index 55bad0f11..b9cb20df6 100644 --- a/src/teams/factories.py +++ b/src/teams/factories.py @@ -1,3 +1,4 @@ +"""Factories for the teams application.""" from __future__ import annotations import factory @@ -6,7 +7,9 @@ class TeamFactory(factory.django.DjangoModelFactory): + """Team Factory for bootstrapping data.""" class Meta: + """Meta.""" model = Team camp = factory.SubFactory("camps.factories.CampFactory") diff --git a/src/teams/models.py b/src/teams/models.py index a74ec638c..f16b34191 100644 --- a/src/teams/models.py +++ b/src/teams/models.py @@ -1,6 +1,8 @@ +"""All models for teams application.""" from __future__ import annotations import logging +from typing import TYPE_CHECKING from django.conf import settings from django.contrib.auth.models import Group @@ -18,6 +20,13 @@ from utils.models import UUIDModel from utils.slugs import unique_slugify +if TYPE_CHECKING: + from typing import ClassVar + + from django.db.models import QuerySet + + from camps.models import Camp + logger = logging.getLogger(f"bornhack.{__name__}") @@ -46,6 +55,7 @@ class Team(ExportModelOperationsMixin("team"), CampRelatedModel): + """Model for team.""" camp = models.ForeignKey( "camps.Camp", related_name="teams", @@ -104,19 +114,23 @@ class Team(ExportModelOperationsMixin("team"), CampRelatedModel): null=True, unique=True, max_length=50, - help_text="The public IRC channel for this team. Will be shown on the team page so people know how to reach the team. Leave empty if the team has no public IRC channel.", + help_text="The public IRC channel for this team. Will be shown on the team page so people know " + "how to reach the team. Leave empty if the team has no public IRC channel.", ) public_irc_channel_bot = models.BooleanField( default=False, - help_text="Check to make the bot join the teams public IRC channel. Leave unchecked to disable the IRC bot for this channel.", + help_text="Check to make the bot join the teams public IRC channel. " + "Leave unchecked to disable the IRC bot for this channel.", ) public_irc_channel_managed = models.BooleanField( default=False, - help_text="Check to make the bot manage the teams public IRC channel by registering it with NickServ and setting +Oo for all teammembers.", + help_text="Check to make the bot manage the teams public IRC channel by registering it with NickServ " + "and setting +Oo for all teammembers.", ) public_irc_channel_fix_needed = models.BooleanField( default=False, - help_text="Used to indicate to the IRC bot that this teams public IRC channel is in need of a permissions and ACL fix.", + help_text="Used to indicate to the IRC bot that this teams public IRC channel is in need of a " + "permissions and ACL fix.", ) private_irc_channel_name = models.CharField( @@ -124,19 +138,23 @@ class Team(ExportModelOperationsMixin("team"), CampRelatedModel): null=True, unique=True, max_length=50, - help_text="The private IRC channel for this team. Will be shown to team members on the team page. Leave empty if the team has no private IRC channel.", + help_text="The private IRC channel for this team. Will be shown to team members on the team page. " + "Leave empty if the team has no private IRC channel.", ) private_irc_channel_bot = models.BooleanField( default=False, - help_text="Check to make the bot join the teams private IRC channel. Leave unchecked to disable the IRC bot for this channel.", + help_text="Check to make the bot join the teams private IRC channel. " + "Leave unchecked to disable the IRC bot for this channel.", ) private_irc_channel_managed = models.BooleanField( default=False, - help_text="Check to make the bot manage the private IRC channel by registering it with NickServ, setting +I and maintaining the ACL.", + help_text="Check to make the bot manage the private IRC channel by registering it with NickServ, " + "setting +I and maintaining the ACL.", ) private_irc_channel_fix_needed = models.BooleanField( default=False, - help_text="Used to indicate to the IRC bot that this teams private IRC channel is in need of a permissions and ACL fix.", + help_text="Used to indicate to the IRC bot that this teams private IRC channel is in need of a " + "permissions and ACL fix.", ) # Signal @@ -149,7 +167,8 @@ class Team(ExportModelOperationsMixin("team"), CampRelatedModel): ) class Meta: - ordering = ["name"] + """Meta.""" + ordering: ClassVar[list[str]] = ["name"] unique_together = (("name", "camp"), ("slug", "camp")) guide = models.TextField( @@ -160,15 +179,18 @@ class Meta: ) def __str__(self) -> str: + """Method to return a str of the model.""" return f"{self.name} ({self.camp})" - def get_absolute_url(self): + def get_absolute_url(self) -> str: + """Method to return the absolute URL.""" return reverse_lazy( "teams:general", kwargs={"camp_slug": self.camp.slug, "team_slug": self.slug}, ) def save(self, **kwargs) -> None: + """Method for generating slugs and add groups if needed.""" # generate slug if needed if not self.slug: self.slug = unique_slugify( @@ -192,6 +214,7 @@ def save(self, **kwargs) -> None: super().save(**kwargs) def clean(self) -> None: + """Method for cleaning data.""" # make sure the public irc channel name is prefixed with a # if it is set if self.public_irc_channel_name and self.public_irc_channel_name[0] != "#": self.public_irc_channel_name = f"#{self.public_irc_channel_name}" @@ -206,7 +229,8 @@ def clean(self) -> None: if self.private_irc_channel_name in (settings.IRCBOT_PUBLIC_CHANNEL, settings.IRCBOT_VOLUNTEER_CHANNEL): raise ValidationError("The private IRC channel name is reserved") - # make sure public_irc_channel_name is not in use as public or private irc channel for another team, case insensitive + # make sure public_irc_channel_name is not in use as public or private irc channel for another team, + # case insensitive if self.public_irc_channel_name and ( Team.objects.filter( private_irc_channel_name__iexact=self.public_irc_channel_name, @@ -223,7 +247,8 @@ def clean(self) -> None: "The public IRC channel name is already in use on another team!", ) - # make sure private_irc_channel_name is not in use as public or private irc channel for another team, case insensitive + # make sure private_irc_channel_name is not in use as public or private irc channel for another team, + # case insensitive if self.private_irc_channel_name and ( Team.objects.filter( private_irc_channel_name__iexact=self.private_irc_channel_name, @@ -241,26 +266,28 @@ def clean(self) -> None: ) @property - def memberships(self): + def memberships(self) -> QuerySet: """Returns all TeamMember objects for this team. + Use self.members.all() to get User objects for all members, or use self.memberships.all() to get TeamMember objects for all members. """ return TeamMember.objects.filter(team=self) @property - def approved_members(self): + def approved_members(self) -> QuerySet: """Returns only approved members (returns User objects, not TeamMember objects).""" return self.members.filter(teammember__approved=True) @property - def unapproved_members(self): + def unapproved_members(self) -> QuerySet: """Returns only unapproved members (returns User objects, not TeamMember objects).""" return self.members.filter(teammember__approved=False) @property - def leads(self): - """Return only approved team leads + def leads(self) -> QuerySet: + """Return only approved team leads. + Used to handle permissions for team leads. """ return self.members.filter( @@ -269,9 +296,9 @@ def leads(self): ) @property - def regular_members(self): - """Return only approved and not lead members with - an approved public_credit_name. + def regular_members(self) -> QuerySet: + """Return only approved and not lead members with an approved public_credit_name. + Used on the people pages. """ return self.members.filter( @@ -280,10 +307,8 @@ def regular_members(self): ) @property - def unnamed_members(self): - """Returns only approved and not team lead members, - without an approved public_credit_name. - """ + def unnamed_members(self) -> QuerySet: + """Returns only approved and not team lead members, without an approved public_credit_name.""" return self.members.filter( teammember__approved=True, teammember__lead=False, @@ -292,34 +317,42 @@ def unnamed_members(self): @property def member_permission_set(self) -> str: + """Method for returning the team member permission set.""" return f"camps.{self.slug}_team_member" @property def mapper_permission_set(self) -> str: + """Method for returning the mapper permission set.""" return f"camps.{self.slug}_team_mapper" @property def facilitator_permission_set(self) -> str: + """Method for returning the facilitator permission set.""" return f"camps.{self.slug}_team_facilitator" @property def lead_permission_set(self) -> str: + """Method for returning the team lead permission set.""" return f"camps.{self.slug}_team_lead" @property def pos_permission_set(self) -> str: + """Method for returning the pos permission set.""" return f"camps.{self.slug}_team_pos" @property def infopager_permission_set(self) -> str: + """Method for returning the infopager permission set.""" return f"camps.{self.slug}_team_infopager" @property def tasker_permission_set(self) -> str: + """Method for returning the tasker permission set.""" return f"camps.{self.slug}_team_tasker" class TeamMember(ExportModelOperationsMixin("team_member"), CampRelatedModel): + """Model for team member.""" user = models.ForeignKey( "auth.User", on_delete=models.PROTECT, @@ -344,13 +377,17 @@ class TeamMember(ExportModelOperationsMixin("team_member"), CampRelatedModel): irc_acl_fix_needed = models.BooleanField( default=False, - help_text="Maintained by the IRC bot, manual editing should not be needed. Will be set to true when a teammember sets or changes NickServ username, and back to false after the ACL has been fixed by the bot.", + help_text="Maintained by the IRC bot, manual editing should not be needed. " + "Will be set to true when a teammember sets or changes NickServ username, " + "and back to false after the ACL has been fixed by the bot.", ) class Meta: - ordering = ["-lead", "-approved"] + """Meta.""" + ordering: ClassVar[list[str]] = ["-lead", "-approved"] def __str__(self) -> str: + """Method for returning str of model.""" return "{} is {} {} member of team {}".format( self.user, "" if self.approved else "an unapproved", @@ -359,7 +396,7 @@ def __str__(self) -> str: ) @property - def camp(self): + def camp(self) -> Camp: """All CampRelatedModels must have a camp FK or a camp property.""" return self.team.camp @@ -399,6 +436,7 @@ def update_team_lead_permissions(self, deleted=False) -> None: class TeamTask(ExportModelOperationsMixin("team_task"), CampRelatedModel): + """Model for team tasks.""" team = models.ForeignKey( "teams.Team", related_name="tasks", @@ -425,10 +463,12 @@ class TeamTask(ExportModelOperationsMixin("team_task"), CampRelatedModel): ) class Meta: - ordering = ["completed", "when", "name"] + """Meta.""" + ordering: ClassVar[list[str]] = ["completed", "when", "name"] unique_together = (("name", "team"), ("slug", "team")) - def get_absolute_url(self): + def get_absolute_url(self) -> str: + """Get the absolute URL for this model.""" return reverse_lazy( "teams:task_detail", kwargs={ @@ -439,13 +479,14 @@ def get_absolute_url(self): ) @property - def camp(self): + def camp(self) -> Camp: """All CampRelatedModels must have a camp FK or a camp property.""" return self.team.camp camp_filter = "team__camp" def save(self, **kwargs) -> None: + """Method for generating the slug if needed.""" # generate slug if needed if not self.slug: self.slug = unique_slugify( @@ -463,6 +504,7 @@ class TaskComment( UUIDModel, CreatedUpdatedModel, ): + """Model for task comments.""" task = models.ForeignKey( "teams.TeamTask", on_delete=models.PROTECT, @@ -473,7 +515,9 @@ class TaskComment( class TeamShift(ExportModelOperationsMixin("team_shift"), CampRelatedModel): + """Model for team shifts.""" class Meta: + """Meta.""" ordering = ("shift_range",) team = models.ForeignKey( @@ -490,15 +534,17 @@ class Meta: people_required = models.IntegerField(default=1) @property - def camp(self): + def camp(self) -> Camp: """All CampRelatedModels must have a camp FK or a camp property.""" return self.team.camp camp_filter = "team__camp" def __str__(self) -> str: + """Method for returning a string of this model.""" return f"{self.team.name} team shift from {self.shift_range.lower} to {self.shift_range.upper}" @property - def users(self): + def users(self) -> list[TeamMember]: + """Returns a list of team members on this shift.""" return [member.user for member in self.team_members.all()] diff --git a/src/teams/templatetags/__init__.py b/src/teams/templatetags/__init__.py index e69de29bb..5787d4d5a 100644 --- a/src/teams/templatetags/__init__.py +++ b/src/teams/templatetags/__init__.py @@ -0,0 +1 @@ +"""All template tags for teams.""" diff --git a/src/teams/templatetags/teams_tags.py b/src/teams/templatetags/teams_tags.py index 1be20bb8b..4ab695062 100644 --- a/src/teams/templatetags/teams_tags.py +++ b/src/teams/templatetags/teams_tags.py @@ -1,20 +1,30 @@ +"""Template tags for teams.""" from __future__ import annotations +from typing import TYPE_CHECKING + from django import template from django.utils.safestring import mark_safe from teams.models import TeamMember +if TYPE_CHECKING: + from django.contrib.auth.models import User + + from teams.models import Team + register = template.Library() @register.filter -def is_team_member(user, team): +def is_team_member(user: User, team: Team) -> bool: + """Template tag to return team member status.""" return TeamMember.objects.filter(team=team, user=user, approved=True).exists() @register.simple_tag -def membershipstatus(user, team, showicon=False): +def membershipstatus(user: User, team: Team, showicon: bool=False) -> str: + """Template tag to return membership status.""" if user in team.leads.all(): text = "Lead" icon = "fa-star" @@ -29,5 +39,5 @@ def membershipstatus(user, team, showicon=False): icon = "fa-times" if showicon: - return mark_safe(f"") + return mark_safe(f"") # noqa: S308 return text diff --git a/src/teams/tests/test_base_views.py b/src/teams/tests/test_base_views.py new file mode 100644 index 000000000..b49652fc0 --- /dev/null +++ b/src/teams/tests/test_base_views.py @@ -0,0 +1,57 @@ +"""Test cases for the base views of the teams application.""" + +from __future__ import annotations + +from django.contrib.auth.models import Permission +from django.contrib.contenttypes.models import ContentType +from django.urls import reverse + +from camps.models import Permission as CampPermission +from utils.tests import BornhackTestBase + + +class TeamBaseViewTest(BornhackTestBase): + """Test Team Base Views.""" + + @classmethod + def setUpTestData(cls) -> None: + """Setup test data.""" + # first add users and other basics + super().setUpTestData() + + cls.categories = cls.bootstrap.create_camp_info_categories(camp=cls.camp, teams=cls.teams) + cls.bootstrap.create_camp_info_items(camp=cls.camp,categories=cls.categories) + + permission_content_type = ContentType.objects.get_for_model(CampPermission) + cls.users[4].user_permissions.add( + Permission.objects.get( + content_type=permission_content_type, + codename="noc_team_lead", + ), + ) + def test_team_general_view(self) -> None: + """Test the team general view.""" + url = reverse("teams:general", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(path=url) + assert response.status_code == 200 + + def test_team_list_view(self) -> None: + """Test the team list view.""" + url = reverse("teams:list", kwargs={ + "camp_slug": self.camp.slug, + }) + response = self.client.get(path=url) + assert response.status_code == 200 + + def test_team_manage_view(self) -> None: + """Test the team manage view.""" + self.client.force_login(self.users[4]) + url = reverse("teams:manage", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(path=url) + assert response.status_code == 200 diff --git a/src/teams/utils.py b/src/teams/utils.py index 6013f52ee..15b45a278 100644 --- a/src/teams/utils.py +++ b/src/teams/utils.py @@ -1,9 +1,10 @@ +"""Utils for the teams application.""" from __future__ import annotations from .models import Team -def get_team_from_irc_channel(channel): +def get_team_from_irc_channel(channel: str) -> Team|bool: """Returns a Team object given an IRC channel name, if possible.""" if not channel: return False From 076d0b99c80c732c36d7f9308da0a180671408ab Mon Sep 17 00:00:00 2001 From: "Rudy (zarya)" Date: Sun, 18 May 2025 20:45:05 +0200 Subject: [PATCH 06/10] Most of the linting and test writing done --- src/teams/__init__.py | 1 + src/teams/admin.py | 37 +++++++--- src/teams/apps.py | 3 +- src/teams/email.py | 14 +++- src/teams/exceptions.py | 39 ++++++++++ src/teams/models.py | 16 ++--- src/teams/signal_handlers.py | 18 +++-- src/teams/tests/test_base_views.py | 110 ++++++++++++++++++++++++++++- src/teams/views/base.py | 4 +- src/teams/views/shifts.py | 8 ++- 10 files changed, 214 insertions(+), 36 deletions(-) create mode 100644 src/teams/exceptions.py diff --git a/src/teams/__init__.py b/src/teams/__init__.py index e69de29bb..d13152713 100644 --- a/src/teams/__init__.py +++ b/src/teams/__init__.py @@ -0,0 +1 @@ +"""Teams application.""" diff --git a/src/teams/admin.py b/src/teams/admin.py index 27d87defa..9b661855d 100644 --- a/src/teams/admin.py +++ b/src/teams/admin.py @@ -1,5 +1,14 @@ +"""Django admin for teams.""" from __future__ import annotations +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import ClassVar + + from django.db.models import QuerySet + from django.http import HttpRequest + from django.contrib import admin from camps.utils import CampPropertyListFilter @@ -14,26 +23,30 @@ @admin.register(TeamTask) class TeamTaskAdmin(admin.ModelAdmin): - list_display = ["id", "team", "name", "description"] + """Django admin for team tasks.""" + list_display: ClassVar[list[str]] = ["id", "team", "name", "description"] class TeamMemberInline(admin.TabularInline): + """Django admin inline field for team member.""" model = TeamMember @admin.register(Team) class TeamAdmin(admin.ModelAdmin): + """Django admin for teams.""" save_as = True @admin.display( description="Leads", ) - def get_leads(self, obj): + def get_leads(self, obj: Team) -> str: + """Method to return team leads.""" return ", ".join( [lead.profile.public_credit_name for lead in obj.leads.all()], ) - list_display = [ + list_display: ClassVar[list[str]] = [ "name", "camp", "get_leads", @@ -46,7 +59,7 @@ def get_leads(self, obj): "private_irc_channel_managed", ] - list_filter = [ + list_filter: ClassVar[list[str]] = [ CampPropertyListFilter, "needs_members", "public_irc_channel_bot", @@ -54,16 +67,18 @@ def get_leads(self, obj): "private_irc_channel_bot", "private_irc_channel_managed", ] - inlines = [TeamMemberInline] + inlines: ClassVar[list] = [TeamMemberInline] @admin.register(TeamMember) class TeamMemberAdmin(admin.ModelAdmin): - list_filter = [CampPropertyListFilter, "team", "approved"] + """Django admin for team members.""" + list_filter: ClassVar[list] = [CampPropertyListFilter, "team", "approved"] - actions = ["approve_membership", "remove_member"] + actions: ClassVar[list[str]] = ["approve_membership", "remove_member"] - def approve_membership(self, request, queryset) -> None: + def approve_membership(self, request: HttpRequest, queryset: QuerySet) -> None: + """Method for approving team membership status.""" teams_count = queryset.values("team").distinct().count() updated = 0 @@ -80,7 +95,8 @@ def approve_membership(self, request, queryset) -> None: approve_membership.description = "Approve membership." - def remove_member(self, request, queryset) -> None: + def remove_member(self, request: HttpRequest, queryset: QuerySet) -> None: + """Method for removing team membership status.""" teams_count = queryset.values("team").distinct().count() updated = 0 @@ -99,4 +115,5 @@ def remove_member(self, request, queryset) -> None: @admin.register(TeamShift) class TeamShiftAdmin(admin.ModelAdmin): - list_filter = ["team"] + """Django admin for team shifts.""" + list_filter: ClassVar[list[str]] = ["team"] diff --git a/src/teams/apps.py b/src/teams/apps.py index 807fd1171..ca9b77e7c 100644 --- a/src/teams/apps.py +++ b/src/teams/apps.py @@ -16,7 +16,8 @@ class TeamsConfig(AppConfig): def ready(self) -> None: """Method to connect the signals.""" - # connect the post_save signal, always including a dispatch_uid to prevent it being called multiple times in corner cases + # connect the post_save signal, always including a dispatch_uid to prevent + # it being called multiple times in corner cases post_save.connect( teammember_saved, sender="teams.TeamMember", diff --git a/src/teams/email.py b/src/teams/email.py index 84b01d352..8bcc4c49f 100644 --- a/src/teams/email.py +++ b/src/teams/email.py @@ -1,13 +1,19 @@ +"""Email functions of teams application.""" from __future__ import annotations import logging +from typing import TYPE_CHECKING from utils.email import add_outgoing_email +if TYPE_CHECKING: + from django.forms import Form + logger = logging.getLogger(f"bornhack.{__name__}") -def add_added_membership_email(membership): +def add_added_membership_email(membership: Form) -> bool: + """Method to send email when team membership added.""" formatdict = {"team": membership.team.name, "camp": membership.team.camp.title} return add_outgoing_email( @@ -21,7 +27,8 @@ def add_added_membership_email(membership): ) -def add_removed_membership_email(membership): +def add_removed_membership_email(membership: Form) -> bool: + """Method to send email when team membership removed.""" formatdict = {"team": membership.team.name, "camp": membership.team.camp.title} if membership.approved: @@ -42,7 +49,8 @@ def add_removed_membership_email(membership): ) -def add_new_membership_email(membership): +def add_new_membership_email(membership: Form) -> bool: + """Method to send email when team membership requested.""" formatdict = { "team": membership.team.name, "camp": membership.team.camp.title, diff --git a/src/teams/exceptions.py b/src/teams/exceptions.py new file mode 100644 index 000000000..2e188218f --- /dev/null +++ b/src/teams/exceptions.py @@ -0,0 +1,39 @@ +"""Exceptions for phonebook.""" + +from __future__ import annotations + +import logging + +from django.core.exceptions import ValidationError + +logger = logging.getLogger(f"bornhack.{__name__}") + + +class ReservedIrcNameError(ValidationError): + """Exception raised on reserved irc name.""" + + def __init__(self) -> None: + """Exception raised on reserved irc name.""" + super().__init__("The public IRC channel name is reserved") + + +class IrcChannelInUseError(ValidationError): + """Exception raised a public irc channel is in use.""" + + def __init__(self) -> None: + """Exception raised a public irc channel is in use.""" + super().__init__("The public IRC channel name is already in use on another team!") + +class StartAfterEndError(ValidationError): + """Exception raised when start date is after end date.""" + + def __init__(self) -> None: + """Exception raised when start date is after end date.""" + super().__init__("Start can not be after end.") + +class StartSameAsEndError(ValidationError): + """Exception raised when start date is the same as end date.""" + + def __init__(self) -> None: + """Exception raised when start date is the same as end date.""" + super().__init__("Start can not be the same as end.") diff --git a/src/teams/models.py b/src/teams/models.py index f16b34191..0bf12a87c 100644 --- a/src/teams/models.py +++ b/src/teams/models.py @@ -9,7 +9,6 @@ from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.contrib.postgres.fields import DateTimeRangeField -from django.core.exceptions import ValidationError from django.db import models from django.urls import reverse_lazy from django_prometheus.models import ExportModelOperationsMixin @@ -20,6 +19,9 @@ from utils.models import UUIDModel from utils.slugs import unique_slugify +from .exceptions import IrcChannelInUseError +from .exceptions import ReservedIrcNameError + if TYPE_CHECKING: from typing import ClassVar @@ -225,9 +227,9 @@ def clean(self) -> None: # make sure the channel names are not reserved if self.public_irc_channel_name in (settings.IRCBOT_PUBLIC_CHANNEL, settings.IRCBOT_VOLUNTEER_CHANNEL): - raise ValidationError("The public IRC channel name is reserved") + raise ReservedIrcNameError if self.private_irc_channel_name in (settings.IRCBOT_PUBLIC_CHANNEL, settings.IRCBOT_VOLUNTEER_CHANNEL): - raise ValidationError("The private IRC channel name is reserved") + raise ReservedIrcNameError # make sure public_irc_channel_name is not in use as public or private irc channel for another team, # case insensitive @@ -243,9 +245,7 @@ def clean(self) -> None: .exclude(pk=self.pk) .exists() ): - raise ValidationError( - "The public IRC channel name is already in use on another team!", - ) + raise IrcChannelInUseError # make sure private_irc_channel_name is not in use as public or private irc channel for another team, # case insensitive @@ -261,9 +261,7 @@ def clean(self) -> None: .exclude(pk=self.pk) .exists() ): - raise ValidationError( - "The private IRC channel name is already in use on another team!", - ) + raise IrcChannelInUseError @property def memberships(self) -> QuerySet: diff --git a/src/teams/signal_handlers.py b/src/teams/signal_handlers.py index b26e39253..1fdc58c5c 100644 --- a/src/teams/signal_handlers.py +++ b/src/teams/signal_handlers.py @@ -1,13 +1,21 @@ +"""Signals for the teams application.""" from __future__ import annotations import logging +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from django.contrib.auth.models import User + + from teams.models import TeamMember + from django.conf import settings logger = logging.getLogger(f"bornhack.{__name__}") -def teammember_saved(sender, instance, created, **kwargs) -> None: +def teammember_saved(sender: User, instance: TeamMember, created: bool, **_kwargs) -> None: """This signal handler is called whenever a TeamMember instance is saved.""" # if this is a new unapproved teammember send a mail to team leads if created and not instance.approved: @@ -24,14 +32,14 @@ def teammember_saved(sender, instance, created, **kwargs) -> None: instance.update_team_lead_permissions() -def teammember_deleted(sender, instance, **kwargs) -> None: +def teammember_deleted(sender: User, instance: TeamMember, **_kwargs) -> None: """This signal handler is called whenever a TeamMember instance is deleted.""" if instance.team.private_irc_channel_name and instance.team.private_irc_channel_managed: - # TODO: remove user from private channel ACL + # TODO(tyk): remove user from private channel ACL pass if instance.team.public_irc_channel_name and instance.team.public_irc_channel_managed: - # TODO: remove user from public channel ACL + # TODO(tyk): remove user from public channel ACL pass # make sure the teams group membership is uptodate @@ -39,7 +47,7 @@ def teammember_deleted(sender, instance, **kwargs) -> None: instance.update_team_lead_permissions(deleted=True) -def team_saved(sender, instance, created, **kwargs) -> None: +def team_saved(sender: User, instance: TeamMember, created: bool, **_kwargs) -> None: """This signal handler is called whenever a Team instance is saved.""" # late imports from django.contrib.auth.models import Permission diff --git a/src/teams/tests/test_base_views.py b/src/teams/tests/test_base_views.py index b49652fc0..98f4f969f 100644 --- a/src/teams/tests/test_base_views.py +++ b/src/teams/tests/test_base_views.py @@ -1,17 +1,19 @@ -"""Test cases for the base views of the teams application.""" +"""Test cases for the base and member views of the teams application.""" from __future__ import annotations +from bs4 import BeautifulSoup from django.contrib.auth.models import Permission from django.contrib.contenttypes.models import ContentType from django.urls import reverse from camps.models import Permission as CampPermission +from teams.models import TeamMember from utils.tests import BornhackTestBase -class TeamBaseViewTest(BornhackTestBase): - """Test Team Base Views.""" +class TeamBaseMemberViewTest(BornhackTestBase): + """Test Team Base and Member Views.""" @classmethod def setUpTestData(cls) -> None: @@ -55,3 +57,105 @@ def test_team_manage_view(self) -> None: }) response = self.client.get(path=url) assert response.status_code == 200 + + def test_team_join_leave_view(self) -> None: + """Test the team member join and leave view.""" + self.client.force_login(self.users[0]) + url = reverse("teams:join", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(path=url) + assert response.status_code == 200 + + response = self.client.post(path=url, follow=True) + + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("div.alert.alert-success") + matches = [s for s in rows if "You request to join the team" in str(s)] + self.assertEqual(len(matches), 1, "failed to join a team.") + + # Try to join the team twice. + response = self.client.get(path=url, follow=True) + + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("div.alert.alert-warning") + matches = [s for s in rows if "You are already a member of this team" in str(s)] + self.assertEqual(len(matches), 1, "member was able to join twice.") + + # Try to join a team that does not need members + url = reverse("teams:join", kwargs={ + "team_slug": self.teams["orga"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(path=url, follow=True) + + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("div.alert.alert-warning") + matches = [s for s in rows if "This team does not need members right now" in str(s)] + self.assertEqual(len(matches), 1, "member was able to join twice.") + + # Test leaving the team. + url = reverse("teams:leave", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.get(path=url) + assert response.status_code == 200 + + response = self.client.post(path=url, follow=True) + + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("div.alert.alert-success") + matches = [s for s in rows if "You are no longer a member of the team" in str(s)] + self.assertEqual(len(matches), 1, "failed to leave a team.") + + def test_team_approve_remove_views(self) -> None: + """Test team member approve and remove views.""" + self.client.force_login(self.users[8]) + url = reverse("teams:join", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + }) + response = self.client.post(path=url) + assert response.status_code == 302 + + member = TeamMember.objects.get(team=self.teams["noc"], user=self.users[8]) + + self.client.force_login(self.users[4]) + # Approve the team member + url = reverse("teams:member_approve", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + "pk": member.pk, + }) + response = self.client.get(path=url) + assert response.status_code == 200 + + response = self.client.post(path=url, follow=True) + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("div.alert.alert-success") + matches = [s for s in rows if "Team member approved" in str(s)] + self.assertEqual(len(matches), 1, "failed to approve a team member.") + + + # Remove the team member + url = reverse("teams:member_remove", kwargs={ + "team_slug": self.teams["noc"].slug, + "camp_slug": self.camp.slug, + "pk": member.pk, + }) + response = self.client.get(path=url) + assert response.status_code == 200 + + response = self.client.post(path=url, follow=True) + content = response.content.decode() + soup = BeautifulSoup(content, "html.parser") + rows = soup.select("div.alert.alert-success") + matches = [s for s in rows if "Team member removed" in str(s)] + self.assertEqual(len(matches), 1, "failed to remove a team member.") diff --git a/src/teams/views/base.py b/src/teams/views/base.py index 3d808760d..f909229c2 100644 --- a/src/teams/views/base.py +++ b/src/teams/views/base.py @@ -42,12 +42,12 @@ def get_queryset(self, *args, **kwargs) -> QuerySet: qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related("members") return qs.prefetch_related("members__profile") - # FIXME: there is more to be gained here but the templatetag we use to see if + # TODO(someone): there is more to be gained here but the templatetag we use to see if # the logged-in user is a member of the current team does not benefit from the prefetching, # also the getting of team leads and their profiles do not use the prefetching # :( /tyk - def get_context_data(self, *, object_list=None, **kwargs) -> dict: + def get_context_data(self, *, object_list: list|None =None, **kwargs) -> dict: """Method for adding user_teams to the context.""" context = super().get_context_data(object_list=object_list, **kwargs) if self.request.user.is_authenticated: diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py index c5ddeeab6..53e3dd854 100644 --- a/src/teams/views/shifts.py +++ b/src/teams/views/shifts.py @@ -21,6 +21,8 @@ from psycopg2.extras import DateTimeTZRange from camps.mixins import CampViewMixin +from teams.exceptions import StartAfterEndError +from teams.exceptions import StartSameAsEndError from teams.models import Team from teams.models import TeamMember from teams.models import TeamShift @@ -69,7 +71,7 @@ def date_choices(camp: Camp) -> list: minute_choices.append(minutes) index += 1 - def get_time_choices(date) -> list: + def get_time_choices(date: str) -> list: """Method for making a list of time options.""" time_choices = [] for hour in range(24): @@ -136,9 +138,9 @@ def clean(self) -> None: self.lower = self._get_from_datetime() self.upper = self._get_to_datetime() if self.lower > self.upper: - raise forms.ValidationError("Start can not be after end.") + raise StartAfterEndError if self.lower == self.upper: - raise forms.ValidationError("Start can not be the same as end.") + raise StartSameAsEndError def save(self, commit=True) -> TeamShift: """Method for saving shift_range from self.lower and self.upper.""" From ed39da44700ad9f53da2118752cd32ec745ba82e Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 18 May 2025 21:05:47 +0200 Subject: [PATCH 07/10] Update src/teams/tests/test_base_views.py --- src/teams/tests/test_base_views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/teams/tests/test_base_views.py b/src/teams/tests/test_base_views.py index 98f4f969f..b18f84d81 100644 --- a/src/teams/tests/test_base_views.py +++ b/src/teams/tests/test_base_views.py @@ -96,7 +96,7 @@ def test_team_join_leave_view(self) -> None: soup = BeautifulSoup(content, "html.parser") rows = soup.select("div.alert.alert-warning") matches = [s for s in rows if "This team does not need members right now" in str(s)] - self.assertEqual(len(matches), 1, "member was able to join twice.") + self.assertEqual(len(matches), 1, "member was able to join a team which does not need members.") # Test leaving the team. url = reverse("teams:leave", kwargs={ From 17ef01f37922113eb02ca905865493a9135bdb3d Mon Sep 17 00:00:00 2001 From: Thomas Steen Rasmussen Date: Sun, 18 May 2025 21:10:21 +0200 Subject: [PATCH 08/10] Update src/teams/views/base.py --- src/teams/views/base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/teams/views/base.py b/src/teams/views/base.py index f909229c2..03c484140 100644 --- a/src/teams/views/base.py +++ b/src/teams/views/base.py @@ -42,7 +42,7 @@ def get_queryset(self, *args, **kwargs) -> QuerySet: qs = super().get_queryset(*args, **kwargs) qs = qs.prefetch_related("members") return qs.prefetch_related("members__profile") - # TODO(someone): there is more to be gained here but the templatetag we use to see if + # TODO(tyk): there is more to be gained here but the templatetag we use to see if # the logged-in user is a member of the current team does not benefit from the prefetching, # also the getting of team leads and their profiles do not use the prefetching # :( /tyk From 5a4f6f676a091d0210c9a4c5164d4735b5ac66a9 Mon Sep 17 00:00:00 2001 From: "Rudy (zarya)" Date: Sun, 18 May 2025 21:27:52 +0200 Subject: [PATCH 09/10] Removing is hard --- src/utils/bootstrap/factories.py | 210 ------------------------------- 1 file changed, 210 deletions(-) delete mode 100644 src/utils/bootstrap/factories.py diff --git a/src/utils/bootstrap/factories.py b/src/utils/bootstrap/factories.py deleted file mode 100644 index ca25ac93e..000000000 --- a/src/utils/bootstrap/factories.py +++ /dev/null @@ -1,210 +0,0 @@ -"""Factories for bootstrapping the application.""" - -from __future__ import annotations - -import logging -import random - -import factory -import pytz -from allauth.account.models import EmailAddress -from django.contrib.auth.models import User -from django.db.models.signals import post_save -from faker import Faker - -from camps.models import Camp -from economy.models import Chain -from economy.models import Credebtor -from economy.models import Expense -from economy.models import Revenue -from profiles.models import Profile -from program.models import EventProposal -from program.models import SpeakerProposal -from program.models import Url -from program.models import UrlType -from teams.models import Team -from utils.slugs import unique_slugify - -from .functions import output_fake_description -from .functions import output_fake_md_description - -fake = Faker() -tz = pytz.timezone("Europe/Copenhagen") -logger = logging.getLogger(f"bornhack.{__name__}") - - -class ChainFactory(factory.django.DjangoModelFactory): - """Factory for creating chains.""" - - class Meta: - """Meta.""" - - model = Chain - - name = factory.Faker("company") - slug = factory.LazyAttribute( - lambda f: unique_slugify( - f.name, - Chain.objects.all().values_list("slug", flat=True), - ), - ) - - -class CredebtorFactory(factory.django.DjangoModelFactory): - """Factory for creating Creditors and debitors.""" - - class Meta: - """Meta.""" - - model = Credebtor - - chain = factory.SubFactory(ChainFactory) - name = factory.Faker("company") - slug = factory.LazyAttribute( - lambda f: unique_slugify( - f.name, - Credebtor.objects.all().values_list("slug", flat=True), - ), - ) - address = factory.Faker("address", locale="dk_DK") - notes = factory.Faker("text") - - -class ExpenseFactory(factory.django.DjangoModelFactory): - """Factory for creating expense data.""" - - class Meta: - """Meta.""" - - model = Expense - - camp = factory.Faker("random_element", elements=Camp.objects.all()) - creditor = factory.Faker("random_element", elements=Credebtor.objects.all()) - user = factory.Faker("random_element", elements=User.objects.all()) - amount = factory.Faker("random_int", min=20, max=20000) - description = factory.Faker("text") - paid_by_bornhack = factory.Faker("random_element", elements=[True, True, False]) - invoice = factory.django.ImageField( - color=random.choice(["#ff0000", "#00ff00", "#0000ff"]), # noqa: S311 - ) - invoice_date = factory.Faker("date") - responsible_team = factory.Faker("random_element", elements=Team.objects.all()) - approved = factory.Faker("random_element", elements=[True, True, False]) - notes = factory.Faker("text") - - -class RevenueFactory(factory.django.DjangoModelFactory): - """Factory for creating revenue data.""" - - class Meta: - """Meta.""" - - model = Revenue - - camp = factory.Faker("random_element", elements=Camp.objects.all()) - debtor = factory.Faker("random_element", elements=Credebtor.objects.all()) - user = factory.Faker("random_element", elements=User.objects.all()) - amount = factory.Faker("random_int", min=20, max=20000) - description = factory.Faker("text") - invoice = factory.django.ImageField( - color=random.choice(["#ff0000", "#00ff00", "#0000ff"]), # noqa: S311 - ) - invoice_date = factory.Faker("date") - responsible_team = factory.Faker("random_element", elements=Team.objects.all()) - approved = factory.Faker("random_element", elements=[True, True, False]) - notes = factory.Faker("text") - - -class ProfileFactory(factory.django.DjangoModelFactory): - """Factory for creating user profiles.""" - - class Meta: - """Meta.""" - - model = Profile - - user = factory.SubFactory("self.UserFactory", profile=None) - name = factory.Faker("name") - description = factory.Faker("text") - public_credit_name = factory.Faker("name") - public_credit_name_approved = True - - -@factory.django.mute_signals(post_save) -class UserFactory(factory.django.DjangoModelFactory): - """Factory for creating a User.""" - - class Meta: - """Meta.""" - - model = User - - profile = factory.RelatedFactory(ProfileFactory, "user") - - -class EmailAddressFactory(factory.django.DjangoModelFactory): - """Factory for email address.""" - - class Meta: - """Meta.""" - - model = EmailAddress - - primary = False - verified = True - - -class SpeakerProposalFactory(factory.django.DjangoModelFactory): - """Factory for speaker proposals.""" - - class Meta: - """Meta.""" - - model = SpeakerProposal - - name = factory.Faker("name") - email = factory.Faker("email") - biography = output_fake_md_description() - submission_notes = factory.Iterator(["", output_fake_description()]) - needs_oneday_ticket = factory.Iterator([True, False]) - - -class EventProposalFactory(factory.django.DjangoModelFactory): - """Factory for event proposals.""" - - class Meta: - """Meta.""" - - model = EventProposal - - user = factory.Iterator(User.objects.all()) - title = factory.Faker("sentence") - abstract = output_fake_md_description() - allow_video_recording = factory.Iterator([True, True, True, False]) - allow_video_streaming = factory.Iterator([True, True, True, False]) - submission_notes = factory.Iterator(["", output_fake_description()]) - use_provided_speaker_laptop = factory.Iterator([True, False]) - - -class EventProposalUrlFactory(factory.django.DjangoModelFactory): - """Factory for event proposal urls.""" - - class Meta: - """Meta.""" - - model = Url - - url = factory.Faker("url") - url_type = factory.Iterator(UrlType.objects.all()) - - -class SpeakerProposalUrlFactory(factory.django.DjangoModelFactory): - """Factory for speaker proposal urls.""" - - class Meta: - """Meta.""" - - model = Url - - url = factory.Faker("url") - url_type = factory.Iterator(UrlType.objects.all()) From 9c639d149955923ea95a072f4fcd65b924c4c43a Mon Sep 17 00:00:00 2001 From: "Rudy (zarya)" Date: Fri, 30 May 2025 17:28:50 +0200 Subject: [PATCH 10/10] Rename IsPermissionMixin to IsTeamPermContextMixin --- src/teams/views/base.py | 8 ++++---- src/teams/views/guide.py | 4 ++-- src/teams/views/info.py | 10 +++++----- src/teams/views/members.py | 4 ++-- src/teams/views/shifts.py | 12 ++++++------ src/teams/views/tasks.py | 10 +++++----- src/utils/mixins.py | 2 +- 7 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/teams/views/base.py b/src/teams/views/base.py index 03c484140..6f6adc7cd 100644 --- a/src/teams/views/base.py +++ b/src/teams/views/base.py @@ -16,7 +16,7 @@ from camps.mixins import CampViewMixin from teams.models import Team from teams.models import TeamMember -from utils.mixins import IsPermissionMixin +from utils.mixins import IsTeamPermContextMixin from utils.widgets import MarkdownWidget from .mixins import EnsureTeamLeadMixin @@ -57,7 +57,7 @@ def get_context_data(self, *, object_list: list|None =None, **kwargs) -> dict: return context -class TeamGeneralView(CampViewMixin, IsPermissionMixin, DetailView): +class TeamGeneralView(CampViewMixin, IsTeamPermContextMixin, DetailView): """General view for a team.""" template_name = "team_general.html" context_object_name = "team" @@ -73,7 +73,7 @@ def get_context_data(self, **kwargs) -> dict: return context -class TeamManageView(CampViewMixin, EnsureTeamLeadMixin, IsPermissionMixin, UpdateView): +class TeamManageView(CampViewMixin, EnsureTeamLeadMixin, IsTeamPermContextMixin, UpdateView): """View for mananaging team members.""" model = Team template_name = "team_manage.html" @@ -111,7 +111,7 @@ def form_valid(self, form: Form) -> HttpResponseRedirect: return super().form_valid(form) -class FixIrcAclView(LoginRequiredMixin, CampViewMixin, IsPermissionMixin, UpdateView): +class FixIrcAclView(LoginRequiredMixin, CampViewMixin, IsTeamPermContextMixin, UpdateView): """View for fixing IRC ACL's.""" template_name = "fix_irc_acl.html" model = Team diff --git a/src/teams/views/guide.py b/src/teams/views/guide.py index ae86e53fb..e2f510a09 100644 --- a/src/teams/views/guide.py +++ b/src/teams/views/guide.py @@ -8,10 +8,10 @@ from camps.mixins import CampViewMixin from teams.models import Team from teams.models import TeamMember -from utils.mixins import IsPermissionMixin +from utils.mixins import IsTeamPermContextMixin -class TeamGuideView(LoginRequiredMixin, CampViewMixin, UserPassesTestMixin, IsPermissionMixin, DetailView): +class TeamGuideView(LoginRequiredMixin, CampViewMixin, UserPassesTestMixin, IsTeamPermContextMixin, DetailView): """View for the team guide.""" template_name = "team_guide.html" context_object_name = "team" diff --git a/src/teams/views/info.py b/src/teams/views/info.py index 52eb075df..06ee4153b 100644 --- a/src/teams/views/info.py +++ b/src/teams/views/info.py @@ -13,7 +13,7 @@ from info.models import InfoCategory from info.models import InfoItem from teams.views.mixins import TeamInfopagerPermissionMixin -from utils.mixins import IsPermissionMixin +from utils.mixins import IsTeamPermContextMixin from utils.widgets import MarkdownWidget from .mixins import TeamViewMixin @@ -24,7 +24,7 @@ class InfoCategoriesListView( TeamViewMixin, TeamInfopagerPermissionMixin, - IsPermissionMixin, + IsTeamPermContextMixin, ListView, ): """Info Categories list view.""" @@ -37,7 +37,7 @@ class InfoCategoriesListView( class InfoItemCreateView( TeamViewMixin, TeamInfopagerPermissionMixin, - IsPermissionMixin, + IsTeamPermContextMixin, CreateView, ): """Info item create view.""" @@ -82,7 +82,7 @@ class InfoItemUpdateView( TeamViewMixin, TeamInfopagerPermissionMixin, RevisionMixin, - IsPermissionMixin, + IsTeamPermContextMixin, UpdateView, ): """Info item update view.""" @@ -111,7 +111,7 @@ class InfoItemDeleteView( TeamViewMixin, TeamInfopagerPermissionMixin, RevisionMixin, - IsPermissionMixin, + IsTeamPermContextMixin, DeleteView, ): """View for deleting a info item.""" diff --git a/src/teams/views/members.py b/src/teams/views/members.py index 2ddac48f9..a2b3993e3 100644 --- a/src/teams/views/members.py +++ b/src/teams/views/members.py @@ -16,7 +16,7 @@ from teams.email import add_removed_membership_email from teams.models import Team from teams.models import TeamMember -from utils.mixins import IsPermissionMixin +from utils.mixins import IsTeamPermContextMixin from .mixins import EnsureTeamMemberLeadMixin from .mixins import TeamViewMixin @@ -30,7 +30,7 @@ logger = logging.getLogger(f"bornhack.{__name__}") -class TeamMembersView(CampViewMixin, IsPermissionMixin, DetailView): +class TeamMembersView(CampViewMixin, IsTeamPermContextMixin, DetailView): """List view for team members.""" template_name = "team_members.html" context_object_name = "team" diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py index 53e3dd854..5a361d8ae 100644 --- a/src/teams/views/shifts.py +++ b/src/teams/views/shifts.py @@ -26,7 +26,7 @@ from teams.models import Team from teams.models import TeamMember from teams.models import TeamShift -from utils.mixins import IsPermissionMixin +from utils.mixins import IsTeamPermContextMixin from .mixins import EnsureTeamLeadMixin @@ -38,7 +38,7 @@ from camps.models import Camp -class ShiftListView(LoginRequiredMixin, CampViewMixin, IsPermissionMixin, ListView): +class ShiftListView(LoginRequiredMixin, CampViewMixin, IsTeamPermContextMixin, ListView): """Shift list view.""" model = TeamShift template_name = "team_shift_list.html" @@ -149,7 +149,7 @@ def save(self, commit=True) -> TeamShift: return super().save(commit=commit) -class ShiftCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsPermissionMixin, CreateView): +class ShiftCreateView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsTeamPermContextMixin, CreateView): """View for creating a single shift.""" model = TeamShift template_name = "team_shift_form.html" @@ -182,7 +182,7 @@ def get_success_url(self) -> str: return reverse("teams:shifts", kwargs=self.kwargs) -class ShiftUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsPermissionMixin, UpdateView): +class ShiftUpdateView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsTeamPermContextMixin, UpdateView): """View for updating a single shift.""" model = TeamShift template_name = "team_shift_form.html" @@ -207,7 +207,7 @@ def get_success_url(self) -> str: return reverse("teams:shifts", kwargs=self.kwargs) -class ShiftDeleteView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsPermissionMixin, DeleteView): +class ShiftDeleteView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsTeamPermContextMixin, DeleteView): """View for deleting a shift.""" model = TeamShift template_name = "team_shift_confirm_delete.html" @@ -247,7 +247,7 @@ def __init__(self, instance: dict|None=None, **kwargs) -> None: people_required = forms.IntegerField() -class ShiftCreateMultipleView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsPermissionMixin, FormView): +class ShiftCreateMultipleView(LoginRequiredMixin, CampViewMixin, EnsureTeamLeadMixin, IsTeamPermContextMixin, FormView): """View for creating multiple shifts.""" template_name = "team_shift_form.html" form_class = MultipleShiftForm diff --git a/src/teams/views/tasks.py b/src/teams/views/tasks.py index 9546f832a..ffe960275 100644 --- a/src/teams/views/tasks.py +++ b/src/teams/views/tasks.py @@ -17,7 +17,7 @@ from teams.models import Team from teams.models import TeamMember from teams.models import TeamTask -from utils.mixins import IsPermissionMixin +from utils.mixins import IsTeamPermContextMixin from .mixins import TeamTaskerPermissionMixin from .mixins import TeamViewMixin @@ -25,7 +25,7 @@ if TYPE_CHECKING: from django.http import HttpRequest -class TeamTasksView(CampViewMixin, IsPermissionMixin, DetailView): +class TeamTasksView(CampViewMixin, IsTeamPermContextMixin, DetailView): """List view of the team tasks.""" template_name = "team_tasks.html" context_object_name = "team" @@ -42,7 +42,7 @@ class Meta: fields = ("comment",) -class TaskDetailView(TeamViewMixin, IsPermissionMixin, DetailView): +class TaskDetailView(TeamViewMixin, IsTeamPermContextMixin, DetailView): """Task detail view.""" template_name = "task_detail.html" context_object_name = "task" @@ -93,7 +93,7 @@ class TaskCreateView( LoginRequiredMixin, TeamViewMixin, TeamTaskerPermissionMixin, - IsPermissionMixin, + IsTeamPermContextMixin, CreateView, ): """View for creating a team task.""" @@ -127,7 +127,7 @@ class TaskUpdateView( LoginRequiredMixin, TeamViewMixin, TeamTaskerPermissionMixin, - IsPermissionMixin, + IsTeamPermContextMixin, UpdateView, ): """Update task view used for updating tasks.""" diff --git a/src/utils/mixins.py b/src/utils/mixins.py index 8184c5912..7eabe716d 100644 --- a/src/utils/mixins.py +++ b/src/utils/mixins.py @@ -39,7 +39,7 @@ class RaisePermissionRequiredMixin(PermissionRequiredMixin): raise_exception = True -class IsPermissionMixin: +class IsTeamPermContextMixin: """Mixing for adding is_team_{perm} to context""" def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs)