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.
This commit is contained in:
parent
b3f127f197
commit
005431d6f6
|
@ -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?",
|
||||
)
|
||||
|
|
|
@ -50,7 +50,7 @@ Shifts | {{ block.super }}
|
|||
{{ shift.people_required }}
|
||||
<td>
|
||||
{% 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 %} <em>(for sale!)</em>{% 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 %}">
|
||||
<i class="fas fa-thumbs-down"></i> Unassign me
|
||||
</a>
|
||||
{% elif shift.people_required > shift.team_members.count %}
|
||||
<a class="btn btn-warning"
|
||||
href="{% url 'teams:shift_member_sell' camp_slug=camp.slug team_slug=team.slug pk=shift.pk %}">
|
||||
<i class="fas fa-thumbs-down"></i> Sell shift
|
||||
</a>
|
||||
{% elif shift.people_required > shift.team_members.filter(for_sale=False).count %}
|
||||
<a class="btn btn-success"
|
||||
href="{% url 'teams:shift_member_take' camp_slug=camp.slug team_slug=team.slug pk=shift.pk %}">
|
||||
<i class="fas fa-thumbs-up"></i> Assign me
|
||||
|
|
|
@ -34,6 +34,10 @@ Your shifts | {{ block.super }}
|
|||
<td>
|
||||
{{ shift.shift_range.upper|date:'H:i' }}
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-warning"
|
||||
href="{% url 'teams:shift_member_sell' camp_slug=camp.slug team_slug=shift.team.slug pk=shift.pk %}">
|
||||
<i class="fas fa-thumbs-down"></i> Sell shift
|
||||
<td>
|
||||
<a class="btn btn-danger"
|
||||
href="{% url 'teams:shift_member_drop' camp_slug=camp.slug team_slug=shift.team.slug pk=shift.pk %}">
|
||||
|
@ -45,4 +49,4 @@ Your shifts | {{ block.super }}
|
|||
</tbody>
|
||||
</table>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
|
|
@ -179,6 +179,11 @@ urlpatterns = [
|
|||
MemberDropsShift.as_view(),
|
||||
name="shift_member_drop",
|
||||
),
|
||||
path(
|
||||
"sell",
|
||||
MemberSellsShift.as_view(),
|
||||
name="shift_member_sell",
|
||||
),
|
||||
]
|
||||
),
|
||||
),
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in a new issue