Initial work on shift planning.
This commit is contained in:
parent
9bfdc714f0
commit
102dfa7330
|
@ -1,5 +1,5 @@
|
||||||
from django.contrib import admin
|
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 .email import add_added_membership_email, add_removed_membership_email
|
||||||
from camps.utils import CampPropertyListFilter
|
from camps.utils import CampPropertyListFilter
|
||||||
|
|
||||||
|
@ -90,3 +90,10 @@ class TeamMemberAdmin(admin.ModelAdmin):
|
||||||
)
|
)
|
||||||
remove_member.description = 'Remove a user from the team.'
|
remove_member.description = 'Remove a user from the team.'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@admin.register(TeamShift)
|
||||||
|
class TeamShiftAdmin(admin.ModelAdmin):
|
||||||
|
list_filter = [
|
||||||
|
'team',
|
||||||
|
]
|
||||||
|
|
43
src/teams/migrations/0043_auto_20180702_1338.py
Normal file
43
src/teams/migrations/0043_auto_20180702_1338.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
18
src/teams/migrations/0044_auto_20180702_1507.py
Normal file
18
src/teams/migrations/0044_auto_20180702_1507.py
Normal 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'),
|
||||||
|
),
|
||||||
|
]
|
|
@ -1,13 +1,14 @@
|
||||||
|
import logging
|
||||||
|
|
||||||
from django.db import models
|
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 django.utils.text import slugify
|
||||||
from utils.models import CampRelatedModel
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.contrib.auth.models import User
|
|
||||||
from django.urls import reverse_lazy
|
from django.urls import reverse_lazy
|
||||||
from django.conf import settings
|
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__)
|
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.'
|
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:
|
class Meta:
|
||||||
ordering = ['name']
|
ordering = ['name']
|
||||||
unique_together = (('name', 'camp'), ('slug', 'camp'))
|
unique_together = (('name', 'camp'), ('slug', 'camp'))
|
||||||
|
@ -301,3 +307,29 @@ class TeamTask(CampRelatedModel):
|
||||||
self.slug = slugify(self.name)
|
self.slug = slugify(self.name)
|
||||||
super().save(**kwargs)
|
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'
|
||||||
|
|
20
src/teams/templates/shifts/shift_form.html
Normal file
20
src/teams/templates/shifts/shift_form.html
Normal 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 %}
|
53
src/teams/templates/shifts/shift_list.html
Normal file
53
src/teams/templates/shifts/shift_list.html
Normal 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 %}
|
|
@ -18,6 +18,11 @@ from teams.views.tasks import (
|
||||||
TaskUpdateView,
|
TaskUpdateView,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from teams.views.shifts import (
|
||||||
|
ShiftListView,
|
||||||
|
ShiftCreateView,
|
||||||
|
)
|
||||||
|
|
||||||
app_name = 'teams'
|
app_name = 'teams'
|
||||||
|
|
||||||
urlpatterns = [
|
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
120
src/teams/views/shifts.py
Normal 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"
|
||||||
|
|
Loading…
Reference in a new issue