diff --git a/dashboard/dashboard_links.py b/dashboard/dashboard_links.py index d6b498f4..f8c6df4e 100644 --- a/dashboard/dashboard_links.py +++ b/dashboard/dashboard_links.py @@ -17,6 +17,7 @@ def get_navigation_links(cls): (_("Start"), cls.get_index_link()), (_("Persönliche Übersicht"), reverse_lazy('dashboard:personal_overview')), (_("Personal"), reverse_lazy('dashboard:staff:index')), + (_("Plan"), reverse_lazy('dashboard:plan:overview')), (_("Ersties"), reverse_lazy('dashboard:students:index')), (_("Klausur"), reverse_lazy('dashboard:exam:assignment')), (_("Kleidung"), reverse_lazy('dashboard:clothing:order_overview')), diff --git a/dashboard/urls.py b/dashboard/urls.py index e24de416..29fc5c3b 100644 --- a/dashboard/urls.py +++ b/dashboard/urls.py @@ -14,4 +14,5 @@ url(r'^students/', include('students.dashboard_urls', namespace='students')), url(r'^exam/', include('exam.dashboard_urls', namespace='exam')), url(r'^clothing/', include('clothing.dashboard_urls', namespace='clothing')), + url(r'^plan/', include('plan.dashboard_urls', namespace='plan')), ] diff --git a/plan/__init__.py b/plan/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plan/admin.py b/plan/admin.py new file mode 100644 index 00000000..32670f1e --- /dev/null +++ b/plan/admin.py @@ -0,0 +1,35 @@ +from django.contrib import admin +from django.urls import reverse +from django.utils.html import format_html + +from plan.admin_actions import update_attendees +from plan.models import TimeSlot, SlotType, Event, Booking +from django.utils.translation import ugettext as _ + + +@admin.register(SlotType) +class SlotTypeAdmin(admin.ModelAdmin): + list_display = ['name', 'split_groups'] + + +@admin.register(TimeSlot) +class TimeSlotAdmin(admin.ModelAdmin): + list_display = ['name', 'begin', 'end', 'relevant_for', 'link_attendance_list'] + actions = [update_attendees] + + @staticmethod + def link_attendance_list(event): + return format_html('{name}', + url=reverse('admin:staff_attendance_changelist'), + id=event.pk, + name=_('Teilnehmerliste')) + + +@admin.register(Event) +class EventAdmin(admin.ModelAdmin): + list_display = ['timeslot', 'room', 'tutorgroup'] + + +@admin.register(Booking) +class BookingAdmin(admin.ModelAdmin): + list_display = ['room', 'begin', 'end', 'status'] diff --git a/plan/admin_actions.py b/plan/admin_actions.py new file mode 100644 index 00000000..a85972c4 --- /dev/null +++ b/plan/admin_actions.py @@ -0,0 +1,13 @@ +from staff.models import Attendance +from django.utils.translation import ugettext as _ + + +def update_attendees(modeladmin, request, queryset): + for event in queryset: + existing_pesons = event.attendance_set.values("person") + relevant_for_queryset = event.relevant_for.get_filtered_staff() + new_attendees = relevant_for_queryset.exclude(pk__in=existing_pesons) + + Attendance.objects.bulk_create([Attendance(event=event, person=person, status='x') for person in new_attendees]) + modeladmin.message_user(request, _("Teilnehmerliste aktualisiert.")) +update_attendees.short_description = _("Liste der Teilnehmer aktualisieren (anhand des Zielgruppenfilters)") diff --git a/plan/apps.py b/plan/apps.py new file mode 100644 index 00000000..7cf7268a --- /dev/null +++ b/plan/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class PlanConfig(AppConfig): + name = 'plan' diff --git a/plan/dashboard_urls.py b/plan/dashboard_urls.py new file mode 100644 index 00000000..47ede698 --- /dev/null +++ b/plan/dashboard_urls.py @@ -0,0 +1,13 @@ +from django.conf.urls import url + +from plan.dashboard_views import PlanOverview, TimeSlotCreateView, TimeSlotCreateSuccessView, PlanCategoryView, \ + PlanCategoryPublicView + +app_name = 'plan' +urlpatterns = [ + url(r'^schedule/$', PlanOverview.as_view(), name='overview'), + url(r'^schedule/(?P[^/]+)/$', PlanCategoryView.as_view(), name='overview_category'), + url(r'^schedule/(?P[^/]+)/public$', PlanCategoryPublicView.as_view(), name='overview_category_public'), + url(r'^timeslot/new/$', TimeSlotCreateView.as_view(), name='timeslot_create'), + url(r'^timeslot/new/success/$', TimeSlotCreateSuccessView.as_view(), name='timeslot_create_success'), +] diff --git a/plan/dashboard_views.py b/plan/dashboard_views.py new file mode 100644 index 00000000..39f8adc2 --- /dev/null +++ b/plan/dashboard_views.py @@ -0,0 +1,63 @@ +from django.urls import reverse_lazy +from django.views.generic import CreateView, ListView, TemplateView, DetailView +from django.utils.translation import ugettext_lazy as _ + +from dashboard.components import DashboardAppMixin +from ophasebase.models import OphaseCategory, Ophase +from plan.forms import TimeSlotForm +from plan.models import TimeSlot + + +class PlanAppMixin(DashboardAppMixin): + app_name_verbose = "Plan" + app_name = 'plan' + permissions = ['plan.add_timeslot'] + + @property + def sidebar_links(self): + return [ + (_('Übersicht'), self.prefix_reverse_lazy('overview')), + (_('Neuer Timeslot'), self.prefix_reverse_lazy('timeslot_create')), + ] + + +class PlanOverview(PlanAppMixin, ListView): + model = TimeSlot + context_object_name = "time_slots" + template_name = "plan/schedule.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["categories"] = Ophase.current().ophaseactivecategory_set.all() + return context + + +class PlanCategoryView(PlanAppMixin, DetailView): + model = OphaseCategory + context_object_name = "category" + template_name = "plan/schedule_category.html" + + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["categories"] = Ophase.current().ophaseactivecategory_set.all() + context["time_slots"] = context["category"].timeslot_set.all() + return context + + +class PlanCategoryPublicView(PlanCategoryView): + def get_context_data(self, **kwargs): + context = super().get_context_data(**kwargs) + context["time_slots"] = context["time_slots"].filter(public=True) + context["public"] = True + return context + + +class TimeSlotCreateView(PlanAppMixin, CreateView): + success_url = reverse_lazy("dashboard:plan:timeslot_create_success") + template_name = "plan/timeslot_create.html" + model = TimeSlot + form_class = TimeSlotForm + + +class TimeSlotCreateSuccessView(PlanAppMixin, TemplateView): + template_name = "plan/timeslot_create_success.html" diff --git a/plan/dashboard_widgets.py b/plan/dashboard_widgets.py new file mode 100644 index 00000000..e69de29b diff --git a/plan/forms.py b/plan/forms.py new file mode 100644 index 00000000..52094804 --- /dev/null +++ b/plan/forms.py @@ -0,0 +1,14 @@ +from django import forms + +from .models import TimeSlot + + +class TimeSlotForm(forms.ModelForm): + class Meta: + model = TimeSlot + fields = ['name', 'slottype', 'begin', 'end', 'category', 'relevant_for', 'attendance_required', 'public'] + widgets = { + # use checkboxes for multipleChoice @see http://stackoverflow.com/a/16937145 + 'category': forms.CheckboxSelectMultiple, + # TODO Use DateTimePicker + } diff --git a/plan/migrations/0001_initial.py b/plan/migrations/0001_initial.py new file mode 100644 index 00000000..d3b6044c --- /dev/null +++ b/plan/migrations/0001_initial.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.5 on 2017-04-10 14:47 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('staff', '0018_person_tutor_experience'), + ('ophasebase', '0007_auto_20170215_1416'), + ] + + operations = [ + migrations.CreateModel( + name='SlotType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('color', models.CharField(default='#FFFFFF', max_length=7)), + ], + options={ + 'verbose_name_plural': 'Veranstaltungsarten', + 'verbose_name': 'Veranstaltungsart', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='TimeSlot', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='Name')), + ('begin', models.DateTimeField(verbose_name='Beginn')), + ('end', models.DateTimeField(verbose_name='Ende')), + ('attendance_required', models.BooleanField(default=False)), + ('public', models.BooleanField(default=False)), + ('category', models.ManyToManyField(to='ophasebase.OphaseCategory')), + ('ophase', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ophasebase.Ophase')), + ('relevant_for', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='staff.StaffFilterGroup', verbose_name='Filterkriterium: Wer muss anwesend sein?')), + ('room', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ophasebase.Room', verbose_name='Raum')), + ('slottype', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plan.SlotType')), + ], + options={ + 'verbose_name_plural': 'Zeitslots', + 'verbose_name': 'Zeitslot', + 'ordering': ['begin'], + }, + ), + ] diff --git a/plan/migrations/0002_auto_20170410_1702.py b/plan/migrations/0002_auto_20170410_1702.py new file mode 100644 index 00000000..d57eee05 --- /dev/null +++ b/plan/migrations/0002_auto_20170410_1702.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-10 15:02 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('staff', '0018_person_tutor_experience'), + ('ophasebase', '0007_auto_20170215_1416'), + ('plan', '0001_initial'), + ] + + operations = [ + migrations.CreateModel( + name='Event', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('room', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='ophasebase.Room', verbose_name='Raum')), + ], + options={ + 'verbose_name': 'Event', + 'verbose_name_plural': 'Events', + }, + ), + migrations.RemoveField( + model_name='timeslot', + name='room', + ), + migrations.AddField( + model_name='event', + name='timeslot', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='plan.TimeSlot', verbose_name='Zeitslot'), + ), + migrations.AddField( + model_name='event', + name='tutorgroup', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='staff.TutorGroup', verbose_name='Kleingruppe'), + ), + migrations.AlterUniqueTogether( + name='event', + unique_together=set([('timeslot', 'tutorgroup')]), + ), + ] diff --git a/plan/migrations/0003_auto_20170410_1825.py b/plan/migrations/0003_auto_20170410_1825.py new file mode 100644 index 00000000..0418e0d4 --- /dev/null +++ b/plan/migrations/0003_auto_20170410_1825.py @@ -0,0 +1,37 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-10 16:25 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('ophasebase', '0007_auto_20170215_1416'), + ('plan', '0002_auto_20170410_1702'), + ] + + operations = [ + migrations.CreateModel( + name='Booking', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('begin', models.DateTimeField(verbose_name='Anfang')), + ('end', models.DateTimeField(verbose_name='Ende')), + ('status', models.PositiveSmallIntegerField(choices=[(1, 'Gebucht'), (2, 'Blockiert'), (3, 'Anfragen?'), (4, 'Angefragt'), (5, 'Überlappung')])), + ('comment', models.TextField(blank=True, verbose_name='Kommentar')), + ('room', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='ophasebase.Room', verbose_name='Raum')), + ], + options={ + 'verbose_name': 'Raumbuchung', + 'verbose_name_plural': 'Raumbuchungen', + }, + ), + migrations.AlterField( + model_name='timeslot', + name='relevant_for', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='staff.StaffFilterGroup', verbose_name='Filterkriterium: Wer könnte anwesend sein?'), + ), + ] diff --git a/plan/migrations/0004_auto_20170410_1946.py b/plan/migrations/0004_auto_20170410_1946.py new file mode 100644 index 00000000..c6737cd2 --- /dev/null +++ b/plan/migrations/0004_auto_20170410_1946.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-10 17:46 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('plan', '0003_auto_20170410_1825'), + ] + + operations = [ + migrations.AlterModelOptions( + name='booking', + options={'ordering': ['room', '-begin'], 'verbose_name': 'Raumbuchung', 'verbose_name_plural': 'Raumbuchungen'}, + ), + ] diff --git a/plan/migrations/0005_auto_20170813_1338.py b/plan/migrations/0005_auto_20170813_1338.py new file mode 100644 index 00000000..0c384527 --- /dev/null +++ b/plan/migrations/0005_auto_20170813_1338.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-13 11:38 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('plan', '0004_auto_20170410_1946'), + ] + + operations = [ + migrations.AddField( + model_name='slottype', + name='split_groups', + field=models.BooleanField(default=False, verbose_name='In Kleingruppen aufteilen?'), + ), + migrations.AlterField( + model_name='timeslot', + name='relevant_for', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='staff.StaffFilterGroup', verbose_name='Filterkriterium: Wer könnte anwesend sein?'), + ), + ] diff --git a/plan/migrations/__init__.py b/plan/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/plan/models.py b/plan/models.py new file mode 100644 index 00000000..83efd955 --- /dev/null +++ b/plan/models.py @@ -0,0 +1,132 @@ +from django.db import models +from django.db.models.signals import post_save, m2m_changed +from django.dispatch import receiver +from django.utils.translation import ugettext_lazy as _ + +from ophasebase.models import Ophase, Room, OphaseCategory +from staff.models import TutorGroup + + +class SlotType(models.Model): + class Meta: + verbose_name = _("Veranstaltungsart") + verbose_name_plural = _("Veranstaltungsarten") + ordering = ['name'] + + name = models.CharField(max_length=50, blank=False) + color = models.CharField(max_length=7, default="#FFFFFF", blank=False) + split_groups = models.BooleanField(verbose_name=_("In Kleingruppen aufteilen?"), default=False, blank=True) + + def __str__(self): + return self.name + + +class TimeSlot(models.Model): + """Time slot for events""" + class Meta: + verbose_name = _("Zeitslot") + verbose_name_plural = _("Zeitslots") + ordering = ['begin'] + + name = models.CharField(max_length=100, verbose_name=_("Name")) + slottype = models.ForeignKey(SlotType, on_delete=models.CASCADE) + begin = models.DateTimeField(verbose_name=_("Beginn")) + end = models.DateTimeField(verbose_name=_("Ende")) + category = models.ManyToManyField(OphaseCategory) + relevant_for = models.ForeignKey("staff.StaffFilterGroup", verbose_name=_("Filterkriterium: Wer könnte anwesend sein?"), blank=True, null=True, on_delete=models.SET_NULL) + attendance_required = models.BooleanField(blank=True, default=False) + ophase = models.ForeignKey(Ophase, models.CASCADE) + public = models.BooleanField(blank=True, default=False) + + @classmethod + def get_current_events(cls, **kwargs): + return cls.objects.filter(ophase=Ophase.current(), **kwargs) + + def __str__(self): + return self.name + + def save(self, *args, **kwargs): + if self.ophase_id is None: + # set Ophase to current active one. We assume that there is only one active Ophase at the same time! + self.ophase = Ophase.current() + super().save(*args, **kwargs) + + +class Event(models.Model): + class Meta: + verbose_name = _('Event') + verbose_name_plural = _('Events') + unique_together = ['timeslot', 'tutorgroup'] + + timeslot = models.ForeignKey(TimeSlot, verbose_name=_('Zeitslot'), on_delete=models.CASCADE) + room = models.ForeignKey(Room, null=True, verbose_name=_("Raum"), blank=True, on_delete=models.SET_NULL) + tutorgroup = models.ForeignKey(TutorGroup, null=True, verbose_name=_('Kleingruppe'), blank=True, on_delete=models.CASCADE) + + def __str__(self): + if self.tutorgroup is not None: + return "{} ({})".format(str(self.timeslot), str(self.tutorgroup)) + return str(self.timeslot) + + @staticmethod + @receiver(post_save, sender=TimeSlot) + def handle_timeslot_created(sender, instance, created, **kwargs): + """ + Create corresponding event for a recently created timeslot + Only those kind of timeslot that require only one event are considered by this callback + + :param created: A boolean; True if a new record was created + :param instance: The actual instance being saved (timeslot) + :param sender: The model class + """ + + # Create a single event if groups don't have to be split + if not instance.slottype.split_groups: + Event.objects.create(timeslot=instance) + + @staticmethod + # @receiver(m2m_changed, sender=TimeSlot.category.though) + def handle_timeslot_created(sender, instance, action, **kwargs): + """ + Create corresponding events for a recently created timeslot + Only those kind of timeslot that require only mulitple event are considered by this callback + + :param instance: The actual instance being saved (timeslot) + :param sender: The model class + """ + + # Added categories? Split groups? + if action == "post_add" and instance.slottype.split_groups: + # Create events for all tutorgroups in all categories + for category in instance.category.all(): + for tg in category.tutorgroup_set.all(): + if not Event.objects.filter(timeslot=instance, tutorgroup=tg).exists(): + Event.objects.create( + timeslot=instance, + tutorgroup=tg + ) + +m2m_changed.connect(Event.handle_timeslot_created, sender=TimeSlot.category.through) + + +class Booking(models.Model): + class Meta: + verbose_name = _('Raumbuchung') + verbose_name_plural = _('Raumbuchungen') + ordering = ['room', '-begin'] + + STATUS_CHOICES = ( + (1, _('Gebucht')), + (2, _('Blockiert')), + (3, _('Anfragen?')), + (4, _('Angefragt')), + (5, _('Überlappung')), + ) + + room = models.ForeignKey(Room, verbose_name=_('Raum'), on_delete=models.CASCADE) + begin = models.DateTimeField(verbose_name=_('Anfang')) + end = models.DateTimeField(verbose_name=_('Ende')) + status = models.PositiveSmallIntegerField(choices=STATUS_CHOICES) + comment = models.TextField(verbose_name=_('Kommentar'), blank=True) + + def __str__(self): + return "{} ({} - {})".format(self.room, self.begin, self.end) diff --git a/plan/templates/plan/schedule.html b/plan/templates/plan/schedule.html new file mode 100644 index 00000000..7cb5d342 --- /dev/null +++ b/plan/templates/plan/schedule.html @@ -0,0 +1,28 @@ +{% extends "dashboard/action.html" %}{% load bootstrap3 %} +{% load i18n %} + +{% block title %}{% trans "Plan" %}{% endblock %} + +{% block content_dashboard_view %} + +
+ {% trans "Übersicht" %} + {% trans "Komplett:" %} + {% for c in categories %} + {{ c.category.name }} + {% endfor %} + {% trans "Öffentlich:" %} + {% for c in categories %} + {{ c.category.name }} + {% endfor %} +
+ +

{% trans "Plan" %}

+ + {% for ts in time_slots %} + {{ ts.name }}: {{ ts.begin }} - {{ ts.end }}
+ {% empty %} +

{% trans "Keine Termine geplant" %}

+ {% endfor %} + +{% endblock %} diff --git a/plan/templates/plan/schedule_category.html b/plan/templates/plan/schedule_category.html new file mode 100644 index 00000000..855b0ae2 --- /dev/null +++ b/plan/templates/plan/schedule_category.html @@ -0,0 +1,28 @@ +{% extends "dashboard/action.html" %}{% load bootstrap3 %} +{% load i18n %} + +{% block title %}{% trans "Plan" %}{% endblock %} + +{% block content_dashboard_view %} + +
+ {% trans "Übersicht" %} + {% trans "Komplett:" %} + {% for c in categories %} + {{ c.category.name }} + {% endfor %} + {% trans "Öffentlich:" %} + {% for c in categories %} + {{ c.category.name }} + {% endfor %} +
+ +

{% trans "Plan" %}: {{ category.name }} {% if public %}({% trans "Öffentliche Termine" %}){% endif %}

+ + {% for ts in time_slots %} + {{ ts.name }}: {{ ts.begin }} - {{ ts.end }}
+ {% empty %} +

{% trans "Keine Termine geplant" %}

+ {% endfor %} + +{% endblock %} diff --git a/plan/templates/plan/timeslot_create.html b/plan/templates/plan/timeslot_create.html new file mode 100644 index 00000000..f67980c4 --- /dev/null +++ b/plan/templates/plan/timeslot_create.html @@ -0,0 +1,25 @@ +{% extends "dashboard/action.html" %}{% load bootstrap3 %} +{% load i18n %} + +{% block title %}{% trans "TimeSlot anlegen" %}{% endblock %} + +{% block content_dashboard_view %} + +

{% trans "TimeSlot anlegen" %}

+ +
+
+ {% csrf_token %} + {% bootstrap_form form %} + {% buttons %} + + + {% endbuttons %} +
+
+ +{% endblock %} diff --git a/plan/templates/plan/timeslot_create_success.html b/plan/templates/plan/timeslot_create_success.html new file mode 100644 index 00000000..6616fbb3 --- /dev/null +++ b/plan/templates/plan/timeslot_create_success.html @@ -0,0 +1,16 @@ +{% extends "dashboard/action.html" %}{% load bootstrap3 %} +{% load i18n %} + +{% block title %}{% trans "TimeSlot anlegen" %}{% endblock %} + +{% block content_dashboard_view %} +

{% trans "TimeSlot anlegen" %}

+ +
{% trans "TimeSlot angelegt" %}
+ + + +{% endblock %} diff --git a/plan/tests.py b/plan/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/plan/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/plan/views.py b/plan/views.py new file mode 100644 index 00000000..91ea44a2 --- /dev/null +++ b/plan/views.py @@ -0,0 +1,3 @@ +from django.shortcuts import render + +# Create your views here. diff --git a/pyophase/settings.py b/pyophase/settings.py index ba1b3bb5..63829d03 100644 --- a/pyophase/settings.py +++ b/pyophase/settings.py @@ -53,6 +53,7 @@ 'exam', 'workshops', 'clothing', + 'plan' ) MIDDLEWARE = ( diff --git a/staff/admin.py b/staff/admin.py index b6508af5..4632bf62 100644 --- a/staff/admin.py +++ b/staff/admin.py @@ -14,7 +14,7 @@ staff_nametag_export, staff_overview_export, tutorgroup_export, - update_attendees, mark_attendance_x, mark_attendance_a, mark_attendance_e, mark_phoned_x, mark_phoned_e, + mark_attendance_x, mark_attendance_a, mark_attendance_e, mark_phoned_x, mark_phoned_e, mark_phoned_n, mark_attendance_v, generate_orga_cert) from .models import ( HelperJob, @@ -24,7 +24,6 @@ TutorGroup, StaffFilterGroup, Attendance, - AttendanceEvent, OrgaSelectedJob, HelperSelectedJob) @@ -147,19 +146,6 @@ def person_phone(event): return event.person.phone -@admin.register(AttendanceEvent) -class AttendanceAdmin(admin.ModelAdmin): - list_display = ['name', 'begin', 'end', 'required_for', 'link_attendance_list'] - actions = [update_attendees] - - @staticmethod - def link_attendance_list(event): - return format_html('{name}', - url=reverse('admin:staff_attendance_changelist'), - id=event.pk, - name=_('Teilnehmerliste')) - - @admin.register(Settings) class SettingsAdmin(admin.ModelAdmin): list_display = ['tutor_registration_enabled', 'orga_registration_enabled', 'helper_registration_enabled'] diff --git a/staff/admin_actions.py b/staff/admin_actions.py index 2f530bdf..d6c38271 100644 --- a/staff/admin_actions.py +++ b/staff/admin_actions.py @@ -246,17 +246,6 @@ def generate_orga_cert(modeladmin, request, queryset): generate_orga_cert.short_description = _('Orga-Bescheinigungen drucken') -def update_attendees(modeladmin, request, queryset): - for event in queryset: - existing_pesons = event.attendance_set.values("person") - required_for_queryset = event.required_for.get_filtered_staff() - new_attendees = required_for_queryset.exclude(pk__in=existing_pesons) - - Attendance.objects.bulk_create([Attendance(event=event, person=person, status='x') for person in new_attendees]) - modeladmin.message_user(request, _("Teilnehmerliste aktualisiert.")) -update_attendees.short_description = _("Liste der Teilnehmer aktualisieren (anhand des Zielgruppenfilters)") - - def mark_attendance_x(modeladmin, request, queryset): queryset.update(status='x') modeladmin.message_user(request, _("Als 'nicht anwesend' markiert.")) diff --git a/staff/dashboard_views.py b/staff/dashboard_views.py index 6e0db1f2..9fdc85d3 100644 --- a/staff/dashboard_views.py +++ b/staff/dashboard_views.py @@ -8,8 +8,9 @@ from dashboard.components import DashboardAppMixin from ophasebase.models import Ophase, OphaseCategory +from plan.models import TimeSlot from .dashboard_forms import GroupMassCreateForm, TutorPairingForm -from .models import Person, TutorGroup, AttendanceEvent, OrgaJob, OrgaSelectedJob, HelperJob, HelperSelectedJob +from .models import Person, TutorGroup, OrgaJob, OrgaSelectedJob, HelperJob, HelperSelectedJob class StaffAppMixin(DashboardAppMixin): @@ -152,13 +153,13 @@ class TutorPairingSuccess(StaffAppMixin, TemplateView): class AttendanceEventIndexView(StaffAppMixin, ListView): permissions = ['staff.edit_attendance'] - model = AttendanceEvent + model = TimeSlot template_name = "staff/dashboard/events_overview.html" context_object_name = "events" class AttendanceEventDetailView(StaffAppMixin, DetailView): permissions = ['staff.edit_attendance'] - model = AttendanceEvent + model = TimeSlot template_name = "staff/dashboard/event.html" context_object_name = "event" diff --git a/staff/migrations/0019_auto_20170410_1946.py b/staff/migrations/0019_auto_20170410_1946.py new file mode 100644 index 00000000..0d3065cf --- /dev/null +++ b/staff/migrations/0019_auto_20170410_1946.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11 on 2017-04-10 17:46 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('staff', '0018_person_tutor_experience'), + ] + + operations = [ + migrations.RemoveField( + model_name='attendanceevent', + name='ophase', + ), + migrations.RemoveField( + model_name='attendanceevent', + name='required_for', + ), + migrations.RemoveField( + model_name='attendanceevent', + name='room', + ), + migrations.AlterField( + model_name='attendance', + name='event', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, to='plan.TimeSlot', verbose_name='Anwesenheitstermin'), + ), + migrations.DeleteModel( + name='AttendanceEvent', + ), + ] diff --git a/staff/migrations/0023_merge_20170813_1201.py b/staff/migrations/0023_merge_20170813_1201.py new file mode 100644 index 00000000..a81cc96f --- /dev/null +++ b/staff/migrations/0023_merge_20170813_1201.py @@ -0,0 +1,16 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.4 on 2017-08-13 10:01 +from __future__ import unicode_literals + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('staff', '0019_auto_20170410_1946'), + ('staff', '0022_auto_20170706_0002'), + ] + + operations = [ + ] diff --git a/staff/models.py b/staff/models.py index 094bfaf9..4598c81c 100644 --- a/staff/models.py +++ b/staff/models.py @@ -266,7 +266,7 @@ class Meta: ("e", _("angerufen + erreicht")) ) - event = models.ForeignKey("AttendanceEvent", on_delete=models.CASCADE, verbose_name=_("Anwesenheitstermin")) + event = models.ForeignKey("plan.TimeSlot", null=True, on_delete=models.CASCADE, verbose_name=_("Anwesenheitstermin")) person = models.ForeignKey(Person, on_delete=models.CASCADE, verbose_name=_("Person")) status = models.CharField(max_length=1, choices=STATUS_CHOICES, default="x", verbose_name=_('Status')) phone_status = models.CharField(max_length=1, choices=PHONECALL_CHOICES, default="x", verbose_name=_('Telefoniestatus')) @@ -276,28 +276,6 @@ def __str__(self): return "{} @ {}: {} ({})".format(self.person, self.event, self.get_status_display(), self.get_phone_status_display()) -class AttendanceEvent(models.Model): - """An attendance event""" - class Meta: - verbose_name = _("Anwesenheitstermin") - verbose_name_plural = _("Anwesenheitstermine") - ordering = ['begin'] - - name = models.CharField(max_length=100, verbose_name=_("Name")) - begin = models.DateTimeField(verbose_name=_("Beginn")) - end = models.DateTimeField(verbose_name=_("Ende")) - required_for = models.ForeignKey(StaffFilterGroup, verbose_name=_("Filterkriterium: Wer muss anwesend sein?"), null=True, on_delete=models.SET_NULL) - ophase = models.ForeignKey(Ophase, models.CASCADE) - room = models.ForeignKey(Room, null=True, verbose_name=_("Raum"), blank=True, on_delete=models.SET_NULL) - - @staticmethod - def get_current_events(**kwargs): - return AttendanceEvent.objects.filter(ophase=Ophase.current(), **kwargs) - - def __str__(self): - return self.name - - class Settings(models.Model): """Configuration for Staff App.""" class Meta: