From 005431d6f6e07ebcba9321c71a3cc45a02bf09c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Reynir=20Bj=C3=B6rnsson?= Date: Tue, 24 Aug 2021 18:07:58 +0200 Subject: [PATCH] team shifts: implement marking shifts for sale A common occurence is that not enough people volunteer for shifts at first, and then the few volunteers take all the shifts (too many). Then late comers aren't able to take any shifts. This commit implements a third state for shift assignment: for sale. A shift marked for sale means the team member is able to take the shift if need be, but would rather someone else takes the shift. Another team member will be able to take shifts at the same time even if the required number of people are met. The current semantics is for-sale shifts are replaced eagerly. Another possible implementation would be to only replace for-sale shifts if the number of required people is met. --- src/teams/models.py | 20 +++++++++++++++++++- src/teams/templates/team_shift_list.html | 8 ++++++-- src/teams/templates/team_user_shifts.html | 6 +++++- src/teams/urls.py | 5 +++++ src/teams/views/shifts.py | 20 ++++++++++++++++++++ 5 files changed, 55 insertions(+), 4 deletions(-) diff --git a/src/teams/models.py b/src/teams/models.py index 6e0862f4..3366926f 100644 --- a/src/teams/models.py +++ b/src/teams/models.py @@ -401,7 +401,7 @@ class TeamShift(CampRelatedModel): shift_range = DateTimeRangeField() - team_members = models.ManyToManyField(TeamMember, blank=True) + team_members = models.ManyToManyField(TeamMember, blank=True, through=TeamShiftAssignment) people_required = models.IntegerField(default=1) @@ -420,3 +420,21 @@ class TeamShift(CampRelatedModel): @property def users(self): return [member.user for member in self.team_members.all()] + +class TeamShiftAssignment(CampRelatedModel): + team_shift = models.ForeignKey( + "teams.TeamShift", + on_delete=models.CASCADE, + help_text="The shift", + ) + + team_member = models.ForeignKey( + "teams.TeamMember", + on_delete=models.CASCADE, + help_text="The team member on shift", + ) + + for_sale = models.BooleanField( + default=False, + help_text="Is the shift assignment for sale?", + ) diff --git a/src/teams/templates/team_shift_list.html b/src/teams/templates/team_shift_list.html index 360e66a9..e8bbf025 100644 --- a/src/teams/templates/team_shift_list.html +++ b/src/teams/templates/team_shift_list.html @@ -50,7 +50,7 @@ Shifts | {{ block.super }} {{ shift.people_required }} {% for member in shift.team_members.all %} - {{ member.user.profile.get_public_credit_name }}{% if not forloop.last %},{% endif %} + {{ member.user.profile.get_public_credit_name }}{% if member.for_sale %} (for sale!){% endif %}{% if not forloop.last %},{% endif %} {% empty %} None! {% endfor %} @@ -71,7 +71,11 @@ Shifts | {{ block.super }} href="{% url 'teams:shift_member_drop' camp_slug=camp.slug team_slug=team.slug pk=shift.pk %}"> Unassign me - {% elif shift.people_required > shift.team_members.count %} + + Sell shift + + {% elif shift.people_required > shift.team_members.filter(for_sale=False).count %} Assign me diff --git a/src/teams/templates/team_user_shifts.html b/src/teams/templates/team_user_shifts.html index 745fd2bb..e3311b90 100644 --- a/src/teams/templates/team_user_shifts.html +++ b/src/teams/templates/team_user_shifts.html @@ -34,6 +34,10 @@ Your shifts | {{ block.super }} {{ shift.shift_range.upper|date:'H:i' }} + + + Sell shift @@ -45,4 +49,4 @@ Your shifts | {{ block.super }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/src/teams/urls.py b/src/teams/urls.py index 92ef4e03..33bdc7c7 100644 --- a/src/teams/urls.py +++ b/src/teams/urls.py @@ -179,6 +179,11 @@ urlpatterns = [ MemberDropsShift.as_view(), name="shift_member_drop", ), + path( + "sell", + MemberSellsShift.as_view(), + name="shift_member_sell", + ), ] ), ), diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py index eaa44fc8..72d0ede9 100644 --- a/src/teams/views/shifts.py +++ b/src/teams/views/shifts.py @@ -274,6 +274,9 @@ class MemberTakesShift(LoginRequiredMixin, CampViewMixin, View): request, template.render(Context({"shifts": overlapping_shifts})) ) else: + # Remove at most one shift assignment for sale + for shift_assignment in shift.team_members.filter(for_sale=True)[:1]: + shift_assignment.delete() shift.team_members.add(team_member) kwargs.pop("pk") @@ -297,6 +300,23 @@ class MemberDropsShift(LoginRequiredMixin, CampViewMixin, View): return HttpResponseRedirect(reverse("teams:shifts", kwargs=kwargs)) +class MemberSellsShift(LoginRequiredMixin, CampViewMixin, View): + + http_methods = ["get"] + + def get(self, request, **kwargs): + shift = TeamShift.objects.get(id=kwargs["pk"]) + team = Team.objects.get(camp=self.camp, slug=kwargs["team_slug"]) + + team_member = TeamMember.objects.get(team=team, user=request.user) + + shift_assignment = shift.team_members.get(team_member = team_member.pk) + shift_assignment.for_sale = True + shift_assignment.save() + + kwargs.pop("pk") + + return HttpResponseRedirect(reverse("teams:shifts", kwargs=kwargs)) class UserShifts(CampViewMixin, TemplateView): template_name = "team_user_shifts.html"