Second commit

Okay, I will start writing more meaningful commits from now on!
Sorry, future readers.
This commit is contained in:
Reynir Björnsson 2020-02-06 13:29:23 +01:00
parent 9a0577e36d
commit 5cf552bf8c
14 changed files with 369 additions and 32 deletions

View File

@ -1,3 +1,18 @@
from django.contrib import admin
# Register your models here.
from .models import LogEntry, ShiftSlot, WeeklyShift, CleaningTask, \
CleaningTaskSignoff
def deactivate_tasks(modeladmin, request, queryset):
queryset.update(active=False)
deactivate_tasks.short_description = 'Mark the selected tasks as inactive'
class CleaningTaskAdmin(admin.ModelAdmin):
actions = [deactivate_tasks]
list_filter = ('active',)
admin.site.register(LogEntry)
admin.site.register(ShiftSlot)
admin.site.register(WeeklyShift)
admin.site.register(CleaningTask, CleaningTaskAdmin)
admin.site.register(CleaningTaskSignoff)

View File

@ -1,3 +1,82 @@
from django.db import models
# Create your models here.
from django.utils.translation import gettext_lazy as g
from django.db.models import SmallIntegerField
from pytz import timezone
class ShiftSlot(models.Model):
tzinfo = timezone('Europe/Copenhagen')
start = models.TimeField('start of shift')
end = models.TimeField('end of shift')
description = models.CharField(max_length=128)
def __str__(self):
return "{0} shift {1}-{2}".format(self.description,
self.start.isoformat(timespec='minutes'),
self.end.isoformat(timespec='minutes'))
class WeeklyShift(models.Model):
DAY_OF_THE_WEEK = (
(0, g('Monday')),
(1, g('Tuesday')),
(2, g('Wednesday')),
(3, g('Thursday')),
(4, g('Friday')),
(5, g('Saturday')),
(6, g('Sunday')),
)
day_of_the_week = models.PositiveSmallIntegerField(choices=DAY_OF_THE_WEEK)
shift_slot = models.ForeignKey(ShiftSlot, on_delete=models.CASCADE)
def __str__(self):
return "{0} {1}".format(self.get_day_of_the_week_display(),
self.shift_slot)
class CleaningTask(models.Model):
shift = models.ForeignKey(WeeklyShift, on_delete=models.CASCADE)
task_description = models.TextField('cleaning task description')
active = models.BooleanField(
verbose_name='active task',
default=True)
def __str__(self):
return "{state}{shift}\n{description}".format(
state='<INACTIVE>' if not self.active else '',
shift=self.shift,
description=self.task_description)
class Meta:
ordering = ['-active', 'shift']
class CleaningTaskSignoff(models.Model):
task = models.ForeignKey(CleaningTask, on_delete=models.PROTECT)
date = models.DateTimeField(
verbose_name = 'sign-off date',
auto_now_add = True,
blank = True,
)
employee = models.CharField(max_length=255, verbose_name='employee')
def __str__(self):
return "{0} at {1} for {2}".format(
self.employee,
self.date.isoformat(timespec='minutes'),
self.task)
class LogEntry(models.Model):
author = models.CharField(
blank=True,
max_length=255,
verbose_name='author',
)
log_text = models.TextField(max_length=8192)
pub_date = models.DateTimeField('date published')
def __str__(self):
return self.log_text
class Meta:
verbose_name_plural = 'log entries'

View File

@ -0,0 +1,16 @@
<!DOCTYPE html>
<title>Cleaning schedule</title>
<h1>Cleaning schedule</h1>
{% if tasks %}
<ul>
{% for task in tasks %}
<li><a href="{% url 'mellemfolk:cleaning_task' task.id %}">
{% if not task.active %}
<span>&lt;inactive&gt;</span>
{% endif %}
{{task.shift}}</a></li>
{% endfor %}
</ul>
{% else %}
<em>No cleaning tasks!</em>
{% endif %}

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
{% load tz %}
{% timezone "Europe/Copenhagen" %}
<title>Cleaning task for {{task.shift}}</title>
<h1>Cleaning task {{task.shift}}</h1>
<p>{{task.task_description|linebreaks}}</p>
{% if task.active %}
<h2>Sign-off when you've done this task today</h2>
<form action="{% url 'mellemfolk:signoff_task' task.id %}" method="POST">
<label for="signoff">Your name:</label>
<input type="text" id="signoff" name="signoff" value="{{ employees }}"/>
<button>Sign-off!</button>
{% csrf_token %}
</form>
{% endif %}
{% if signoffs %}
<h2>Signed off by</h2>
<ul>
{% for signoff in signoffs %}
<li>{{signoff.employee}} at {{signoff.date|date:'Y-m-d H:i'}}</li>
{% endfor %}
</ul>
{% endif %}
{% endtimezone %}

View File

@ -0,0 +1,8 @@
<!DOCTYPE html>
<title>Volunteer {{first_name}} {{last_name}}</title>
<h1>{{first_name}} {{last_name}}</h1>
<ul>
<li>Email: <a href="mailto:{{email}}">{{email}}</a></li>
<li>Cellphone number: <a href="tel:{{cell_phone}}">
{{cell_phone}}</a></li>
</ul>

View File

@ -0,0 +1,32 @@
<!DOCTYPE html>
{% load tz %}
{% timezone "Europe/Copenhagen" %}
<title>Latest log entries</title>
<h1>Mellemfolk log book</h1>
<form action="{% url 'mellemfolk:submit_log_entry' %}" method="POST">
<label for="author">Your name (optional):</label>
<input type="text" name="author" id="author" list="author_list"/>
<datalist id="author_list">
{% for employee in employees %}
<option value="{{employee}}" />
{% endfor %}
</datalist>
</br>
<textarea name="log_text" id="log_text" cols="80" rows="24"
placeholder="Write a new log entry..."></textarea>
<button>Submit!</button>
{% csrf_token %}
</form>
{% if latest_log_entries %}
<h2>Latest {{latest_log_entries|length}} entries</h2>
<ul>
{% for log_entry in latest_log_entries %}
<li><a href="{% url 'mellemfolk:log_entry' log_entry.id %}">Posted
{% if log_entry.author %}by {{log_entry.author}}{% endif %}
at {{ log_entry.pub_date | date:'Y-m-d H:i' }}</a></li>
{% endfor %}
</ul>
{% else %}
<p>No log entries yet!</p>
{% endif %}
{% endtimezone %}

View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
{% load tz %}
{% timezone "Europe/Copenhagen" %}
<title>Log entry of {{ log_entry.pub_date | date:'Y-m-d H:i' }}{% if log_entry.author %}
by {{ log_entry.author }}{% endif %}</title>
<h1>Log entry of {{ log_entry.pub_date | date:'Y-m-d H:i' }}{% if log_entry.author %}
by {{ log_entry.author }}{% endif %}</h1>
<p>{{ log_entry.log_text|linebreaks }}</p>
{% endtimezone %}

View File

@ -0,0 +1,8 @@
{% extends django_slack %}
{% load django_slack %}
{% block channel %}#logbook{% endblock %}
{% block text %}
A new log entry!{% if log_entry.author %} By {{ log_entry.author|escapeslack }}{% endif %}
{{ log_entry.log_text|escapeslack }}
{% endblock %}

View File

@ -0,0 +1,7 @@
{% extends django_slack %}
{% load django_slack %}
{% block channel %}#logbook{% endblock %}
{% block text %}
{{ message|escapeslack }}
{% endblock %}

View File

@ -0,0 +1,19 @@
<!DOCTYPE html>
<title>On shift today</title>
<h1>Today's shifts</h1>
<table>
<thead><tr>
<th>Time slot</th>
<th>{# shift status #}</th>
<th>Volunteer</th></tr></thead>
{% for shift in shifts %}
<tr>
<td>{{shift.start_time}} - {{shift.end_time}}</td>
<td>{% if shift.status != 'Assigned' %}{{shift.status}}{% endif %}</td>
<td>{% if 'employee' in shift %}
<a href="{% url 'mellemfolk:employee' shift.employee.id %}">
{{shift.employee.firstName}} {{shift.employee.lastName}}</a>
{% endif %}</td>
</tr>
{% endfor %}
</table>

View File

@ -2,6 +2,18 @@ from django.urls import path
from . import views
app_name = 'mellemfolk'
urlpatterns = [
path('', views.index, name='index'),
path('onshift/', views.onshift_today, name='onshift'),
path('onshift/now/', views.onshift_now_view, name='onshift_now'),
path('employee/<int:employee_id>', views.employee, name='employee'),
path('log/', views.log_entries, name='log_entries'),
path('log/<int:log_entry_id>', views.log_entry, name='log_entry'),
path('log/submit', views.submit_log_entry, name='submit_log_entry'),
path('cleaning-task/', views.cleaning_schedule, name='cleaning_schedule'),
path('cleaning-task/now/', views.cleaning_schedule_now, name='cleaning_schedule_now'),
path('cleaning-task/<int:cleaning_task_id>', views.cleaning_task, name='cleaning_task'),
path('cleaning-task/<int:cleaning_task_id>/signoff', views.signoff_task, name='signoff_task'),
]

View File

@ -1,11 +1,19 @@
from django.shortcuts import render
from django.http import HttpResponse
import django.utils.html
from django.shortcuts import get_object_or_404, render
from django.http import HttpResponse, HttpResponseRedirect
from django.utils.html import format_html
from django.utils.dateparse import parse_datetime
from django.utils import timezone
from django.urls import reverse
from django.conf import settings
import pytz
import datetime
from planday import Planday
from django_slack import slack_message
from .models import LogEntry, CleaningTask, CleaningTaskSignoff
local_timezone = pytz.timezone('Europe/Copenhagen')
planday = Planday(settings.PLANDAY_APPID, settings.PLANDAY_REFRESH_TOKEN)
planday.refresh_access_token()
@ -18,39 +26,133 @@ def get_employee(employee_id):
# FIXME: Error handling
if employee_id in employee_cache:
return employee_cache[employee_id]
employee = planday.get_employee(employee_id)['data']
employee = planday.get_employee(employee_id)
if employee == None:
return
employee_cache[employee_id] = employee
return employee
def index(request):
shifts = planday.get_shifts(from_=today(), to=today())['data']
response = HttpResponse()
response.write('<!DOCTYPE html><title>On shift today</title>')
response.write("<h1>Today's shifts</h1>")
response.write('<table>')
response.write('<thead><tr><th>Time slot</th>')
response.write('<th>Volunteer</th></tr></thead>')
return onshift_today(request)
def onshift_today(request):
shifts = planday.get_shifts(from_=today(), to=today())
for shift in shifts:
status = shift['status']
timeslot = "[{0} - {1}]".format(
shift['startDateTime'],
shift['endDateTime'])
response.write('<tr><td>{0}</td>'.format(
django.utils.html.escape(timeslot)))
if status == 'Assigned':
shift['start_time'] = parse_datetime(shift['startDateTime']).time().isoformat()
shift['end_time'] = parse_datetime(shift['endDateTime']).time().isoformat()
if 'employeeId' in shift:
employee_id = shift['employeeId']
employee = get_employee(employee_id)
employee_name = "{0} {1}".format(
employee['firstName'], employee['lastName'])
response.write('<td>{0}</td>'.format(
django.utils.html.escape(employee_name)))
else:
response.write('<td>{0}</td>'.format(
django.utils.html.escape('<{0}>'.format(status))))
response.write('</tr>')
response.write('</table>')
shift['employee'] = employee
return render(request, 'mellemfolk/onshift.html', { 'shifts': shifts })
return response
def onshift_now_view(request):
shifts = shifts_now()
for shift in shifts:
status = shift['status']
shift['start_time'] = parse_datetime(shift['startDateTime']).time().isoformat()
shift['end_time'] = parse_datetime(shift['endDateTime']).time().isoformat()
if 'employeeId' in shift:
employee_id = shift['employeeId']
employee = get_employee(employee_id)
shift['employee'] = employee
return render(request, 'mellemfolk/onshift.html', { 'shifts': shifts })
def shifts_now():
now = timezone.now()
now = now.astimezone(local_timezone)
def is_now(shift):
start = local_timezone.localize(parse_datetime(shift['startDateTime']))
end = local_timezone.localize(parse_datetime(shift['endDateTime']))
return start <= now <= end
shifts = planday.get_shifts(from_=today(), to=today())
return list(filter(is_now, shifts))
def employee(request, employee_id):
employee = get_employee(employee_id)
return render(request, 'mellemfolk/employee.html',
{ 'first_name': employee['firstName'],
'last_name': employee['lastName'],
'email': employee['email'], 'cell_phone': employee['cellPhone'],
})
def submit_log_entry(request):
if 'log_text' not in request.POST:
return # FIXME
log_text = request.POST['log_text']
author = request.POST.get('author', default='')
log_entry = LogEntry(author=author,
log_text=log_text, pub_date=timezone.now())
log_entry.save()
s = slack_message('mellemfolk/log_entry.slack', {
'log_entry': log_entry,
})
return HttpResponseRedirect(reverse('mellemfolk:log_entry', args=(log_entry.id,)))
def log_entry(request, log_entry_id):
log_entry = get_object_or_404(LogEntry, pk=log_entry_id)
return render(request, 'mellemfolk/log-entry.html',
{ 'log_entry': log_entry })
def log_entries(request):
latest_log_entries = LogEntry.objects.order_by('-pub_date')[:25]
employees = filter(None, map(
lambda s: planday.get_employee(s['employeeId'])
if 'employeeId' in s
else None,
shifts_now()))
employees = list(map(lambda e: "{firstName} {lastName}".format(**e),
employees))
return render(request, 'mellemfolk/log-entries.html',
{ 'latest_log_entries': latest_log_entries,
'employees': employees, })
def cleaning_task(request, cleaning_task_id):
cleaning_task = CleaningTask.objects.get(pk=cleaning_task_id)
employees = filter(None, map(
lambda s: planday.get_employee(s['employeeId'])
if 'employeeId' in s
else None,
shifts_now()))
employees = " & ".join(map(
lambda e: "{0} {1}".format(e['firstName'], e['lastName']),
employees))
signoffs = []
signoffs = CleaningTaskSignoff.objects.filter(task=cleaning_task)\
.order_by('-date')[:5]
return render(request, 'mellemfolk/cleaning-task.html', {
'task': cleaning_task,
'employees': employees,
'signoffs': signoffs, })
def cleaning_schedule(request):
tasks = CleaningTask.objects.order_by('shift__day_of_the_week')\
.order_by('shift__shift_slot__start')
if 'show-inactive' not in request.GET:
tasks = tasks.filter(active=True)
return render(request, 'mellemfolk/cleaning-schedule.html',
{ 'tasks': tasks })
def cleaning_schedule_now(request):
now = timezone.now().astimezone(local_timezone)
day_of_the_week = now.weekday()
time = now.time()
tasks = CleaningTask.objects.filter(active=True,
shift__day_of_the_week=day_of_the_week,
shift__shift_slot__start__lte=time,
shift__shift_slot__end__gte=time)
return render(request, 'mellemfolk/cleaning-schedule.html',
{ 'tasks': tasks })
def signoff_task(request, cleaning_task_id):
if 'signoff' not in request.POST:
return # FIXME
employee = request.POST['signoff']
task = CleaningTask.objects.get(pk=cleaning_task_id)
signoff = CleaningTaskSignoff(task = task, employee = employee)
signoff.save()
return HttpResponseRedirect(reverse('mellemfolk:cleaning_task', args=(cleaning_task_id,)))
def slack(request):
return HttpResponse('Sent message to slack: {}'.format(s))

View File

@ -28,6 +28,8 @@ ALLOWED_HOSTS = ['gaia', 'mf.reynir.dk']
# Application definition
INSTALLED_APPS = [
'onshift.apps.OnshiftConfig',
'django_slack',
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@ -117,3 +119,7 @@ USE_TZ = True
STATIC_URL = '/static/'
from .secret_settings import *
SLACK_CHANNEL = '#logbook'
SLACK_AS_USER = True
SLACK_BACKEND = "django_slack.backends.UrllibBackend"

View File

@ -17,6 +17,6 @@ from django.contrib import admin
from django.urls import include, path
urlpatterns = [
path('onshift/', include('onshift.urls')),
path('mellemfolk/', include('onshift.urls')),
path('admin/', admin.site.urls),
]