Initial work on shift planning.
This commit is contained in:
parent
9bfdc714f0
commit
102dfa7330
|
@ -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',
|
||||
]
|
||||
|
|
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.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'
|
||||
|
|
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,
|
||||
)
|
||||
|
||||
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
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