From 102dfa7330ea665d5a3c247781614d416b20bd34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=AD=C3=B0ir=20Valberg=20Gu=C3=B0mundsson?= Date: Mon, 2 Jul 2018 23:52:52 +0200 Subject: [PATCH] Initial work on shift planning. --- src/teams/admin.py | 9 +- .../migrations/0043_auto_20180702_1338.py | 43 +++++++ .../migrations/0044_auto_20180702_1507.py | 18 +++ src/teams/models.py | 42 +++++- src/teams/templates/shifts/shift_form.html | 20 +++ src/teams/templates/shifts/shift_list.html | 53 ++++++++ src/teams/urls.py | 19 ++- src/teams/views/shifts.py | 120 ++++++++++++++++++ 8 files changed, 317 insertions(+), 7 deletions(-) create mode 100644 src/teams/migrations/0043_auto_20180702_1338.py create mode 100644 src/teams/migrations/0044_auto_20180702_1507.py create mode 100644 src/teams/templates/shifts/shift_form.html create mode 100644 src/teams/templates/shifts/shift_list.html create mode 100644 src/teams/views/shifts.py diff --git a/src/teams/admin.py b/src/teams/admin.py index 0539b6d0..59646a99 100644 --- a/src/teams/admin.py +++ b/src/teams/admin.py @@ -1,5 +1,5 @@ from django.contrib import admin -from .models import Team, TeamMember, TeamTask +from .models import Team, TeamMember, TeamTask, TeamShift from .email import add_added_membership_email, add_removed_membership_email from camps.utils import CampPropertyListFilter @@ -90,3 +90,10 @@ class TeamMemberAdmin(admin.ModelAdmin): ) remove_member.description = 'Remove a user from the team.' + + +@admin.register(TeamShift) +class TeamShiftAdmin(admin.ModelAdmin): + list_filter = [ + 'team', + ] diff --git a/src/teams/migrations/0043_auto_20180702_1338.py b/src/teams/migrations/0043_auto_20180702_1338.py new file mode 100644 index 00000000..ad64e0dc --- /dev/null +++ b/src/teams/migrations/0043_auto_20180702_1338.py @@ -0,0 +1,43 @@ +# Generated by Django 2.0.4 on 2018-07-02 18:38 + +import django.contrib.postgres.fields.ranges +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0042_auto_20180413_1933'), + ] + + operations = [ + migrations.CreateModel( + name='TeamShift', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', models.DateTimeField(auto_now_add=True)), + ('updated', models.DateTimeField(auto_now=True)), + ('shift_range', django.contrib.postgres.fields.ranges.DateTimeRangeField()), + ('people_required', models.IntegerField(default=1)), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='team', + name='shifts_enabled', + field=models.BooleanField(default=False, help_text='Does this team have shifts? This enables defining shifts for this team.'), + ), + migrations.AddField( + model_name='teamshift', + name='team', + field=models.ForeignKey(help_text='The team this shift belongs to', on_delete=django.db.models.deletion.PROTECT, related_name='shifts', to='teams.Team'), + ), + migrations.AddField( + model_name='teamshift', + name='team_members', + field=models.ManyToManyField(to='teams.TeamMember'), + ), + ] diff --git a/src/teams/migrations/0044_auto_20180702_1507.py b/src/teams/migrations/0044_auto_20180702_1507.py new file mode 100644 index 00000000..cb19b4ac --- /dev/null +++ b/src/teams/migrations/0044_auto_20180702_1507.py @@ -0,0 +1,18 @@ +# Generated by Django 2.0.4 on 2018-07-02 20:07 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('teams', '0043_auto_20180702_1338'), + ] + + operations = [ + migrations.AlterField( + model_name='teamshift', + name='team_members', + field=models.ManyToManyField(blank=True, to='teams.TeamMember'), + ), + ] diff --git a/src/teams/models.py b/src/teams/models.py index b145c44b..5d445508 100644 --- a/src/teams/models.py +++ b/src/teams/models.py @@ -1,13 +1,14 @@ +import logging + from django.db import models -from django.db.models.signals import post_save -from django.dispatch import receiver from django.utils.text import slugify -from utils.models import CampRelatedModel from django.core.exceptions import ValidationError -from django.contrib.auth.models import User from django.urls import reverse_lazy from django.conf import settings -import logging +from django.contrib.postgres.fields import DateTimeRangeField + +from utils.models import CampRelatedModel + logger = logging.getLogger("bornhack.%s" % __name__) @@ -102,6 +103,11 @@ class Team(CampRelatedModel): help_text='Used to indicate to the IRC bot that this teams private IRC channel is in need of a permissions and ACL fix.' ) + shifts_enabled = models.BooleanField( + default=False, + help_text="Does this team have shifts? This enables defining shifts for this team." + ) + class Meta: ordering = ['name'] unique_together = (('name', 'camp'), ('slug', 'camp')) @@ -301,3 +307,29 @@ class TeamTask(CampRelatedModel): self.slug = slugify(self.name) super().save(**kwargs) + +class TeamShift(CampRelatedModel): + team = models.ForeignKey( + 'teams.Team', + related_name='shifts', + on_delete=models.PROTECT, + help_text='The team this shift belongs to', + ) + + shift_range = DateTimeRangeField() + + team_members = models.ManyToManyField( + TeamMember, + blank=True, + ) + + people_required = models.IntegerField( + default=1 + ) + + @property + def camp(self): + """ All CampRelatedModels must have a camp FK or a camp property """ + return self.team.camp + + camp_filter = 'team__camp' diff --git a/src/teams/templates/shifts/shift_form.html b/src/teams/templates/shifts/shift_form.html new file mode 100644 index 00000000..b4bbb7fa --- /dev/null +++ b/src/teams/templates/shifts/shift_form.html @@ -0,0 +1,20 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load bootstrap3 %} + + +{% block content %} + +{% if form.errors %} +{{ form.errors }} +{% endif %} + +
+ {% csrf_token %} + {% bootstrap_form form %} + +
+ +{% endblock %} diff --git a/src/teams/templates/shifts/shift_list.html b/src/teams/templates/shifts/shift_list.html new file mode 100644 index 00000000..41aa1ac7 --- /dev/null +++ b/src/teams/templates/shifts/shift_list.html @@ -0,0 +1,53 @@ +{% extends 'base.html' %} +{% load commonmark %} +{% load bootstrap3 %} + + +{% block content %} + + + Back to team detail + + +
+ +{% if request.user in team.responsible_members.all %} + + Create shift + +{% endif %} + + + + {% for shift in shifts %} + {% ifchanged shift.shift_range.lower|date:'d' %} + + + +
+

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

+
+ From + + To + + People required + + People + {% endifchanged %} + +
+ {{ shift.shift_range.lower|date:'H:i' }} + + {{ shift.shift_range.upper|date:'H:i' }} + + {{ shift.people_required }} + + {{ shift.team_members}} + {% endfor %} +
+{% endblock %} diff --git a/src/teams/urls.py b/src/teams/urls.py index 330adfb9..efc1cf72 100644 --- a/src/teams/urls.py +++ b/src/teams/urls.py @@ -18,6 +18,11 @@ from teams.views.tasks import ( TaskUpdateView, ) +from teams.views.shifts import ( + ShiftListView, + ShiftCreateView, +) + app_name = 'teams' urlpatterns = [ @@ -113,7 +118,19 @@ urlpatterns = [ ]), ), ]) - ) + ), + path('shifts/', include([ + path( + '', + ShiftListView.as_view(), + name="shift_list" + ), + path( + 'create/', + ShiftCreateView.as_view(), + name="shift_create" + ), + ])) ]), ), ] diff --git a/src/teams/views/shifts.py b/src/teams/views/shifts.py new file mode 100644 index 00000000..2a112173 --- /dev/null +++ b/src/teams/views/shifts.py @@ -0,0 +1,120 @@ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.views.generic import CreateView, UpdateView, ListView +from django import forms +from django.contrib.postgres.forms.ranges import RangeWidget +from django.utils import timezone +from django.urls import reverse +from psycopg2.extras import DateTimeTZRange + +from camps.mixins import CampViewMixin + +from ..models import ( + Team, + TeamShift, +) + + +class ShiftListView(LoginRequiredMixin, CampViewMixin, ListView): + model = TeamShift + template_name = "shifts/shift_list.html" + context_object_name = "shifts" + + def get_queryset(self): + queryset = super().get_queryset() + return queryset.filter(team__slug=self.kwargs['team_slug']) + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context['team'] = Team.objects.get( + camp=self.camp, + slug=self.kwargs['team_slug'] + ) + return context + + +def time_choices(): + index = 0 + minute_choices = [] + SHIFT_MINIMUM_LENGTH = 15 # TODO: Maybe this should be configurable per team? + while index * SHIFT_MINIMUM_LENGTH < 60: + minutes = int(index * SHIFT_MINIMUM_LENGTH) + minute_choices.append(minutes) + index += 1 + + time_choices = [] + for hour in range(0, 24): + for minute in minute_choices: + choice_label = "{hour:02d}:{minutes:02d}".format(hour=hour, minutes=minute) + time_choices.append((choice_label, choice_label)) + + return time_choices + + +class ShiftForm(forms.ModelForm): + class Meta: + model = TeamShift + fields = [ + 'from_date', + 'from_time', + 'to_date', + 'to_time', + 'people_required' + ] + + from_date = forms.DateField( + help_text="Format is YYYY-MM-DD - ie. 2018-08-15" + ) + + from_time = forms.ChoiceField( + choices=time_choices + ) + + to_date = forms.DateField( + help_text="Format is YYYY-MM-DD - ie. 2018-08-15" + ) + + to_time = forms.ChoiceField( + choices=time_choices + ) + + def save(self, commit=True): + from_string = "{} {}".format( + self.cleaned_data['from_date'], + self.cleaned_data['from_time'] + ) + to_string = "{} {}".format( + self.cleaned_data['to_date'], + self.cleaned_data['to_time'] + ) + datetime_format = '%Y-%m-%d %H:%M' + self.instance.shift_range = DateTimeTZRange( + timezone.datetime.strptime(from_string, datetime_format), + timezone.datetime.strptime(to_string, datetime_format) + ) + return super().save(commit=commit) + + +class ShiftCreateView(LoginRequiredMixin, CampViewMixin, CreateView): + model = TeamShift + template_name = "shifts/shift_form.html" + form_class = ShiftForm + + def form_valid(self, form): + 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:shift_list', + kwargs=self.kwargs + ) + + +class ShiftUpdateView(LoginRequiredMixin, CampViewMixin, UpdateView): + model = TeamShift + template_name = "shifts/shift_form.html" +