Initial work on shift planning.

This commit is contained in:
Víðir Valberg Guðmundsson 2018-07-02 23:52:52 +02:00
parent 9bfdc714f0
commit 102dfa7330
8 changed files with 317 additions and 7 deletions

View file

@ -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',
]

View file

@ -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'),
),
]

View file

@ -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'),
),
]

View file

@ -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'

View file

@ -0,0 +1,20 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load bootstrap3 %}
{% block content %}
{% if form.errors %}
{{ form.errors }}
{% endif %}
<form method="POST">
{% csrf_token %}
{% bootstrap_form form %}
<button type="submit" class="btn btn-success">
Create
</button>
</form>
{% endblock %}

View file

@ -0,0 +1,53 @@
{% extends 'base.html' %}
{% load commonmark %}
{% load bootstrap3 %}
{% block content %}
<a class="btn btn-info"
href="{% url 'teams:detail' camp_slug=camp.slug team_slug=team.slug %}">
<i class="fas fa-chevron-left"></i> Back to team detail
</a>
<hr />
{% if request.user in team.responsible_members.all %}
<a class="btn btn-success"
href="{% url 'teams:shift_create' camp_slug=camp.slug team_slug=team.slug %}">
Create shift
</a>
{% endif %}
<table class="table table-condensed">
<tbody>
{% for shift in shifts %}
{% ifchanged shift.shift_range.lower|date:'d' %}
<tr>
<td colspan=4>
<h4>
{{ shift.shift_range.lower|date:'Y-m-d l' }}
</h4>
<tr>
<th>
From
<th>
To
<th>
People required
<th>
People
{% endifchanged %}
<tr>
<td>
{{ shift.shift_range.lower|date:'H:i' }}
<td>
{{ shift.shift_range.upper|date:'H:i' }}
<td>
{{ shift.people_required }}
<td>
{{ shift.team_members}}
{% endfor %}
</table>
{% endblock %}

View file

@ -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"
),
]))
]),
),
]

120
src/teams/views/shifts.py Normal file
View file

@ -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"