diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6470abff..083061a4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: args: [--fix=lf] - id: requirements-txt-fixer - repo: https://github.com/paulhfischer/pre-commit-hooks - rev: v1.2.39 + rev: v1.2.40 hooks: - id: format-general - id: format-web @@ -28,11 +28,11 @@ repos: hooks: - id: isort - repo: https://github.com/asottile/add-trailing-comma - rev: v2.2.1 + rev: v2.2.2 hooks: - id: add-trailing-comma - repo: https://github.com/psf/black - rev: 22.1.0 + rev: 22.3.0 hooks: - id: black language_version: python3 diff --git a/bags/locale/de/LC_MESSAGES/django.po b/bags/locale/de/LC_MESSAGES/django.po index 55459307..bb6546e7 100644 --- a/bags/locale/de/LC_MESSAGES/django.po +++ b/bags/locale/de/LC_MESSAGES/django.po @@ -140,6 +140,13 @@ msgstr "E-Mail erfolgreich gesendet" msgid "Promise" msgstr "Zusage" +#: bags/models.py bags/templates/bags/bags_dashboard.html +#: bags/templates/bags/company/list_companys.html +#: bags/templates/bags/company/view_company.html +#: bags/templates/bags/maintenance/mail/send_mail.html +msgid "Contact again" +msgstr "Erneut kontaktieren" + #: bags/models.py bags/templates/bags/company/list_companys.html #: bags/templates/bags/company/view_company.html #: bags/templates/bags/maintenance/mail/del_mail.html @@ -148,13 +155,6 @@ msgstr "Zusage" msgid "Comment" msgstr "Kommentar" -#: bags/models.py bags/templates/bags/bags_dashboard.html -#: bags/templates/bags/company/list_companys.html -#: bags/templates/bags/company/view_company.html -#: bags/templates/bags/maintenance/mail/send_mail.html -msgid "Contact again" -msgstr "Erneut kontaktieren" - #: bags/models.py msgid "Giveaway-groups' name" msgstr "Name der Giveaway-Gruppe" @@ -185,10 +185,6 @@ msgstr "Jede {every_x_bags}te Tüte" msgid "{per_bag_count} every bag" msgstr "{per_bag_count} jede Tüte" -#: bags/models.py -msgid "Giveaway-description/comment" -msgstr "Werbegeschenk-Beschreibung/Kommentar" - #: bags/models.py bags/templates/bags/bags_dashboard.html #: bags/templates/bags/giveaways/giveaway/list/arrival/confirm_giveaways_arrivals.html #: bags/templates/bags/giveaways/giveaway/list/arrival/list_giveaways_arrivals.html @@ -213,6 +209,10 @@ msgstr "Ankunftszeit" msgid "Arrived" msgstr "Angekommen" +#: bags/models.py +msgid "Giveaway-description/comment" +msgstr "Werbegeschenk-Beschreibung/Kommentar" + #: bags/templates/bags/bags_dashboard.html bags/templates/bags/base_bags.html msgid "Dashboard" msgstr "Dashboard" diff --git a/bags/migrations/0004_auto_20160531_1312.py b/bags/migrations/0004_auto_20160531_1312.py index d863ae16..8874a54e 100644 --- a/bags/migrations/0004_auto_20160531_1312.py +++ b/bags/migrations/0004_auto_20160531_1312.py @@ -16,10 +16,7 @@ class Migration(migrations.Migration): "permissions": (("view_companies", "Can view and edit the companies"),), }, ), - migrations.RemoveField( - model_name="company", - name="zusage", - ), + migrations.RemoveField(model_name="company", name="zusage"), migrations.AddField( model_name="company", name="promise", diff --git a/bags/migrations/0008_auto_20160608_1212.py b/bags/migrations/0008_auto_20160608_1212.py index 3ff413a0..48756073 100644 --- a/bags/migrations/0008_auto_20160608_1212.py +++ b/bags/migrations/0008_auto_20160608_1212.py @@ -10,10 +10,7 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( - model_name="company", - name="contact", - ), + migrations.RemoveField(model_name="company", name="contact"), migrations.AddField( model_name="company", name="contact_firstname", diff --git a/bags/migrations/0017_remove_mail_semester.py b/bags/migrations/0017_remove_mail_semester.py index b70259c3..a3eed937 100644 --- a/bags/migrations/0017_remove_mail_semester.py +++ b/bags/migrations/0017_remove_mail_semester.py @@ -18,8 +18,5 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(subject_includes_semester), - migrations.RemoveField( - model_name="mail", - name="semester", - ), + migrations.RemoveField(model_name="mail", name="semester"), ] diff --git a/bags/migrations/0021_auto_20210326_1632.py b/bags/migrations/0021_auto_20210326_1632.py index f5c9d55b..3c682d42 100644 --- a/bags/migrations/0021_auto_20210326_1632.py +++ b/bags/migrations/0021_auto_20210326_1632.py @@ -12,14 +12,8 @@ class Migration(migrations.Migration): ] operations = [ - migrations.RemoveField( - model_name="giveaway", - name="every_x_bags", - ), - migrations.RemoveField( - model_name="giveaway", - name="per_bag_count", - ), + migrations.RemoveField(model_name="giveaway", name="every_x_bags"), + migrations.RemoveField(model_name="giveaway", name="per_bag_count"), migrations.AddField( model_name="giveaway", name="item_count", diff --git a/bags/migrations/0023_auto_20210330_1154.py b/bags/migrations/0023_auto_20210330_1154.py index 8ed19713..6728e6b1 100644 --- a/bags/migrations/0023_auto_20210330_1154.py +++ b/bags/migrations/0023_auto_20210330_1154.py @@ -33,8 +33,5 @@ class Migration(migrations.Migration): ), ), migrations.RunPython(keep_giveaway_data), - migrations.RemoveField( - model_name="giveaway", - name="name", - ), + migrations.RemoveField(model_name="giveaway", name="name"), ] diff --git a/bags/migrations/0026_bagsettings_created_at_bagsettings_updated_at_and_more.py b/bags/migrations/0026_bagsettings_created_at_bagsettings_updated_at_and_more.py new file mode 100644 index 00000000..052e7441 --- /dev/null +++ b/bags/migrations/0026_bagsettings_created_at_bagsettings_updated_at_and_more.py @@ -0,0 +1,58 @@ +# Generated by Django 4.0.3 on 2022-04-01 13:10 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("bags", "0025_auto_20220321_1902"), + ] + + operations = [ + migrations.AddField( + model_name="bagsettings", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="bagsettings", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="company", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="company", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="giveaway", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="giveaway", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="giveawaygroup", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="giveawaygroup", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/bags/migrations/0027_alter_company_semester_alter_giveawaygroup_semester.py b/bags/migrations/0027_alter_company_semester_alter_giveawaygroup_semester.py new file mode 100644 index 00000000..651c6ae4 --- /dev/null +++ b/bags/migrations/0027_alter_company_semester_alter_giveawaygroup_semester.py @@ -0,0 +1,33 @@ +# Generated by Django 4.0.3 on 2022-04-01 16:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("settool_common", "0018_alter_anonymisationlog_semester"), + ("bags", "0026_bagsettings_created_at_bagsettings_updated_at_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="company", + name="semester", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="settool_common.semester", + verbose_name="Semester", + ), + ), + migrations.AlterField( + model_name="giveawaygroup", + name="semester", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="settool_common.semester", + verbose_name="Semester", + ), + ), + ] diff --git a/bags/models.py b/bags/models.py index 22758cb9..bdd43366 100644 --- a/bags/models.py +++ b/bags/models.py @@ -42,39 +42,20 @@ def get_mail_company(self): return self.get_mail(context) -class BagSettings(models.Model): - semester = models.OneToOneField( - Semester, - on_delete=models.CASCADE, - ) - bag_count = models.PositiveSmallIntegerField( - verbose_name=_("Total amount of Bags"), - default=0, - ) +class BagSettings(common_models.LoggedModelBase): + semester = models.OneToOneField(Semester, on_delete=models.CASCADE) + bag_count = models.PositiveSmallIntegerField(verbose_name=_("Total amount of Bags"), default=0) def __str__(self) -> str: return f"Bag-Settings for {self.semester}" -class Company(models.Model): +class Company(common_models.LoggedModelBase, common_models.SemesterModelBase): class Meta: unique_together = ("semester", "name") - permissions = ( - ( - "view_companies", - "Can view and edit the companies", - ), - ) - - semester = models.ForeignKey( - Semester, - on_delete=models.CASCADE, - ) + permissions = (("view_companies", "Can view and edit the companies"),) - name = models.CharField( - _("Name"), - max_length=200, - ) + name = models.CharField(_("Name"), max_length=200) contact_gender = models.CharField( _("Contact person (Gender)"), @@ -82,50 +63,18 @@ class Meta: choices=(("Herr", _("Herr")), ("Frau", _("Frau"))), blank=True, ) + contact_firstname = models.CharField(_("Contact person (First Name)"), max_length=200, blank=True) + contact_lastname = models.CharField(_("Contact person (Last Name)"), max_length=200, blank=True) - contact_firstname = models.CharField( - _("Contact person (First Name)"), - max_length=200, - blank=True, - ) - - contact_lastname = models.CharField( - _("Contact person (Last Name)"), - max_length=200, - blank=True, - ) - - email = models.EmailField( - _("Email address"), - ) - - email_sent = models.BooleanField( - _("Email sent"), - ) - - email_sent_success = models.BooleanField( - _("Email successfully sent"), - ) - - promise = models.BooleanField( - _("Promise"), - null=True, - ) - - comment = models.CharField( - _("Comment"), - max_length=200, - blank=True, - ) + email = models.EmailField(_("Email address")) + email_sent = models.BooleanField(_("Email sent")) + email_sent_success = models.BooleanField(_("Email successfully sent")) - last_year = models.BooleanField( - _("Participated last year"), - ) + promise = models.BooleanField(_("Promise"), null=True) + last_year = models.BooleanField(_("Participated last year")) + contact_again = models.BooleanField(_("Contact again"), null=True) - contact_again = models.BooleanField( - _("Contact again"), - null=True, - ) + comment = models.CharField(_("Comment"), max_length=200, blank=True) def __str__(self) -> str: return str(self.name) @@ -155,19 +104,11 @@ def contact_name(self): return f"{self.contact_firstname} {self.contact_lastname}" -class GiveawayGroup(models.Model): +class GiveawayGroup(common_models.LoggedModelBase, common_models.SemesterModelBase): class Meta: unique_together = (("semester", "name"),) - semester = models.ForeignKey( - Semester, - on_delete=models.CASCADE, - ) - - name = models.CharField( - _("Giveaway-groups' name"), - max_length=200, - ) + name = models.CharField(_("Giveaway-groups' name"), max_length=200) def __str__(self) -> str: return self.name @@ -196,11 +137,8 @@ def custom_per_group_message(self): return _("{per_bag_count} every bag").format(per_bag_count=round(total_items / total_bags, 1)) -class Giveaway(models.Model): - company = models.ForeignKey( - Company, - on_delete=models.CASCADE, - ) +class Giveaway(common_models.LoggedModelBase): + company = models.ForeignKey(Company, on_delete=models.CASCADE) group = models.ForeignKey( GiveawayGroup, verbose_name=_("Giveaway-title/group/tag"), @@ -208,27 +146,12 @@ class Giveaway(models.Model): null=True, ) - comment = models.CharField( - _("Giveaway-description/comment"), - blank=True, - max_length=200, - ) - - item_count = models.PositiveSmallIntegerField( - verbose_name=_("Item Count"), - default=0, - ) + item_count = models.PositiveSmallIntegerField(verbose_name=_("Item Count"), default=0) - arrival_time = models.CharField( - _("Arrival time"), - max_length=200, - blank=True, - ) + arrival_time = models.CharField(_("Arrival time"), max_length=200, blank=True) + arrived = models.BooleanField(_("Arrived"), default=False) - arrived = models.BooleanField( - _("Arrived"), - default=False, - ) + comment = models.CharField(_("Giveaway-description/comment"), blank=True, max_length=200) @property def custom_per_bag_message(self): diff --git a/bags/views.py b/bags/views.py index 45be9e24..e5147b26 100644 --- a/bags/views.py +++ b/bags/views.py @@ -408,7 +408,7 @@ def dashboard(request: WSGIRequest) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) companies: QuerySet[Company] = Company.objects.filter(semester=semester) - g_companies: QuerySet[Company] = Company.objects.filter(giveaway__isnull=False, semester=semester) + g_companies: QuerySet[Company] = companies.filter(giveaway__isnull=False) giveaways: QuerySet[Giveaway] = Giveaway.objects.filter(company__semester=semester) g_by_group = list(giveaways.filter(group__isnull=False).values("group").annotate(group_count=Count("group"))) diff --git a/fahrt/forms.py b/fahrt/forms.py index 43794530..122a0d5f 100644 --- a/fahrt/forms.py +++ b/fahrt/forms.py @@ -214,7 +214,7 @@ def __init__(self, *args, **kwargs): def get_choosable_subjects(self) -> QuerySet[Subject]: choosable_subjects_ids = ( - self.semester.fahrt_participant.filter(status="confirmed") + Participant.objects.filter(semester=self.semester, status="confirmed") .values("subject") .distinct() .values_list("subject", flat=True) @@ -276,7 +276,7 @@ class ParticipantSelectForm(SemesterBasedForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - self.fields["selected"].queryset = self.semester.fahrt_participant.filter(status="confirmed").all() + self.fields["selected"].queryset = Participant.objects.filter(semester=self.semester, status="confirmed").all() class CSVFileUploadForm(forms.Form): diff --git a/fahrt/locale/de/LC_MESSAGES/django.mo b/fahrt/locale/de/LC_MESSAGES/django.mo index f68fab52..61502d41 100644 Binary files a/fahrt/locale/de/LC_MESSAGES/django.mo and b/fahrt/locale/de/LC_MESSAGES/django.mo differ diff --git a/fahrt/locale/de/LC_MESSAGES/django.po b/fahrt/locale/de/LC_MESSAGES/django.po index e51fa56f..e6862fd1 100644 --- a/fahrt/locale/de/LC_MESSAGES/django.po +++ b/fahrt/locale/de/LC_MESSAGES/django.po @@ -142,13 +142,13 @@ msgstr "Der Teilnehmer" msgid "" "If the Email is configured as the fahrt's registration mail, the " "participants' personalised non-liability form is automatically attached. " -"Please notify the Participant to atach his ID (THIS-->{{ participant.uuid }}" -"<--THIS) in the Payment-Subject-Line." +"Please notify the Participant to atach his ID (THIS-->{{ participant.id }}<--" +"THIS) in the Payment-Subject-Line." msgstr "" "Falls eine Anmeldungs Email konfiguriert ist wird der personalisierte " "Haftungsauschuss automatisch an die Teilnehmer*in gesendet, sobald sie sich " "registrieren. Bitte weise den Teilnehmer darauf hin, seine ID (THIS--" -">{{ participant.uuid }}<--THIS) in der Zahlungs-Betreff-Zeile zu nennen." +">{{ participant.id }}<--THIS) in der Zahlungs-Betreff-Zeile zu nennen." #: fahrt/models.py msgid "Date" @@ -235,12 +235,6 @@ msgstr "Bahn ({free_places} frei)" msgid "Car ({free_places} free)" msgstr "Auto ({free_places} frei)" -#: fahrt/models.py fahrt/templates/fahrt/participants/list/list_registered.html -#: fahrt/templates/fahrt/participants/list/list_waitinglist.html -#: fahrt/templates/fahrt/participants/view_participant_details.html -msgid "Registration time" -msgstr "Anmeldezeitpunkt" - #: fahrt/models.py msgid "male" msgstr "Männlich" @@ -366,10 +360,6 @@ msgstr "Text" msgid "{sender} on {commented_on}: {comment_content}" msgstr "{sender} auf {commented_on}: {comment_content}" -#: fahrt/models.py -msgid "Time" -msgstr "Zeit" - #: fahrt/templates/fahrt/base_fahrt.html #: fahrt/templates/fahrt/fahrt_dashboard.html msgid "Dashboard" @@ -775,6 +765,12 @@ msgstr "%(places)s Sitze in %(cars)s Autos" msgid "List registered participants" msgstr "Angemeldete Teilnehmer auflisten" +#: fahrt/templates/fahrt/participants/list/list_registered.html +#: fahrt/templates/fahrt/participants/list/list_waitinglist.html +#: fahrt/templates/fahrt/participants/view_participant_details.html +msgid "Registration time" +msgstr "Anmeldezeitpunkt" + #: fahrt/templates/fahrt/participants/list/list_waitinglist.html msgid "List participants on waitinglist" msgstr "Teilnehmer auf der Warteliste auflisten" @@ -1120,6 +1116,9 @@ msgstr "" "Derzeit ist dies nur eine Chatwall und kein Live-Chat. Das bedeutet, dass du " "die Seite aktualisieren musst, um neue Nachrichten zu erhalten." +#~ msgid "Time" +#~ msgstr "Zeit" + #~ msgid "With car" #~ msgstr "Mit Auto" diff --git a/fahrt/migrations/0006_auto_20160906_1418.py b/fahrt/migrations/0006_auto_20160906_1418.py index 5e596fa6..fadb5508 100644 --- a/fahrt/migrations/0006_auto_20160906_1418.py +++ b/fahrt/migrations/0006_auto_20160906_1418.py @@ -5,7 +5,6 @@ class Migration(migrations.Migration): - dependencies = [ ("settool_common", "0004_auto_20151220_2255"), ("fahrt", "0005_auto_20160906_1411"), @@ -144,9 +143,7 @@ class Migration(migrations.Migration): ), ( "mailinglist", - models.BooleanField( - verbose_name="Mailing list", - ), + models.BooleanField(verbose_name="Mailing list"), ), ( "comment", @@ -182,15 +179,7 @@ class Migration(migrations.Migration): ), ], ), - migrations.RemoveField( - model_name="person", - name="semester", - ), - migrations.RemoveField( - model_name="person", - name="subject", - ), - migrations.DeleteModel( - name="Person", - ), + migrations.RemoveField(model_name="person", name="semester"), + migrations.RemoveField(model_name="person", name="subject"), + migrations.DeleteModel(name="Person"), ] diff --git a/fahrt/migrations/0024_remove_mail_semester.py b/fahrt/migrations/0024_remove_mail_semester.py index ec8b19dd..187032d0 100644 --- a/fahrt/migrations/0024_remove_mail_semester.py +++ b/fahrt/migrations/0024_remove_mail_semester.py @@ -18,8 +18,5 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(subject_includes_semester), - migrations.RemoveField( - model_name="mail", - name="semester", - ), + migrations.RemoveField(model_name="mail", name="semester"), ] diff --git a/fahrt/migrations/0028_auto_20210218_2258.py b/fahrt/migrations/0028_auto_20210218_2258.py index 4aeba313..945ba076 100644 --- a/fahrt/migrations/0028_auto_20210218_2258.py +++ b/fahrt/migrations/0028_auto_20210218_2258.py @@ -32,14 +32,8 @@ class Migration(migrations.Migration): ), ], ), - migrations.RemoveField( - model_name="participant", - name="car", - ), - migrations.RemoveField( - model_name="participant", - name="car_places", - ), + migrations.RemoveField(model_name="participant", name="car"), + migrations.RemoveField(model_name="participant", name="car_places"), migrations.CreateModel( name="TransportationComment", fields=[ diff --git a/fahrt/migrations/0031_rename_deparure_place_transportation_departure_place_and_more.py b/fahrt/migrations/0031_rename_deparure_place_transportation_departure_place_and_more.py new file mode 100644 index 00000000..a49d90de --- /dev/null +++ b/fahrt/migrations/0031_rename_deparure_place_transportation_departure_place_and_more.py @@ -0,0 +1,53 @@ +# Generated by Django 4.0.3 on 2022-04-01 13:10 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("fahrt", "0030_auto_20220321_1911"), + ] + + operations = [ + migrations.AddField( + model_name="fahrt", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField(model_name="fahrt", name="updated_at", field=models.DateTimeField(auto_now=True)), + migrations.AddField( + model_name="logentry", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField(model_name="logentry", name="updated_at", field=models.DateTimeField(auto_now=True)), + migrations.AddField( + model_name="participant", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField(model_name="participant", name="updated_at", field=models.DateTimeField(auto_now=True)), + migrations.AddField( + model_name="transportation", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField(model_name="transportation", name="updated_at", field=models.DateTimeField(auto_now=True)), + migrations.AddField( + model_name="transportationcomment", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="transportationcomment", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/fahrt/migrations/0032_rename_deparure_place_transportation_departure_place_and_more.py b/fahrt/migrations/0032_rename_deparure_place_transportation_departure_place_and_more.py new file mode 100644 index 00000000..755f2d97 --- /dev/null +++ b/fahrt/migrations/0032_rename_deparure_place_transportation_departure_place_and_more.py @@ -0,0 +1,13 @@ +# Generated by Django 4.0.3 on 2022-04-01 16:07 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("fahrt", "0031_rename_deparure_place_transportation_departure_place_and_more"), + ] + + operations = [ + migrations.RenameField(model_name="transportation", old_name="deparure_place", new_name="departure_place"), + ] diff --git a/fahrt/migrations/0033_logentry_participant_uuid_and_more.py b/fahrt/migrations/0033_logentry_participant_uuid_and_more.py new file mode 100644 index 00000000..d6cdc206 --- /dev/null +++ b/fahrt/migrations/0033_logentry_participant_uuid_and_more.py @@ -0,0 +1,143 @@ +# Generated by Django 4.0.3 on 2022-04-04 19:14 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +def migrate_participant_id_for_log_entry(apps, _): + LogEntry = apps.get_model("fahrt", "LogEntry") + for row in LogEntry.objects.all(): + row.participant_uuid_id = row.participant.uuid + row.save(update_fields=["participant_uuid"]) + + +def migrate_participant_id_for_transportation_comment(apps, _): + TransportationComment = apps.get_model("fahrt", "TransportationComment") + for row in TransportationComment.objects.all(): + row.sender_uuid_id = row.sender.uuid + row.save(update_fields=["sender_uuid"]) + + +def migrate_participant_id_for_transportation(apps, _): + Transportation = apps.get_model("fahrt", "Transportation") + for row in Transportation.objects.all(): + row.creator_uuid_id = row.creator.uuid + row.save(update_fields=["creator_uuid"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("fahrt", "0032_rename_deparure_place_transportation_departure_place_and_more"), + ] + operations = [ + migrations.AlterField( + model_name="participant", + name="uuid", + field=models.UUIDField(default=uuid.uuid4, editable=False, serialize=False, unique=True), + ), + # following https://stackoverflow.com/a/48235821 + # add different fk-fields for each participant and logentry + migrations.AddField( + model_name="logentry", + name="participant_uuid", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="logentry_uuid", + to="fahrt.participant", + to_field="uuid", + db_constraint=False, + ), + ), + migrations.AddField( + model_name="transportationcomment", + name="sender_uuid", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="transportationcomment_uuid", + to="fahrt.participant", + to_field="uuid", + db_constraint=False, + ), + ), + migrations.AddField( + model_name="transportation", + name="creator_uuid", + field=models.OneToOneField( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="fahrt_transportation_creator_uuid", + to="fahrt.participant", + to_field="uuid", + db_constraint=False, + ), + ), + migrations.AlterField( + model_name="logentry", + name="participant", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="logentry", + to="fahrt.participant", + ), + ), + migrations.AlterField( + model_name="transportationcomment", + name="sender", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="transportationcomment", + to="fahrt.participant", + ), + ), + # fill new fields with valid data + migrations.RunPython(migrate_participant_id_for_transportation_comment), + migrations.RunPython(migrate_participant_id_for_transportation), + migrations.RunPython(migrate_participant_id_for_log_entry), + # after filling, we can assure the db, that they are filled + migrations.AlterField( + model_name="logentry", + name="participant_uuid", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="logentry_uuid", + to="fahrt.participant", + to_field="uuid", + ), + ), + migrations.AlterField( + model_name="transportationcomment", + name="sender_uuid", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="logentry_uuid", + to="fahrt.participant", + to_field="uuid", + ), + ), + migrations.AlterField( + model_name="transportation", + name="creator_uuid", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="fahrt_transportation_creator_uuid", + to="fahrt.participant", + to_field="uuid", + ), + ), + # remove old fields and rename new ones + migrations.RemoveField(model_name="logentry", name="participant"), + migrations.RenameField(model_name="logentry", old_name="participant_uuid", new_name="participant"), + migrations.RemoveField(model_name="transportationcomment", name="sender"), + migrations.RenameField(model_name="transportationcomment", old_name="sender_uuid", new_name="sender"), + migrations.RemoveField(model_name="transportation", name="creator"), + migrations.RenameField(model_name="transportation", old_name="creator_uuid", new_name="creator"), + ] diff --git a/fahrt/migrations/0034_remove_participant_id_alter_logentry_participant_and_more.py b/fahrt/migrations/0034_remove_participant_id_alter_logentry_participant_and_more.py new file mode 100644 index 00000000..72dbda6e --- /dev/null +++ b/fahrt/migrations/0034_remove_participant_id_alter_logentry_participant_and_more.py @@ -0,0 +1,49 @@ +# Generated by Django 4.0.3 on 2022-04-04 19:44 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("fahrt", "0033_logentry_participant_uuid_and_more"), + ] + + operations = [ + # uuid->id + migrations.RemoveField(model_name="participant", name="id"), + migrations.AlterField( + model_name="participant", + name="uuid", + field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True), + ), + migrations.RenameField( + model_name="participant", + old_name="uuid", + new_name="id", + ), + # cleanup + migrations.AlterField( + model_name="transportationcomment", + name="sender", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="fahrt.participant"), + ), + migrations.AlterField( + model_name="logentry", + name="participant", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="fahrt.participant"), + ), + migrations.AlterField( + model_name="transportation", + name="creator", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="fahrt_transportation_creator", + to="fahrt.participant", + ), + ), + ] diff --git a/fahrt/migrations/0035_alter_participant_semester.py b/fahrt/migrations/0035_alter_participant_semester.py new file mode 100644 index 00000000..4acabfed --- /dev/null +++ b/fahrt/migrations/0035_alter_participant_semester.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.3 on 2022-04-04 22:42 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("settool_common", "0018_alter_anonymisationlog_semester"), + ("fahrt", "0034_remove_participant_id_alter_logentry_participant_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="participant", + name="semester", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="settool_common.semester", + verbose_name="Semester", + ), + ), + ] diff --git a/fahrt/migrations/0036_remove_logentry_time.py b/fahrt/migrations/0036_remove_logentry_time.py new file mode 100644 index 00000000..0c11988a --- /dev/null +++ b/fahrt/migrations/0036_remove_logentry_time.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.3 on 2022-04-05 15:23 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("fahrt", "0035_alter_participant_semester"), + ] + + operations = [ + migrations.RemoveField(model_name="logentry", name="time"), + ] diff --git a/fahrt/migrations/0037_remove_participant_registration_time.py b/fahrt/migrations/0037_remove_participant_registration_time.py new file mode 100644 index 00000000..c4ba69a5 --- /dev/null +++ b/fahrt/migrations/0037_remove_participant_registration_time.py @@ -0,0 +1,22 @@ +# Generated by Django 4.0.3 on 2022-04-05 15:39 + +from django.db import migrations + + +def migrate_participant_registration_time_created_at(apps, schema_editor): + Participant = apps.get_model("fahrt", "Participant") + for participant in Participant.objects.all(): + participant.created_at = participant.registration_time + participant.save(update_fields=["created_at"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("fahrt", "0036_remove_logentry_time"), + ] + + operations = [ + migrations.RunPython(migrate_participant_registration_time_created_at), + migrations.RemoveField(model_name="participant", name="registration_time"), + ] diff --git a/fahrt/models.py b/fahrt/models.py index 595d8584..8539548b 100644 --- a/fahrt/models.py +++ b/fahrt/models.py @@ -1,9 +1,9 @@ import datetime -import uuid from dateutil.relativedelta import relativedelta from django.contrib.auth import get_user_model from django.db import models +from django.http import HttpResponse from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -25,7 +25,7 @@ class FahrtMail(common_models.Mail): notes = _( "If the Email is configured as the fahrt's registration mail, the participants' personalised non-liability " "form is automatically attached. Please notify the Participant to atach his ID " - "(THIS-->{{ participant.uuid }}<--THIS) in the Payment-Subject-Line.", + "(THIS-->{{ participant.id }}<--THIS) in the Payment-Subject-Line.", ) required_perm = common_models.Mail.required_perm + ["fahrt.view_participants"] @@ -35,7 +35,7 @@ def save(self, *args, **kwargs): self.sender = common_models.Mail.SET_FAHRT super().save(*args, **kwargs) - def send_mail_participant(self, participant): + def send_mail_participant(self, participant: "Participant") -> bool: context = { "vorname": participant.firstname, "frist": participant.payment_deadline, @@ -43,7 +43,7 @@ def send_mail_participant(self, participant): } return self.send_mail(context, participant.email) - def send_mail_registration(self, participant, non_liability): + def send_mail_registration(self, participant: "Participant", non_liability: HttpResponse) -> bool: context = { "vorname": participant.firstname, "frist": participant.payment_deadline, @@ -51,7 +51,7 @@ def send_mail_registration(self, participant, non_liability): } return self.send_mail(context, participant.email, attachments=[non_liability]) - def get_mail_participant(self): + def get_mail_participant(self) -> tuple[str, str, str]: context = { "vorname": "", "frist": "", @@ -60,23 +60,12 @@ def get_mail_participant(self): return self.get_mail(context) -class Fahrt(models.Model): - semester = models.OneToOneField( - Semester, - on_delete=models.CASCADE, - ) +class Fahrt(common_models.LoggedModelBase): + semester = models.OneToOneField(Semester, on_delete=models.CASCADE) - date = models.DateField( - _("Date"), - ) - - open_registration = models.DateTimeField( - _("Open registration"), - ) - - close_registration = models.DateTimeField( - _("Close registration"), - ) + date = models.DateField(_("Date")) + open_registration = models.DateTimeField(_("Open registration")) + close_registration = models.DateTimeField(_("Close registration")) mail_registration = models.ForeignKey( FahrtMail, @@ -125,7 +114,7 @@ def registration_open(self): return self.open_registration < timezone.now() < self.close_registration -class Transportation(models.Model): +class Transportation(common_models.LoggedModelBase): CAR = 0 TRAIN = 1 @@ -158,11 +147,7 @@ class Transportation(models.Model): blank=True, ) - deparure_place = models.CharField( - _("The place we will start our trip"), - max_length=100, - blank=True, - ) + departure_place = models.CharField(_("The place we will start our trip"), max_length=100, blank=True) places = models.PositiveSmallIntegerField( _("Number of people (totally) for this mode of transport"), @@ -176,7 +161,7 @@ def __str__(self) -> str: return _("Car ({free_places} free)").format(free_places=free_places) -class Participant(models.Model): +class Participant(common_models.UUIDModelBase, common_models.LoggedModelBase, common_models.SemesterModelBase): class Meta: permissions = ( ( @@ -185,13 +170,9 @@ class Meta: ), ) - uuid = models.UUIDField(unique=True, default=uuid.uuid4) - registration_time = models.DateTimeField(_("Registration time"), auto_now_add=True) - semester = models.ForeignKey( - Semester, - on_delete=models.CASCADE, - related_name="fahrt_participant", - ) + @property + def registration_time(self): + return self.created_at GENDER_CHOICES = ( ("male", _("male")), @@ -219,12 +200,7 @@ class Meta: nutrition = models.CharField(_("Nutrition"), max_length=200, choices=NUTRITION_CHOICES) allergies = models.CharField(_("Allergies"), max_length=200, blank=True) - transportation = models.ForeignKey( - Transportation, - on_delete=models.SET_NULL, - blank=True, - null=True, - ) + transportation = models.ForeignKey(Transportation, on_delete=models.SET_NULL, blank=True, null=True) publish_contact_to_other_paricipants = models.BooleanField( _("Publish your most relevant (mobile > phone > email), contact-info to other Fahrt-participants."), default=False, @@ -291,7 +267,7 @@ def toggle_mailinglist(self) -> None: # self.payment_deadline = deadline.strftime("%d.%m.%Y") -class TransportationComment(models.Model): +class TransportationComment(common_models.LoggedModelBase): sender = models.ForeignKey(Participant, on_delete=models.CASCADE) commented_on = models.ForeignKey(Transportation, on_delete=models.CASCADE) comment_content = models.CharField(_("Text"), max_length=200) @@ -308,8 +284,7 @@ def __str__(self) -> str: ) -class LogEntry(models.Model): - time = models.DateTimeField(_("Time"), auto_now_add=True) +class LogEntry(common_models.LoggedModelBase): participant = models.ForeignKey(Participant, on_delete=models.CASCADE) text = models.CharField(_("Text"), max_length=200) user = models.ForeignKey( @@ -321,4 +296,4 @@ class LogEntry(models.Model): ) def __str__(self) -> str: - return f"{self.time}, {self.user}: {self.text}" + return f"{self.updated_at}, {self.user}: {self.text}" diff --git a/fahrt/templates/fahrt/finanz/simple_finanz.html b/fahrt/templates/fahrt/finanz/simple_finanz.html index c0a39537..cd95377d 100644 --- a/fahrt/templates/fahrt/finanz/simple_finanz.html +++ b/fahrt/templates/fahrt/finanz/simple_finanz.html @@ -55,11 +55,11 @@

{% trans "List of all confirmed participants" %}

{% for participant, select in participants_and_select %} {% if 'fahrt.view_participants' in perms %} - {{ participant.uuid }} + {{ participant.id }} {{ participant.firstname }} {{ participant.surname }} {% else %} - {{ participant.uuid }} + {{ participant.id }} {{ participant.firstname }} {{ participant.surname }} {% endif %} diff --git a/fahrt/templates/fahrt/participants/view_participant_details.html b/fahrt/templates/fahrt/participants/view_participant_details.html index 09486296..f7d9f5cf 100644 --- a/fahrt/templates/fahrt/participants/view_participant_details.html +++ b/fahrt/templates/fahrt/participants/view_participant_details.html @@ -169,7 +169,7 @@ {% if participant.status == "confirmed" %} {% trans "View transportation as participant" %} {% endif %} @@ -196,12 +196,12 @@ {% for entry in log_entries %} {% if entry.user %} {% if entry.user.full_name %} -
  • {{ entry.time }}, {{ entry.user.full_name }}: {{ entry.text }}
  • +
  • {{ entry.updated_at }}, {{ entry.user.full_name }}: {{ entry.text }}
  • {% else %} -
  • {{ entry.time }}, {{ entry.user }}: {{ entry.text }}
  • +
  • {{ entry.updated_at }}, {{ entry.user }}: {{ entry.text }}
  • {% endif %} {% else %} -
  • {{ entry.time }}, www: {{ entry.text }}
  • +
  • {{ entry.updated_at }}, www: {{ entry.text }}
  • {% endif %} {% endfor %} diff --git a/fahrt/templates/fahrt/transportation/management/add_transport.html b/fahrt/templates/fahrt/transportation/management/add_transport.html index 74f42c4a..a6c1c548 100644 --- a/fahrt/templates/fahrt/transportation/management/add_transport.html +++ b/fahrt/templates/fahrt/transportation/management/add_transport.html @@ -22,7 +22,7 @@ {{ transport_name_pl }} {% endif %} {% if not calling_participant and transport.creator %} - - + + {% endif %} @@ -70,7 +70,7 @@

    {{ transport_name_pl }}

    {% trans "Meeting-Place:" %} - {{ transport.deparure_place|default:"n.A." }} + {{ transport.departure_place|default:"n.A." }} {% trans "Departure:" %} @@ -101,7 +101,7 @@

    {{ transport_name_pl }}

    {% for participant in transport.participant_set.all %} - {% if participant.uuid != transport.creator.uuid %} + {% if participant.id != transport.creator.id %} {{ transport_name_pl }} {% if not calling_participant %} - - + + {% endif %} @@ -146,8 +146,8 @@

    {{ transport_name_pl }}

    {% else %} - - + + {% endif %} {% endfor %} @@ -164,7 +164,7 @@

    {{ transport_name_pl }}

    > {% trans "Chatwall" %} {% if transport.transportationcomment_set.count == 0 %} @@ -188,7 +188,7 @@

    {{ transport_name_pl }}

    Add {{ transport_name }} {% endblocktrans %} -
    +
    diff --git a/fahrt/urls.py b/fahrt/urls.py index 732f3c24..68caadaa 100644 --- a/fahrt/urls.py +++ b/fahrt/urls.py @@ -25,12 +25,12 @@ ], ), ), - path("view//", participants_views.view_participant, name="view_participant"), - path("edit//", participants_views.edit_participant, name="edit_participant"), - path("delete//", participants_views.del_participant, name="del_participant"), - path("non_liability/", tex_views.non_liability_form, name="non_liability_form"), + path("view//", participants_views.view_participant, name="view_participant"), + path("edit//", participants_views.edit_participant, name="edit_participant"), + path("delete//", participants_views.del_participant, name="del_participant"), + path("non_liability/", tex_views.non_liability_form, name="non_liability_form"), path( - "togglemailinglist//", + "togglemailinglist//", participants_views.toggle_mailinglist, name="toggle_mailinglist", ), @@ -38,14 +38,14 @@ "set/", include( [ - path("paid//", participants_views.set_paid, name="set_paid"), + path("paid//", participants_views.set_paid, name="set_paid"), path( - "nonliability//", + "nonliability//", participants_views.set_nonliability, name="set_nonliability", ), path( - "payment_deadline///", + "payment_deadline///", participants_views.set_payment_deadline, name="set_payment_deadline", ), @@ -54,17 +54,17 @@ include( [ path( - "confirm//", + "confirm//", participants_views.set_status_confirmed, name="set_status_confirmed", ), path( - "waitinglist//", + "waitinglist//", participants_views.set_status_waitinglist, name="set_status_waitinglist", ), path( - "cancel//", + "cancel//", participants_views.set_status_canceled, name="set_status_canceled", ), diff --git a/fahrt/views/finanz_views.py b/fahrt/views/finanz_views.py index d633d3f5..364c56b1 100644 --- a/fahrt/views/finanz_views.py +++ b/fahrt/views/finanz_views.py @@ -126,8 +126,8 @@ def finanz_auto_matching(request: WSGIRequest) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) participants: QuerySet[Participant] = Participant.objects.filter(semester=semester, status="confirmed") - # mypy is weird for this one. equivalent [but not Typechecking]: participants.values_list("uuid", flat=True) - participants_ids_pre_mypy: list[Optional[UUID]] = [element["uuid"] for element in participants.values("uuid")] + # mypy is weird for this one. equivalent [but not Typechecking]: participants.values_list("id", flat=True) + participants_ids_pre_mypy: list[Optional[UUID]] = [element["id"] for element in participants.values("id")] participants_ids: list[UUID] = [uuid for uuid in participants_ids_pre_mypy if uuid] transactions: list[Entry] = [Entry.from_json(entry) for entry in request.session["results"]] diff --git a/fahrt/views/maintinance_views.py b/fahrt/views/maintinance_views.py index c36ce1e9..989f2b0e 100644 --- a/fahrt/views/maintinance_views.py +++ b/fahrt/views/maintinance_views.py @@ -8,7 +8,7 @@ from settool_common.models import get_semester, Semester from ..forms import FahrtForm, MailForm -from ..models import Fahrt, FahrtMail +from ..models import Fahrt, FahrtMail, Participant @permission_required("fahrt.view_participants") @@ -68,7 +68,7 @@ def send_mail(request: WSGIRequest, mail_pk: int) -> HttpResponse: mail: FahrtMail = get_object_or_404(FahrtMail, pk=mail_pk) selected_participants = request.session["selected_participants"] semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) - participants = semester.fahrt_participant.filter(id__in=selected_participants).order_by("surname") + participants = Participant.objects.filter(semester=semester, id__in=selected_participants).order_by("surname") subject, text, from_email = mail.get_mail_participant() diff --git a/fahrt/views/participants_views.py b/fahrt/views/participants_views.py index 55c6c4bb..765cdb7d 100644 --- a/fahrt/views/participants_views.py +++ b/fahrt/views/participants_views.py @@ -1,5 +1,6 @@ from datetime import date, timedelta from typing import Optional +from uuid import UUID from django import forms from django.contrib import messages @@ -30,9 +31,7 @@ @permission_required("fahrt.view_participants") def list_registered(request: WSGIRequest) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) - participants = semester.fahrt_participant.filter(status="registered").order_by( - "-registration_time", - ) + participants = Participant.objects.filter(semester=semester, status="registered").order_by("-created_at") context = { "participants": participants, @@ -43,7 +42,7 @@ def list_registered(request: WSGIRequest) -> HttpResponse: @permission_required("fahrt.view_participants") def list_waitinglist(request: WSGIRequest) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) - participants = semester.fahrt_participant.filter(status="waitinglist").order_by("-registration_time") + participants = Participant.objects.filter(semester=semester, status="waitinglist").order_by("-created_at") context = { "participants": participants, @@ -52,7 +51,7 @@ def list_waitinglist(request: WSGIRequest) -> HttpResponse: def get_possibly_filtered_participants(filterform, semester): - participants = semester.fahrt_participant.filter(status="confirmed").order_by( + participants = Participant.objects.filter(semester=semester, status="confirmed").order_by( "payment_deadline", "surname", "firstname", @@ -139,7 +138,7 @@ def get_nutritunal_information( participants: QuerySet[Participant], semester: Semester, ) -> list[dict[str, object]]: - nutritions = semester.fahrt_participant.filter(status="confirmed").values("nutrition").distinct() + nutritions = Participant.objects.filter(semester=semester, status="confirmed").values("nutrition").distinct() nutrition_choices = [choice["nutrition"] for choice in nutritions] return [ { @@ -154,7 +153,7 @@ def get_nutritunal_information( @permission_required("fahrt.view_participants") def list_cancelled(request: WSGIRequest) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) - participants = semester.fahrt_participant.filter(status="cancelled").order_by("surname") + participants = Participant.objects.filter(semester=semester, status="cancelled").order_by("surname") context = { "participants": participants, @@ -163,9 +162,9 @@ def list_cancelled(request: WSGIRequest) -> HttpResponse: @permission_required("fahrt.view_participants") -def view_participant(request: WSGIRequest, participant_pk: int) -> HttpResponse: +def view_participant(request: WSGIRequest, participant_pk: UUID) -> HttpResponse: participant = get_object_or_404(Participant, pk=participant_pk) - log_entries = participant.logentry_set.order_by("time") + log_entries = participant.logentry_set.order_by("created_at") form = SelectMailForm(request.POST or None) @@ -184,7 +183,7 @@ def view_participant(request: WSGIRequest, participant_pk: int) -> HttpResponse: @permission_required("fahrt.view_participants") -def edit_participant(request: WSGIRequest, participant_pk: int) -> HttpResponse: +def edit_participant(request: WSGIRequest, participant_pk: UUID) -> HttpResponse: participant = get_object_or_404(Participant, pk=participant_pk) form = ParticipantAdminForm( @@ -206,7 +205,7 @@ def edit_participant(request: WSGIRequest, participant_pk: int) -> HttpResponse: @permission_required("fahrt.view_participants") -def del_participant(request: WSGIRequest, participant_pk: int) -> HttpResponse: +def del_participant(request: WSGIRequest, participant_pk: UUID) -> HttpResponse: participant = get_object_or_404(Participant, pk=participant_pk) form = forms.Form(request.POST or None) @@ -223,7 +222,7 @@ def del_participant(request: WSGIRequest, participant_pk: int) -> HttpResponse: @permission_required("fahrt.view_participants") -def toggle_mailinglist(request: WSGIRequest, participant_pk: int) -> HttpResponse: +def toggle_mailinglist(request: WSGIRequest, participant_pk: UUID) -> HttpResponse: participant = get_object_or_404(Participant, pk=participant_pk) participant.toggle_mailinglist() participant.log(request.user, "Toggle mailinglist") @@ -232,7 +231,7 @@ def toggle_mailinglist(request: WSGIRequest, participant_pk: int) -> HttpRespons @permission_required("fahrt.view_participants") -def set_paid(request: WSGIRequest, participant_pk: int) -> HttpResponse: +def set_paid(request: WSGIRequest, participant_pk: UUID) -> HttpResponse: participant = get_object_or_404(Participant, pk=participant_pk) Participant.objects.filter(pk=participant_pk).update( paid=timezone.now().date(), @@ -243,7 +242,7 @@ def set_paid(request: WSGIRequest, participant_pk: int) -> HttpResponse: @permission_required("fahrt.view_participants") -def set_nonliability(request: WSGIRequest, participant_pk: int) -> HttpResponse: +def set_nonliability(request: WSGIRequest, participant_pk: UUID) -> HttpResponse: participant = get_object_or_404(Participant, pk=participant_pk) Participant.objects.filter(pk=participant_pk).update( non_liability=timezone.now().date(), @@ -254,7 +253,7 @@ def set_nonliability(request: WSGIRequest, participant_pk: int) -> HttpResponse: @permission_required("fahrt.view_participants") -def set_status_confirmed(request: WSGIRequest, participant_pk: int) -> HttpResponse: +def set_status_confirmed(request: WSGIRequest, participant_pk: UUID) -> HttpResponse: participant = get_object_or_404(Participant, pk=participant_pk) Participant.objects.filter(pk=participant_pk).update( status="confirmed", @@ -265,7 +264,7 @@ def set_status_confirmed(request: WSGIRequest, participant_pk: int) -> HttpRespo @permission_required("fahrt.view_participants") -def set_status_waitinglist(request: WSGIRequest, participant_pk: int) -> HttpResponse: +def set_status_waitinglist(request: WSGIRequest, participant_pk: UUID) -> HttpResponse: participant = get_object_or_404(Participant, pk=participant_pk) Participant.objects.filter(pk=participant_pk).update( status="waitinglist", @@ -276,7 +275,7 @@ def set_status_waitinglist(request: WSGIRequest, participant_pk: int) -> HttpRes @permission_required("fahrt.view_participants") -def set_payment_deadline(request: WSGIRequest, participant_pk: int, weeks: int) -> HttpResponse: +def set_payment_deadline(request: WSGIRequest, participant_pk: UUID, weeks: int) -> HttpResponse: weeks = int(weeks) # save due to regex in urls.py if weeks not in [1, 2, 3]: raise Http404("Invalid number of weeks") @@ -291,7 +290,7 @@ def set_payment_deadline(request: WSGIRequest, participant_pk: int, weeks: int) @permission_required("fahrt.view_participants") -def set_status_canceled(request: WSGIRequest, participant_pk: int) -> HttpResponse: +def set_status_canceled(request: WSGIRequest, participant_pk: UUID) -> HttpResponse: participant = get_object_or_404(Participant, pk=participant_pk) Participant.objects.filter(pk=participant_pk).update( status="cancelled", @@ -387,7 +386,7 @@ def filter_participants(request: WSGIRequest) -> HttpResponse: except ObjectDoesNotExist: messages.error(request, _("You have to create Fahrt Settings to manage the fahrt")) return redirect("fahrt:settings") - participants = semester.fahrt_participant.order_by("surname") + participants = Participant.objects.filter(semester=semester).order_by("surname") filterform = FilterParticipantsForm(request.POST or None) if filterform.is_valid(): @@ -448,7 +447,7 @@ def set_request_session_filtered_participants( u18 = filterform.cleaned_data["u18"] should_not_filter = u18 is None - filtered_participants: list[int] = [p.id for p in participants if should_not_filter or p.u18 is u18] + filtered_participants: list[UUID] = [p.id for p in participants if should_not_filter or p.u18 is u18] request.session["filtered_participants"] = filtered_participants @@ -456,9 +455,7 @@ def set_request_session_filtered_participants( def filtered_list(request: WSGIRequest) -> HttpResponse: filtered_participants = request.session["filtered_participants"] semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) - participants = semester.fahrt_participant.filter( - id__in=filtered_participants, - ).order_by("surname") + participants = Participant.objects.filter(semester=semester, id__in=filtered_participants).order_by("surname") form = SelectMailForm(request.POST or None) select_participant_form_set = formset_factory( diff --git a/fahrt/views/tex_views.py b/fahrt/views/tex_views.py index 04849477..701d3e6f 100644 --- a/fahrt/views/tex_views.py +++ b/fahrt/views/tex_views.py @@ -1,5 +1,6 @@ import time from typing import Union +from uuid import UUID from django.contrib import messages from django.contrib.auth.decorators import permission_required @@ -25,7 +26,7 @@ def export(request: WSGIRequest, file_format: str = "csv") -> Union[HttpResponse except ObjectDoesNotExist: messages.error(request, _("Please setup the SETtings for the Fahrt")) return redirect("fahrt:settings") - participants = semester.fahrt_participant.order_by("surname", "firstname") + participants = Participant.objects.filter(semester=semester).order_by("surname", "firstname") filename = f"fahrt_participants_{fahrt.semester}_{fahrt.date}_{time.strftime('%Y%m%d-%H%M')}" context = {"participants": participants, "fahrt": fahrt} if file_format == "csv": @@ -48,11 +49,11 @@ def export(request: WSGIRequest, file_format: str = "csv") -> Union[HttpResponse @permission_required("fahrt.view_participants") -def non_liability_form(request: WSGIRequest, participant_pk: int) -> PDFResponse: +def non_liability_form(request: WSGIRequest, participant_pk: UUID) -> PDFResponse: return get_non_liability(request, participant_pk) -def get_non_liability(request: WSGIRequest, participant_pk: int) -> PDFResponse: +def get_non_liability(request: WSGIRequest, participant_pk: UUID) -> PDFResponse: participant: Participant = get_object_or_404(Participant, pk=participant_pk) fahrt: Fahrt = get_object_or_404(Fahrt, semester=participant.semester) context = { diff --git a/fahrt/views/transport_views.py b/fahrt/views/transport_views.py index a3f4d772..00c242cd 100644 --- a/fahrt/views/transport_views.py +++ b/fahrt/views/transport_views.py @@ -311,7 +311,7 @@ def transport_chat(request: WSGIRequest, participant_uuid: UUID, transport_pk: i form = TransportationCommentForm(request.POST or None, transport=transport, participant=participant) if form.is_valid(): form.save() - return redirect("fahrt:transport_chat", participant.uuid, transport.pk) + return redirect("fahrt:transport_chat", participant.id, transport.pk) context = { "form": form, diff --git a/guidedtours/forms.py b/guidedtours/forms.py index d39f6387..c890a892 100644 --- a/guidedtours/forms.py +++ b/guidedtours/forms.py @@ -10,13 +10,17 @@ class ParticipantForm(CommonParticipantForm): class Meta: model = Participant - exclude = ["time"] + exclude = ["created_at", "updated_at"] def __init__(self, *args, **kwargs): - tours = kwargs.pop("tours") + self.tours_and_dates = kwargs.pop("tours_and_dates") + self.tours = kwargs.pop("tours") + self.dates = kwargs.pop("dates") super().__init__(*args, **kwargs) - self.fields["tour"].queryset = tours + self.fields["tour"].queryset = self.tours self.fields["tour"].widget.attrs = {"class": "no-automatic-choicejs"} + self.fields["date"].queryset = self.dates + self.fields["date"].widget.attrs = {"class": "no-automatic-choicejs"} class TourForm(SemesterBasedModelForm): diff --git a/guidedtours/locale/de/LC_MESSAGES/django.mo b/guidedtours/locale/de/LC_MESSAGES/django.mo index d6bd5fd4..7214ab94 100644 Binary files a/guidedtours/locale/de/LC_MESSAGES/django.mo and b/guidedtours/locale/de/LC_MESSAGES/django.mo differ diff --git a/guidedtours/locale/de/LC_MESSAGES/django.po b/guidedtours/locale/de/LC_MESSAGES/django.po index c301caf6..6e33c211 100644 --- a/guidedtours/locale/de/LC_MESSAGES/django.po +++ b/guidedtours/locale/de/LC_MESSAGES/django.po @@ -54,27 +54,6 @@ msgstr "Der Zeitpunkt der Tour" msgid "'Tour' or 'Waitinglist' depending on on_the_tour status" msgstr "'Tour' oder 'Waitinglist' (je nach on_the_tour Status)" -#: guidedtours/models.py -msgid "Name" -msgstr "Name" - -#: guidedtours/models.py -msgid "Description" -msgstr "Beschreibung" - -#: guidedtours/models.py -#: guidedtours/templates/guidedtours/tours/list_tours.html -msgid "Date" -msgstr "Datum" - -#: guidedtours/models.py -msgid "" -"How long (minutes) a Participant should be blocked after a Tour (additonal " -"to 30min leadup-blacklist-Time)" -msgstr "" -"Wie lang (Minuten) ein Teilnehmer nach dem beginn einer Tour (zusätzlich zu " -"der 30Minuten Leadup-Blacklist-Zeit) blockert wird" - #: guidedtours/models.py guidedtours/templates/guidedtours/tour_dashboard.html #: guidedtours/templates/guidedtours/tours/list_tours.html msgid "Capacity" @@ -88,6 +67,11 @@ msgstr "Anmeldungsstart" msgid "Close registration" msgstr "Anmeldungsschluss" +#: guidedtours/models.py +#: guidedtours/templates/guidedtours/tours/list_tours.html +msgid "Date" +msgstr "Datum" + #: guidedtours/models.py #: guidedtours/templates/guidedtours/maintenance/mail/send_mail.html #: guidedtours/templates/guidedtours/participants/filtered_participants.html @@ -119,10 +103,6 @@ msgstr "Handynummer" msgid "Subject" msgstr "Studiengang" -#: guidedtours/models.py -msgid "Registration Time" -msgstr "Registrierungszeit" - #: guidedtours/models.py msgid "On the tour" msgstr "Bei der Tour" @@ -326,7 +306,7 @@ msgstr "Anmeldung Fehlgeschlagen" #: guidedtours/templates/guidedtours/signup/blocked.html #, python-format msgid "" -"We are sorry, you can't participate in two guidedtours: at the same time. If " +"We are sorry, you can't participate in two guidedtours at the same time. If " "you think there is an error, contact %(mail)s." msgstr "" "Wir sind Sorry, aber du kannst nicht an zwei Institutsführungen gleichzeitig " @@ -342,11 +322,6 @@ msgstr "Institutsführungs-Anmeldung" msgid "All availible Guided guidedtours:" msgstr "Alle zur verfügung stehenden Institutsführungen:" -#: guidedtours/templates/guidedtours/signup/signup.html -#, python-format -msgid "%(name)s on %(date)s" -msgstr "%(name)s am %(date)s" - #: guidedtours/templates/guidedtours/signup/signup.html msgid "Length:" msgstr "Länge:" @@ -423,6 +398,10 @@ msgstr "Bestätigte Teilnehmer als CSV ausgeben" msgid "Export confired participants as PDF" msgstr "Bestätigte Teilnehmer als PDF ausgeben" +#: guidedtours/views.py +msgid "No dates" +msgstr "Keine Daten" + #: guidedtours/views.py #, python-brace-format msgid "" @@ -443,6 +422,26 @@ msgstr "" "Konnte die Registrierungs-Email nicht senden. Bitte vergewissere dich, das " "diese richtig konfiguirert ist." +#, python-format +#~ msgid "%(name)s on %(date)s" +#~ msgstr "%(name)s am %(date)s" + +#~ msgid "Registration Time" +#~ msgstr "Registrierungszeit" + +#~ msgid "Name" +#~ msgstr "Name" + +#~ msgid "Description" +#~ msgstr "Beschreibung" + +#~ msgid "" +#~ "How long (minutes) a Participant should be blocked after a Tour " +#~ "(additonal to 30min leadup-blacklist-Time)" +#~ msgstr "" +#~ "Wie lang (Minuten) ein Teilnehmer nach dem beginn einer Tour (zusätzlich " +#~ "zu der 30Minuten Leadup-Blacklist-Zeit) blockert wird" + #~ msgid "I accept the terms and conditions of the following privacy policy:" #~ msgstr "Ich stimme der folgenden Datenschutzerklärung zu:" diff --git a/guidedtours/migrations/0005_participant_time.py b/guidedtours/migrations/0005_participant_time.py index 3657e4f1..d5c223c4 100644 --- a/guidedtours/migrations/0005_participant_time.py +++ b/guidedtours/migrations/0005_participant_time.py @@ -1,8 +1,7 @@ # Generated by Django 1.9 on 2015-12-21 08:08 -import datetime - from django.db import migrations, models +from django.utils import timezone class Migration(migrations.Migration): @@ -16,7 +15,7 @@ class Migration(migrations.Migration): model_name="participant", name="time", field=models.DateTimeField( - default=datetime.datetime.now, + default=timezone.now, verbose_name="Registration Time", ), ), diff --git a/guidedtours/migrations/0013_remove_mail_semester.py b/guidedtours/migrations/0013_remove_mail_semester.py index 50c7b7d0..ba67cbd2 100644 --- a/guidedtours/migrations/0013_remove_mail_semester.py +++ b/guidedtours/migrations/0013_remove_mail_semester.py @@ -18,8 +18,5 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(subject_includes_semester), - migrations.RemoveField( - model_name="mail", - name="semester", - ), + migrations.RemoveField(model_name="mail", name="semester"), ] diff --git a/guidedtours/migrations/0020_participant_created_at_participant_updated_at_and_more.py b/guidedtours/migrations/0020_participant_created_at_participant_updated_at_and_more.py new file mode 100644 index 00000000..f0cfde4d --- /dev/null +++ b/guidedtours/migrations/0020_participant_created_at_participant_updated_at_and_more.py @@ -0,0 +1,47 @@ +# Generated by Django 4.0.3 on 2022-04-01 13:10 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("guidedtours", "0019_auto_20220321_1913"), + ] + + operations = [ + migrations.AddField( + model_name="participant", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="participant", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="setting", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="setting", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="tour", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="tour", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/guidedtours/migrations/0021_alter_tour_semester.py b/guidedtours/migrations/0021_alter_tour_semester.py new file mode 100644 index 00000000..3259c6c1 --- /dev/null +++ b/guidedtours/migrations/0021_alter_tour_semester.py @@ -0,0 +1,24 @@ +# Generated by Django 4.0.3 on 2022-04-01 16:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("settool_common", "0018_alter_anonymisationlog_semester"), + ("guidedtours", "0020_participant_created_at_participant_updated_at_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="tour", + name="semester", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="settool_common.semester", + verbose_name="Semester", + ), + ), + ] diff --git a/guidedtours/migrations/0022_remove_tour_date_remove_tour_length_and_more.py b/guidedtours/migrations/0022_remove_tour_date_remove_tour_length_and_more.py new file mode 100644 index 00000000..21933fd5 --- /dev/null +++ b/guidedtours/migrations/0022_remove_tour_date_remove_tour_length_and_more.py @@ -0,0 +1,88 @@ +# Generated by Django 4.0.3 on 2022-04-04 23:04 + +import django.db.models.deletion +from django.db import migrations, models + +import kalendar.models + + +def migrate_associated_meetings(apps, schema_editor): + Tour = apps.get_model("guidedtours", "Tour") + for tour in Tour.objects.all(): + tour.associated_meetings_id = kalendar.models.create_associated_meetings() + tour.save() + + +def migrate_tour_dates(apps, schema_editor): + date_lut = {} + Date = apps.get_model("kalendar", "Date") + Tour = apps.get_model("guidedtours", "Tour") + for tour in Tour.objects.all(): + group = tour.associated_meetings + date = Date.objects.create( + group=group, + date=tour.date, + probable_length=tour.length, + ) + date_lut[tour.id] = date + + Participant = apps.get_model("guidedtours", "Participant") + for participant in Participant.objects.all(): + participant.date = date_lut[participant.tour_id] + participant.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("kalendar", "0002_date_created_at_date_updated_at_dategroup_created_at_and_more"), + ("guidedtours", "0021_alter_tour_semester"), + ] + + operations = [ + migrations.AlterField( + model_name="tour", + name="description", + field=models.TextField(blank=True, default="", verbose_name="Description"), + preserve_default=False, + ), + migrations.AlterField( + model_name="tour", + name="name", + field=models.CharField(max_length=250, verbose_name="Name"), + ), + migrations.AddField( + model_name="tour", + name="associated_meetings", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="kalendar.dategroup", + verbose_name="Associated Meetings", + ), + ), + migrations.RunPython(migrate_associated_meetings), + migrations.AddField( + model_name="participant", + name="date", + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to="kalendar.date", + verbose_name="Date", + ), + preserve_default=False, + ), + migrations.RunPython(migrate_tour_dates), + migrations.AlterField( + model_name="participant", + name="date", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="kalendar.date", + verbose_name="Date", + ), + ), + migrations.RemoveField(model_name="tour", name="date"), + migrations.RemoveField(model_name="tour", name="length"), + ] diff --git a/guidedtours/migrations/0023_remove_participant_time_participant_date_and_more.py b/guidedtours/migrations/0023_remove_participant_time_participant_date_and_more.py new file mode 100644 index 00000000..5a893638 --- /dev/null +++ b/guidedtours/migrations/0023_remove_participant_time_participant_date_and_more.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.3 on 2022-04-05 15:23 + +from django.db import migrations + + +def migrate_participant_time_created_at(apps, schema_editor): + Participant = apps.get_model("guidedtours", "Participant") + for participant in Participant.objects.all(): + participant.created_at = participant.time + participant.save(update_fields=["created_at"]) + + +class Migration(migrations.Migration): + + dependencies = [ + ("kalendar", "0002_date_created_at_date_updated_at_dategroup_created_at_and_more"), + ("guidedtours", "0022_remove_tour_date_remove_tour_length_and_more"), + ] + + operations = [ + migrations.RunPython(migrate_participant_time_created_at), + migrations.RemoveField(model_name="participant", name="time"), + ] diff --git a/guidedtours/migrations/0024_alter_tour_id.py b/guidedtours/migrations/0024_alter_tour_id.py new file mode 100644 index 00000000..b02b5f03 --- /dev/null +++ b/guidedtours/migrations/0024_alter_tour_id.py @@ -0,0 +1,82 @@ +# Generated by Django 4.0.3 on 2022-04-05 15:39 + +import uuid + +import django +from django.db import migrations, models + + +def migrate_tour_uuid(apps, schema_editor): + Tour = apps.get_model("guidedtours", "Tour") + for tour in Tour.objects.all(): + tour.uuid = uuid.uuid4() + tour.save(update_fields=["uuid"]) + + +def migrate_tour_id_for_participant(apps, schema_editor): + Participant = apps.get_model("guidedtours", "Participant") + for participant in Participant.objects.all(): + participant.tour_uuid_id = participant.tour.uuid + participant.save(update_fields=["tour_uuid"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("guidedtours", "0023_remove_participant_time_participant_date_and_more"), + ] + + operations = [ + # see https://stackoverflow.com/a/48235821 + # add a new uuid-field to the Tour model + migrations.AddField( + model_name="tour", + name="uuid", + field=models.UUIDField(default=uuid.uuid4, blank=True, null=True), + ), + migrations.RunPython(migrate_tour_uuid), + migrations.AlterField( + model_name="tour", + name="uuid", + field=models.UUIDField(default=uuid.uuid4, serialize=False, unique=True), + ), + # add different fk-fields for each participant and logentry + migrations.AddField( + model_name="participant", + name="tour_uuid", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name="participant_uuid", + to="guidedtours.tour", + to_field="uuid", + db_constraint=False, + ), + ), + migrations.AlterField( + model_name="participant", + name="tour", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="participant", + to="guidedtours.tour", + to_field="id", + ), + ), + # fill new fields with valid data + migrations.RunPython(migrate_tour_id_for_participant), + # after filling, we can assure the db, that they are filled + migrations.AlterField( + model_name="participant", + name="tour_uuid", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="participant_uuid", + to="guidedtours.tour", + to_field="uuid", + ), + ), + # switch to the new uuid-field + # migrations.RemoveField(model_name="participant", name="tour"), + migrations.RenameField(model_name="participant", old_name="tour_uuid", new_name="tour"), + ] diff --git a/guidedtours/migrations/0025_remove_tour_uuid_alter_participant_tour_and_more.py b/guidedtours/migrations/0025_remove_tour_uuid_alter_participant_tour_and_more.py new file mode 100644 index 00000000..bb2fb670 --- /dev/null +++ b/guidedtours/migrations/0025_remove_tour_uuid_alter_participant_tour_and_more.py @@ -0,0 +1,34 @@ +# Generated by Django 4.0.3 on 2022-04-05 16:28 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("guidedtours", "0024_alter_tour_id"), + ] + + operations = [ + # uuid->id for original field + migrations.RemoveField(model_name="tour", name="id"), + migrations.AlterField( + model_name="tour", + name="uuid", + field=models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True), + ), + migrations.RenameField(model_name="tour", old_name="uuid", new_name="id"), + # cleanup + migrations.AlterField( + model_name="participant", + name="tour", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="guidedtours.tour", + verbose_name="Tour", + ), + ), + ] diff --git a/guidedtours/models.py b/guidedtours/models.py index 24cd298b..6b0fb657 100644 --- a/guidedtours/models.py +++ b/guidedtours/models.py @@ -1,8 +1,11 @@ from dateutil.relativedelta import relativedelta from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver from django.utils import timezone from django.utils.translation import gettext_lazy as _ +import kalendar.models import settool_common.models as common_models from settool_common.models import Semester, Subject @@ -50,7 +53,7 @@ def send_mail_participant(self, participant): return self.send_mail(context, participant.email) -class Tour(models.Model): +class Tour(kalendar.models.BaseDateGroupInstance): class Meta: permissions = ( ( @@ -59,45 +62,10 @@ class Meta: ), ) - semester = models.ForeignKey( - Semester, - on_delete=models.CASCADE, - ) - - name = models.CharField( - max_length=200, - verbose_name=_("Name"), - ) - - description = models.TextField( - null=True, - blank=True, - verbose_name=_("Description"), - ) - - date = models.DateTimeField( - verbose_name=_("Date"), - ) - - length = models.IntegerField( - verbose_name=_( - "How long (minutes) a Participant should be blocked after a Tour (additonal to 30min " - "leadup-blacklist-Time)", - ), - default=0, - ) - - capacity = models.PositiveIntegerField( - verbose_name=_("Capacity"), - ) + capacity = models.PositiveIntegerField(verbose_name=_("Capacity")) - open_registration = models.DateTimeField( - _("Open registration"), - ) - - close_registration = models.DateTimeField( - _("Close registration"), - ) + open_registration = models.DateTimeField(_("Open registration")) + close_registration = models.DateTimeField(_("Close registration")) def __str__(self) -> str: return self.name @@ -107,24 +75,35 @@ def registration_open(self): return self.open_registration < timezone.now() < self.close_registration -class Participant(models.Model): +# pylint: disable=unused-argument +@receiver(post_save, sender=Tour) +def create_event_meetings(sender, instance, created, **kwargs): + if not instance.associated_meetings: + instance.associated_meetings = kalendar.models.create_associated_meetings() + instance.save() + + +# pylint: enable=unused-argument + + +class Participant(common_models.LoggedModelBase): class Meta: unique_together = ("tour", "email") tour = models.ForeignKey(Tour, on_delete=models.CASCADE, verbose_name=_("Tour")) + date = models.ForeignKey("kalendar.Date", on_delete=models.CASCADE, verbose_name=_("Date")) firstname = models.CharField(max_length=200, verbose_name=_("First name")) surname = models.CharField(max_length=200, verbose_name=_("Surname")) email = models.EmailField(verbose_name=_("E-Mail")) phone = models.CharField(max_length=200, verbose_name=_("Mobile phone")) subject = models.ForeignKey(Subject, on_delete=models.CASCADE, verbose_name=_("Subject")) - time = models.DateTimeField(auto_now_add=True, verbose_name=_("Registration Time")) def __str__(self) -> str: return f"{self.firstname} {self.surname}" @property def on_the_tour(self): - participants = self.tour.participant_set.order_by("time") + participants = self.tour.participant_set.order_by("created_at") participants = participants[: self.tour.capacity] return self in participants @@ -135,11 +114,8 @@ def status(self): return _("On waitinglist") -class Setting(models.Model): - semester = models.OneToOneField( - Semester, - on_delete=models.CASCADE, - ) +class Setting(common_models.LoggedModelBase): + semester = models.OneToOneField(Semester, on_delete=models.CASCADE) mail_registration = models.ForeignKey( TourMail, diff --git a/guidedtours/templates/guidedtours/signup/blocked.html b/guidedtours/templates/guidedtours/signup/blocked.html index b3c90159..0a1094d8 100644 --- a/guidedtours/templates/guidedtours/signup/blocked.html +++ b/guidedtours/templates/guidedtours/signup/blocked.html @@ -5,6 +5,6 @@ {% block set_common_content %} {% blocktrans trimmed %} -We are sorry, you can't participate in two guidedtours: at the same time. If you think there is an error, contact {{ mail }}. +We are sorry, you can't participate in two guidedtours at the same time. If you think there is an error, contact {{ mail }}. {% endblocktrans %} {% endblock %} diff --git a/guidedtours/templates/guidedtours/signup/signup.html b/guidedtours/templates/guidedtours/signup/signup.html index b2cc6a19..f1611070 100644 --- a/guidedtours/templates/guidedtours/signup/signup.html +++ b/guidedtours/templates/guidedtours/signup/signup.html @@ -27,7 +27,7 @@

    {% trans "All availible Guided guidedtours:" %}

    class="accordion" id="accordion" > - {% for tour in tours %} + {% for tour,dates in tours_and_dates %}

    {% trans "All availible Guided guidedtours:" %}

    aria-controls="collapse{{ forloop.counter }}" onclick='assignTour("{{ tour.id }}")' > - {% blocktranslate trimmed with name=tour.name date=tour.date lenth=tour.length %} - {{ name }} on {{ date }} - {% endblocktranslate %} + {{ name }} + {% for date in dates %} +
    + {{ date.date }}-{{ date.probable_end }} +
    + {% endfor %}
    /", views.view_tour, name="view_tour"), - path("edit//", views.edit_tour, name="edit_tour"), - path("delete//", views.del_tour, name="del_tour"), - path("export///", views.export_tour, name="export_tour"), + path("view//", views.view_tour, name="view_tour"), + path("edit//", views.edit_tour, name="edit_tour"), + path("delete//", views.del_tour, name="del_tour"), + path("export///", views.export_tour, name="export_tour"), ], ), ), diff --git a/guidedtours/views.py b/guidedtours/views.py index a0a60fc4..dff4790f 100644 --- a/guidedtours/views.py +++ b/guidedtours/views.py @@ -1,22 +1,23 @@ +import dataclasses +import datetime import time -from datetime import timedelta from typing import Any, Optional, Union +from uuid import UUID from django import forms from django.contrib import messages from django.contrib.auth.decorators import permission_required from django.core.handlers.wsgi import WSGIRequest from django.db.models import Q, QuerySet -from django.db.models.aggregates import Count from django.forms import formset_factory from django.http import HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.utils import timezone -from django.utils.datetime_safe import date from django.utils.translation import gettext as _ from django_tex.response import PDFResponse from django_tex.shortcuts import render_to_pdf +from kalendar.models import Date from settool_common import utils from settool_common.models import get_semester, Semester @@ -41,27 +42,54 @@ def list_tours(request: WSGIRequest) -> HttpResponse: return render(request, "guidedtours/tours/list_tours.html", context) +@dataclasses.dataclass +class MinimalTour: + capacity: int + name: str + dates: list[datetime.datetime] + registered: int + + @property + def label(self) -> str: + label = [self.name] + if self.dates: + for date in self.dates: + label.append(f"{date.strftime('%d.%m %H')}Uhr") + else: + label.append(_("No dates")) + return " ".join(label) + + @permission_required("guidedtours.view_participants") def dashboard(request: WSGIRequest) -> HttpResponse: - tours = ( - Tour.objects.filter(Q(semester=get_semester(request)) & Q(date__gte=date.today())) - .values("capacity", "name", "date") - .annotate(registered=Count("participant")) - .order_by("date") - ) + tours_current_semester: list[Tour] = Tour.sorted_by_semester(get_semester(request)) # type:ignore + tours: list[MinimalTour] = [] + for tour in tours_current_semester: + registered = Participant.objects.filter(tour=tour).count() + meetings = tour.associated_meetings + if not meetings: + raise ValueError(f"Tour {tour} has no associated meetings") + tours.append( + MinimalTour( + capacity=tour.capacity, + name=tour.name, + dates=meetings.dates, + registered=registered, + ), + ) context = { - "tour_labels": [f"{tour['name']} {tour['date'].strftime('%d.%m %H')}Uhr" for tour in tours], - "tour_registrations": [tour["registered"] for tour in tours], - "tour_capacity": [tour["capacity"] for tour in tours], + "tour_labels": [tour.label for tour in tours], + "tour_registrations": [tour.registered for tour in tours], + "tour_capacity": [tour.capacity for tour in tours], } return render(request, "guidedtours/tour_dashboard.html", context) @permission_required("guidedtours.view_participants") -def view_tour(request: WSGIRequest, tour_pk: int) -> HttpResponse: +def view_tour(request: WSGIRequest, tour_pk: UUID) -> HttpResponse: tour = get_object_or_404(Tour, pk=tour_pk) - participants = tour.participant_set.order_by("time") + participants = tour.participant_set.order_by("created_at") waitinglist = participants[tour.capacity :] participants = participants[: tour.capacity] @@ -77,10 +105,7 @@ def view_tour(request: WSGIRequest, tour_pk: int) -> HttpResponse: def add_tour(request: WSGIRequest) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) - form = TourForm( - request.POST or None, - semester=semester, - ) + form = TourForm(request.POST or None, semester=semester) if form.is_valid(): form.save() @@ -94,7 +119,7 @@ def add_tour(request: WSGIRequest) -> HttpResponse: @permission_required("guidedtours.view_participants") -def edit_tour(request: WSGIRequest, tour_pk: int) -> HttpResponse: +def edit_tour(request: WSGIRequest, tour_pk: UUID) -> HttpResponse: tour = get_object_or_404(Tour, pk=tour_pk) form = TourForm( @@ -115,7 +140,7 @@ def edit_tour(request: WSGIRequest, tour_pk: int) -> HttpResponse: @permission_required("guidedtours.view_participants") -def del_tour(request: WSGIRequest, tour_pk: int) -> HttpResponse: +def del_tour(request: WSGIRequest, tour_pk: UUID) -> HttpResponse: tour = get_object_or_404(Tour, pk=tour_pk) form = forms.Form(request.POST or None) @@ -134,14 +159,9 @@ def del_tour(request: WSGIRequest, tour_pk: int) -> HttpResponse: @permission_required("guidedtours.view_participants") def filter_participants(request: WSGIRequest) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) - participants: QuerySet[Participant] = Participant.objects.filter( - tour__semester=semester.id, - ).order_by("surname") + participants: QuerySet[Participant] = Participant.objects.filter(tour__semester=semester).order_by("surname") - filterform = FilterParticipantsForm( - request.POST or None, - semester=semester, - ) + filterform = FilterParticipantsForm(request.POST or None, semester=semester) if filterform.is_valid(): search: str = filterform.cleaned_data["search"] @@ -307,28 +327,61 @@ def send_mail(request: WSGIRequest, mail_pk: int) -> HttpResponse: return render(request, "guidedtours/maintenance/mail/send_mail.html", context) +def _get_tours_and_dates(semester: Semester) -> tuple[list[tuple[Tour, list[Date]]], list[Tour], list[Date]]: + tours: QuerySet[Tour] = Tour.objects.filter( + semester=semester, + open_registration__lt=timezone.now(), + close_registration__gt=timezone.now(), + associated_meetings__isnull=False, + ).all() + tmp_tours_and_dates = [] + for tour in tours: + meeting = tour.associated_meetings + if not meeting: + raise ValueError("Tour without associated meeting") + first_datetime, dates = tour.first_datetime, meeting.date_objects + if first_datetime is not None and dates: + tmp_tours_and_dates.append((first_datetime, tour, dates)) + tours_and_dates = [(tour, dates) for first_datetime, tour, dates in sorted(tmp_tours_and_dates)] + all_tours: list[Tour] = [] + all_dates: list[Date] = [] + for tour, dates in tours_and_dates: + all_tours.append(tour) + all_dates += dates + return tours_and_dates, all_tours, all_dates + + def signup(request: WSGIRequest) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) curr_settings: Setting = Setting.objects.get_or_create(semester=semester)[0] - tours = semester.tour_set.filter( - open_registration__lt=timezone.now(), - close_registration__gt=timezone.now(), - ).order_by("date") - if not tours: + tours_and_dates, all_tours, all_dates = _get_tours_and_dates(semester) + + if not tours_and_dates: context: dict[str, Any] = {"semester": semester} return render(request, "guidedtours/signup/signup_notour.html", context) - form = ParticipantForm(request.POST or None, tours=tours, semester=semester) + form = ParticipantForm( + request.POST or None, + tours_and_dates=tours_and_dates, + tours=all_tours, + dates=all_dates, + semester=semester, + ) if form.is_valid(): participant: Participant = form.save(commit=False) # if there is a tour with a participant using the same mail in the blocked time-window # (15min for transitioning and 15min for meeting) - conflicting_tours = semester.tour_set.filter( - date__gt=participant.tour.date - timedelta(minutes=30), - date__lt=participant.tour.date + timedelta(minutes=participant.tour.length) + timedelta(minutes=30), - ) - if Participant.objects.filter(Q(email=participant.email) & Q(tours__in=conflicting_tours)).exists(): + + conflicting_dates = [] + for tour, dates in tours_and_dates: + if tour == participant.tour: + continue + for date_obj in dates: + if date_obj.intersects(participant.date): + conflicting_dates.append(date_obj) + + if Participant.objects.filter(Q(email=participant.email) & Q(date__in=conflicting_dates)).exists(): return render(request, "guidedtours/signup/blocked.html", {"mail": TourMail.SET}) participant.save() if curr_settings.mail_registration: @@ -347,7 +400,7 @@ def signup(request: WSGIRequest) -> HttpResponse: context = { "semester": semester, "form": form, - "tours": tours, + "tours_and_dates": tours_and_dates, } return render(request, "guidedtours/signup/signup.html", context) @@ -390,15 +443,16 @@ def signup_internal(request: WSGIRequest) -> HttpResponse: @permission_required("guidedtours.view_participants") -def export_tour(request: WSGIRequest, file_format: str, tour_pk: int) -> Union[HttpResponse, PDFResponse]: +def export_tour(request: WSGIRequest, file_format: str, tour_pk: UUID) -> Union[HttpResponse, PDFResponse]: tour = get_object_or_404(Tour, pk=tour_pk) - participants = tour.participant_set.order_by("time") + participants = tour.participant_set.order_by("created_at") confirmed_participants = participants[: tour.capacity] - filename = f"participants_{tour.name}_{tour.date}_{time.strftime('%Y%m%d-%H%M')}" + tour_date = tour.first_datetime or "No-Date" + filename = f"participants_{tour.name}_{tour_date}_{time.strftime('%Y%m%d-%H%M')}" context = {"participants": confirmed_participants, "tour": tour} if file_format == "csv": return utils.download_csv( - ["surname", "firstname", "time", "email", "phone", "subject"], + ["surname", "firstname", "created_at", "updated_at", "email", "phone", "subject"], f"{filename}.csv", confirmed_participants, ) diff --git a/kalendar/__init__.py b/kalendar/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kalendar/admin.py b/kalendar/admin.py new file mode 100644 index 00000000..846f6b40 --- /dev/null +++ b/kalendar/admin.py @@ -0,0 +1 @@ +# Register your models here. diff --git a/kalendar/apps.py b/kalendar/apps.py new file mode 100644 index 00000000..cce5d3e6 --- /dev/null +++ b/kalendar/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class KalendarConfig(AppConfig): + name = "kalendar" diff --git a/kalendar/feeds.py b/kalendar/feeds.py new file mode 100644 index 00000000..91f8b4a8 --- /dev/null +++ b/kalendar/feeds.py @@ -0,0 +1,93 @@ +import datetime +from typing import Optional +from uuid import UUID + +import django.utils.timezone +import icalendar +from django.db.models import QuerySet +from django.http import Http404, HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.html import escape +from django_ical.views import ICAL_EXTRA_FIELDS, ICalFeed + +from tutors.models import Tutor + +from .models import Date + + +class PersonalMeetingFeed(ICalFeed): + file_name = "meetings.ics" + timezone = "MET" + tutor: Optional[Tutor] = None + + # noinspection PyMethodOverriding + # pylint: disable=arguments-differ + def get_object(self, request: HttpRequest, tutor_uuid: UUID) -> Tutor: + tutor: Tutor = get_object_or_404(Tutor, pk=tutor_uuid) + self.tutor = tutor + return tutor + + @staticmethod + def product_id(tutor: Tutor) -> str: + return f"-//set.mpi.fs.tum.de//user//ical//{tutor.pk}" + + @staticmethod + def items(tutor: Tutor) -> QuerySet[Date]: + reference_time = django.utils.timezone.now() - datetime.timedelta(days=7 * 6) + return Date.get_dates_for_tutor(tutor.pk, reference_time) + + @staticmethod + def item_title(item: Date) -> str: + super_type = item.group.group_type.capitalize() + # Titles should be double escaped by default + return escape(f"[SET-{super_type}] {item.group.super_group.name}") + + @staticmethod + def item_updateddate(item: Date) -> datetime.datetime: + return item.updated_at + + @staticmethod + def item_created(item: Date) -> datetime.datetime: + return item.created_at + + @staticmethod + def item_description(item: Date) -> str: + return str(item.group.super_group.description) + + def item_link(self, item: Date) -> str: + if not self.tutor: + raise Http404() + return reverse("kalendar:view_date_public", args=[self.tutor.pk, item.pk]) + + @staticmethod + def item_organizer(_item: Date) -> icalendar.vCalAddress: + organizer = icalendar.vCalAddress("MAILTO:set-tutoren@fs.tum.de") + organizer.params["CN"] = icalendar.vText("SET-Tutor-Team") + return organizer + + @staticmethod + def item_start_datetime(item: Date) -> datetime.datetime: + return item.date + + @staticmethod + def item_end_datetime(item: Date) -> datetime.datetime: + return item.date + datetime.timedelta(minutes=item.probable_length) + + @staticmethod + def item_location(item: Date) -> str: + location = item.group.location + return str(location) if location else "" + + @staticmethod + def item_categories(item: Date) -> str: + group_type = item.group.group_type.upper() + return f"SET,{group_type}" + + def item_extra_kwargs(self, item): + kwargs = {} + for field in ICAL_EXTRA_FIELDS + ["categories"]: + val = self._get_dynamic_attr("item_" + field, item) + if val: + kwargs[field] = val + return kwargs diff --git a/kalendar/forms.py b/kalendar/forms.py new file mode 100644 index 00000000..e16a767c --- /dev/null +++ b/kalendar/forms.py @@ -0,0 +1,24 @@ +from bootstrap_datepicker_plus.widgets import DateTimePickerInput +from django import forms + +from kalendar.models import Date + + +class DateForm(forms.ModelForm): + class Meta: + model = Date + exclude: list[str] = [] + widgets = { + "date": DateTimePickerInput(format="%Y-%m-%d %H:%M"), + } + + def __init__(self, *args, **kwargs): + self.date_group = kwargs.pop("date_group") + super().__init__(*args, **kwargs) + + def save(self, commit: bool = True) -> Date: + date: Date = super().save(commit=False) + date.group = self.date_group + if commit: + date.save() + return date diff --git a/kalendar/locale/de/LC_MESSAGES/django.mo b/kalendar/locale/de/LC_MESSAGES/django.mo new file mode 100644 index 00000000..efe9a9cb Binary files /dev/null and b/kalendar/locale/de/LC_MESSAGES/django.mo differ diff --git a/kalendar/locale/de/LC_MESSAGES/django.po b/kalendar/locale/de/LC_MESSAGES/django.po new file mode 100644 index 00000000..49653d92 --- /dev/null +++ b/kalendar/locale/de/LC_MESSAGES/django.po @@ -0,0 +1,250 @@ +msgid "" +msgstr "" +"Report-Msgid-Bugs-To: elsinga fs.tum.de\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: kalendar/models.py +#: kalendar/templates/kalendar/management/list/chronological.html +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "short/simplified Address" +msgstr "verkürtzte/vereinfachte Adresse" + +#: kalendar/models.py +#: kalendar/templates/kalendar/management/list/chronological.html +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "(Street)map-address" +msgstr "Straßenkarten-Adresse" + +#: kalendar/models.py +#: kalendar/templates/kalendar/management/list/chronological.html +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "Room" +msgstr "Raum" + +#: kalendar/models.py kalendar/templates/kalendar/management/list/grouped.html +msgid "Comment" +msgstr "Kommentar" + +#: kalendar/models.py +msgid "Subscribers to a date_group" +msgstr "Abonnenten einer date_group" + +#: kalendar/models.py +#: kalendar/templates/kalendar/management/list/chronological.html +msgid "Event" +msgstr "Event" + +#: kalendar/models.py +msgid "Tour" +msgstr "Institutsführung" + +#: kalendar/models.py +#: kalendar/templates/kalendar/management/list/chronological.html +msgid "Task" +msgstr "Aufgabe" + +#: kalendar/models.py +#: kalendar/templates/kalendar/management/list/chronological.html +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "Name" +msgstr "Name" + +#: kalendar/models.py kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "Description" +msgstr "Beschreibung" + +#: kalendar/models.py kalendar/templates/kalendar/management/delete_date.html +#: kalendar/templates/kalendar/management/list/grouped.html +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "no meeting point specified" +msgstr "kein Treffpunkt angegeben" + +#: kalendar/models.py +msgid "Associated Meetings" +msgstr "Zugeordnete Meetings" + +#: kalendar/models.py +msgid "Date and Time" +msgstr "Datum und Zeit" + +#: kalendar/models.py kalendar/templates/kalendar/management/delete_date.html +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "probable length in minutes" +msgstr "warscheinliche länge in Minuten" + +#: kalendar/models.py +msgid "Subscribers to one meeting" +msgstr "Abonnenten einer Sitzung" + +#: kalendar/templates/kalendar/base_kalendar.html +#: kalendar/templates/kalendar/dashboard.html +msgid "Dashboard" +msgstr "Dashboard" + +#: kalendar/templates/kalendar/base_kalendar.html +msgid "Management" +msgstr "Management" + +#: kalendar/templates/kalendar/base_kalendar.html +msgid "List all dates" +msgstr "Liste aller Daten" + +#: kalendar/templates/kalendar/base_kalendar.html +msgid "List future dates" +msgstr "Liste zukünftigen Daten" + +#: kalendar/templates/kalendar/base_kalendar.html +msgid "List dates by event" +msgstr "Daten nach Event auflisten" + +#: kalendar/templates/kalendar/management/add_date.html +msgid "Add date" +msgstr "Datum hinzufügen" + +#: kalendar/templates/kalendar/management/delete_date.html +#: kalendar/templates/kalendar/management/view_date.html +msgid "Delete date" +msgstr "Datum löschen" + +#: kalendar/templates/kalendar/management/delete_date.html +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "Date" +msgstr "Datum" + +#: kalendar/templates/kalendar/management/delete_date.html +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "Location" +msgstr "Standort" + +#: kalendar/templates/kalendar/management/delete_date.html +#: kalendar/templates/kalendar/management/edit_date.html +msgid "Cancel" +msgstr "Abbrechen" + +#: kalendar/templates/kalendar/management/edit_date.html +#: kalendar/templates/kalendar/management/view_date.html +msgid "Edit date" +msgstr "Datum bearbeiten" + +#: kalendar/templates/kalendar/management/list/chronological.html +msgid "List of dates" +msgstr "Liste der Daten" + +#: kalendar/templates/kalendar/management/list/chronological.html +msgid "Event lies in the past" +msgstr "Event liegt in der vergangenheit" + +#: kalendar/templates/kalendar/management/list/chronological.html +msgid "Task lies in the past" +msgstr "Aufgabe liegt in der vergangenheit" + +#: kalendar/templates/kalendar/management/list/chronological.html +msgid "Guidedtour" +msgstr "Institutsführung" + +#: kalendar/templates/kalendar/management/list/chronological.html +msgid "Guidedtour lies in the past" +msgstr "Institutsführung liegt in der vergangenheit" + +#: kalendar/templates/kalendar/management/list/chronological.html +msgid "Date/Time" +msgstr "Datum/Zeit" + +#: kalendar/templates/kalendar/management/list/chronological.html +msgid "Probable length" +msgstr "Warscheinliche länge" + +#: kalendar/templates/kalendar/management/list/chronological.html +msgid "Actions" +msgstr "Aktionen" + +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "List of events" +msgstr "Liste der Events" + +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "No Events exist" +msgstr "Keine Events existieren" + +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "Event Data" +msgstr "Eventdaten" + +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "Description:" +msgstr "Beschreibung:" + +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "Location:" +msgstr "Standort:" + +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "Dates:" +msgstr "Daten:" + +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "Associated Tasks" +msgstr "Zugeordnete Aufgaben" + +#: kalendar/templates/kalendar/management/list/grouped.html +msgid "no Tasks exist" +msgstr "keine Aufgaben existeren" + +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "Date details" +msgstr "Datumsdetails" + +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "Event-information" +msgstr "Event-informationen" + +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "Task-information" +msgstr "Aufgaben-informationen" + +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "Date-information" +msgstr "Datum-informationen" + +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "Location-information" +msgstr "Standort-informationen" + +#: kalendar/templates/kalendar/management/view_date.html +#: kalendar/templates/kalendar/management/view_date_public.html +msgid "Back" +msgstr "Zurück" + +#: kalendar/templates/kalendar/management/view_date.html +msgid "Add date to the same group" +msgstr "Datum zur selben Datumsgruppe hinzufügen" + +#: kalendar/views.py +#, python-brace-format +msgid "Successfully added date {date}." +msgstr "Datum {date} wurde erfolgreich hinzugefügt" + +#: kalendar/views.py +#, python-brace-format +msgid "Successfully edited date {date}." +msgstr "Datum {date} wurde erfolgreich hinzugefügt" + +#: kalendar/views.py +#, python-brace-format +msgid "Deleted date {date}." +msgstr "Datum {date} wurde gelöscht" diff --git a/kalendar/migrations/0001_initial.py b/kalendar/migrations/0001_initial.py new file mode 100644 index 00000000..664167de --- /dev/null +++ b/kalendar/migrations/0001_initial.py @@ -0,0 +1,120 @@ +# Generated by Django 4.0.3 on 2022-03-23 00:34 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ("tutors", "0008_auto_20220321_1851"), + ] + + operations = [ + migrations.CreateModel( + name="Date", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True)), + ("date", models.DateTimeField(verbose_name="Date and Time")), + ("probable_length", models.IntegerField(default=60, verbose_name="probable length in minutes")), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="DateGroup", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True)), + ("comment", models.CharField(blank=True, default="", max_length=200, verbose_name="Comment")), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="Location", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True)), + ("shortname", models.CharField(max_length=200, verbose_name="short/simplified Address")), + ("shortname_de", models.CharField(max_length=200, null=True, verbose_name="short/simplified Address")), + ("shortname_en", models.CharField(max_length=200, null=True, verbose_name="short/simplified Address")), + ("address", models.CharField(blank=True, max_length=100, verbose_name="(Street)map-address")), + ( + "address_de", + models.CharField(blank=True, max_length=100, null=True, verbose_name="(Street)map-address"), + ), + ( + "address_en", + models.CharField(blank=True, max_length=100, null=True, verbose_name="(Street)map-address"), + ), + ("room", models.CharField(blank=True, max_length=50, verbose_name="Room")), + ("comment", models.CharField(blank=True, max_length=200, verbose_name="Comment")), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="DateSubscriber", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True)), + ("date", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="kalendar.date")), + ("tutor", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="tutors.tutor")), + ], + options={ + "abstract": False, + }, + ), + migrations.CreateModel( + name="DateGroupSubscriber", + fields=[ + ("id", models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, unique=True)), + ("date", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="kalendar.dategroup")), + ("tutor", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="tutors.tutor")), + ], + options={ + "abstract": False, + }, + ), + migrations.AddField( + model_name="dategroup", + name="location", + field=models.ForeignKey( + blank=True, + default=None, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="kalendar.location", + ), + ), + migrations.AddField( + model_name="dategroup", + name="subscribers", + field=models.ManyToManyField( + blank=True, + through="kalendar.DateGroupSubscriber", + to="tutors.tutor", + verbose_name="Subscribers to a date_group", + ), + ), + migrations.AddField( + model_name="date", + name="group", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="kalendar.dategroup"), + ), + migrations.AddField( + model_name="date", + name="meeting_subscribers", + field=models.ManyToManyField( + blank=True, + through="kalendar.DateSubscriber", + to="tutors.tutor", + verbose_name="Subscribers to one meeting", + ), + ), + ] diff --git a/kalendar/migrations/0002_date_created_at_date_updated_at_dategroup_created_at_and_more.py b/kalendar/migrations/0002_date_created_at_date_updated_at_dategroup_created_at_and_more.py new file mode 100644 index 00000000..78d0e349 --- /dev/null +++ b/kalendar/migrations/0002_date_created_at_date_updated_at_dategroup_created_at_and_more.py @@ -0,0 +1,69 @@ +# Generated by Django 4.0.3 on 2022-04-04 22:42 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("kalendar", "0001_initial"), + ] + + operations = [ + migrations.AddField( + model_name="date", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="date", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="dategroup", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="dategroup", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="dategroupsubscriber", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="dategroupsubscriber", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="datesubscriber", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="datesubscriber", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="location", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="location", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/kalendar/migrations/__init__.py b/kalendar/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kalendar/models.py b/kalendar/models.py new file mode 100644 index 00000000..6d45b4e3 --- /dev/null +++ b/kalendar/models.py @@ -0,0 +1,208 @@ +import datetime +from typing import Optional +from uuid import UUID + +from django.db import IntegrityError, models +from django.db.models import Q, QuerySet +from django.utils import timezone +from django.utils.safestring import mark_safe +from django.utils.translation import gettext_lazy as _ + +import kalendar +import settool_common.models as common_models + + +class Location(common_models.UUIDModelBase, common_models.LoggedModelBase): + shortname = models.CharField(_("short/simplified Address"), max_length=200) + + address = models.CharField(_("(Street)map-address"), blank=True, max_length=100) + room = models.CharField(_("Room"), blank=True, max_length=50) + + comment = models.CharField(_("Comment"), blank=True, max_length=200) + + def __str__(self) -> str: + message = self.shortname + if self.address: + message += f"
    {_('Adress')}: {self.address}" + if self.room: + message += f"
    {_('Room')}: {self.room}" + return mark_safe(message) # nosec: fully defined + + +class DateGroup(common_models.UUIDModelBase, common_models.LoggedModelBase): + location = models.ForeignKey(Location, null=True, blank=True, default=None, on_delete=models.SET_NULL) + comment = models.CharField(_("Comment"), blank=True, default="", max_length=200) + subscribers = models.ManyToManyField( + "tutors.Tutor", + verbose_name=_("Subscribers to a date_group"), + through="DateGroupSubscriber", + blank=True, + ) + + @property + def super_group(self): + if hasattr(self, "event"): + return self.event + if hasattr(self, "tour"): + return self.tour + return self.task + + @property + def group_type(self): + if hasattr(self, "event"): + return "event" + if hasattr(self, "tour"): + return "tour" + return "task" + + @property + def group_type_str(self): + lut = { + "event": _("Event"), + "tour": _("Tour"), + "task": _("Task"), + } + return lut[self.group_type] + + @property + def dates(self) -> list[datetime.datetime]: + return [date.date for date in Date.objects.filter(group=self.id).all()] + + @property + def date_objects(self) -> list["Date"]: + return list(Date.objects.filter(group=self.id).all()) + + def __str__(self) -> str: + name = self.super_group.name + if self.location: + return f"[{self.group_type_str}] {name} at {self.location}" + return f"[{self.group_type_str}] {name} ({_('no meeting point specified')})" + + +class BaseDateGroupInstance( + common_models.UUIDModelBase, + common_models.LoggedModelBase, + common_models.SemesterModelBase, +): + class Meta: + abstract = True + + name = models.CharField(_("Name"), max_length=250) + description = models.TextField(_("Description"), blank=True) + + @property + def meeting_point_str(self) -> str: + if not self.associated_meetings: + raise IntegrityError(f"{self.__class__} {self.id} has no associated_meetings") + if not self.associated_meetings.location: + return _("no meeting point specified") + return str(self.associated_meetings.location) + + associated_meetings = models.OneToOneField( + DateGroup, + verbose_name=_("Associated Meetings"), + null=True, + on_delete=models.SET_NULL, + ) + + @property + def first_datetime(self) -> Optional[datetime.datetime]: + if not self.associated_meetings: + raise IntegrityError(f"{self.__class__} {self.id} has no associated_meetings") + date: Optional[kalendar.models.Date] = self.associated_meetings.date_set.order_by("date").first() + if not date: + return None + return date.date + + @property + def last_datetime(self) -> Optional[datetime.datetime]: + if not self.associated_meetings: + raise IntegrityError(f"{self.__class__} {self.id} has no associated_meetings") + date: Optional[kalendar.models.Date] = self.associated_meetings.date_set.order_by("-date").first() + if not date: + return None + return date.date + datetime.timedelta(minutes=date.probable_length) + + @classmethod + def sorted_by_semester(cls, semester: int) -> list[type["BaseDateGroupInstance"]]: + all_instances: QuerySet[type[BaseDateGroupInstance]] = cls.objects.filter( # type: ignore + semester=semester, + ).all() + instances_sorting: list[tuple[datetime.datetime, type[BaseDateGroupInstance]]] = [ + (instance.first_datetime, instance) for instance in all_instances if instance.first_datetime # type: ignore + ] + return [instance for (_, instance) in sorted(instances_sorting, key=lambda t: t[0])] + + +class DateGroupSubscriber(common_models.UUIDModelBase, common_models.LoggedModelBase): + tutor = models.ForeignKey("tutors.Tutor", on_delete=models.CASCADE) + date = models.ForeignKey(DateGroup, on_delete=models.CASCADE) + + def __str__(self) -> str: + return f"{self.tutor}" + + +class Date(common_models.UUIDModelBase, common_models.LoggedModelBase): + group = models.ForeignKey(DateGroup, on_delete=models.CASCADE) + date = models.DateTimeField(_("Date and Time")) + probable_length = models.IntegerField(_("probable length in minutes"), default=60) + meeting_subscribers = models.ManyToManyField( + "tutors.Tutor", + verbose_name=_("Subscribers to one meeting"), + through="DateSubscriber", + blank=True, + ) + + def __str__(self) -> str: + return self.date.strftime("%x %X") + + def intersects(self, other_date: "Date") -> bool: + latest_start = max(self.date, other_date.date) + end_self = self.date + datetime.timedelta(minutes=self.probable_length) + end_other = other_date.date + datetime.timedelta(minutes=other_date.probable_length) + earliest_end = min(end_self, end_other) + return latest_start <= earliest_end + + @property + def table_color(self) -> str: + lut = { + ("event", True): "warning", + ("event", False): "success", + ("task", True): "secondary", + ("task", False): "light", + ("tour", True): "primary", + ("tour", False): "info", + } + return lut[self.group.group_type, self.is_in_future] + + @classmethod + def get_dates_for_tutor(cls, tutor_uuid: UUID, reference_time: datetime.datetime) -> QuerySet["Date"]: + subbed_date_groups = DateGroupSubscriber.objects.filter(tutor=tutor_uuid).values("date") + subbed_dates = DateSubscriber.objects.filter(tutor=tutor_uuid).values("date") + return ( + cls.objects.filter(date__gte=reference_time) + .filter(Q(group__in=subbed_date_groups) | Q(pk__in=subbed_dates)) + .order_by("-date") + .distinct() + .all() + ) + + @property + def probable_end(self) -> datetime.datetime: + return self.date + datetime.timedelta(minutes=self.probable_length) + + @property + def is_in_future(self) -> bool: + return self.probable_end > timezone.now() + + +class DateSubscriber(common_models.UUIDModelBase, common_models.LoggedModelBase): + tutor = models.ForeignKey("tutors.Tutor", on_delete=models.CASCADE) + date = models.ForeignKey(Date, on_delete=models.CASCADE) + + def __str__(self) -> str: + return f"{self.tutor}" + + +def create_associated_meetings() -> UUID: + return kalendar.models.DateGroup.objects.create().id diff --git a/kalendar/templates/kalendar/base_kalendar.html b/kalendar/templates/kalendar/base_kalendar.html new file mode 100644 index 00000000..f1def2a5 --- /dev/null +++ b/kalendar/templates/kalendar/base_kalendar.html @@ -0,0 +1,27 @@ +{% extends "base_card_layout.html" %} +{% load active_link_tags %} +{% load i18n %} +{% load django_bootstrap5 %} + +{% block set_common_navigation %} + +{% if perms.tutors.edit_tutors %} +
    {% trans "Dashboard" %} + +{% trans "List all dates" %} +{% trans "List future dates" %} +{% trans "List dates by event" %} +{% endif %} +{% endblock %} diff --git a/kalendar/templates/kalendar/dashboard.html b/kalendar/templates/kalendar/dashboard.html new file mode 100644 index 00000000..75f0bce4 --- /dev/null +++ b/kalendar/templates/kalendar/dashboard.html @@ -0,0 +1,16 @@ +{% extends "kalendar/base_kalendar.html" %} +{% load i18n %} +{% block head %} + +{% endblock %} + +{% block set_common_headercontent %}{% trans "Dashboard" %}{% endblock %} +{% block set_common_content %} +DASHBOARD +{% endblock %} diff --git a/kalendar/templates/kalendar/management/add_date.html b/kalendar/templates/kalendar/management/add_date.html new file mode 100644 index 00000000..da57201b --- /dev/null +++ b/kalendar/templates/kalendar/management/add_date.html @@ -0,0 +1,27 @@ +{% extends "kalendar/base_kalendar.html" %} +{% load static %} +{% load i18n %} +{% load django_bootstrap5 %} + +{% block set_common_headercontent %}{% trans "Add date" %}{% endblock %} + +{% block set_common_content %} +
    + {% csrf_token %} + +
    +
    + {% bootstrap_form form %} +
    +
    + + +
    + +{% endblock %} diff --git a/kalendar/templates/kalendar/management/delete_date.html b/kalendar/templates/kalendar/management/delete_date.html new file mode 100644 index 00000000..7056e1f9 --- /dev/null +++ b/kalendar/templates/kalendar/management/delete_date.html @@ -0,0 +1,42 @@ +{% extends "kalendar/base_kalendar.html" %} +{% load i18n %} + +{% block set_common_headercontent %}{% trans "Delete date" %}: {{ date }}{% endblock %} + +{% block set_common_content %} +
    +
    + + + + + + + + + + + + {% trans "no meeting point specified" as no_meeting_point %} + + +
    {% trans "Date" %}{{ date.date }}
    {% trans "probable length in minutes" %}{{ date.probable_length }}
    {% trans "Location" %}{{ date.group.location|default:no_meeting_point }}
    +
    +
    + +
    + {% csrf_token %} + + {% trans "Cancel" %} + +
    +{% endblock %} diff --git a/kalendar/templates/kalendar/management/edit_date.html b/kalendar/templates/kalendar/management/edit_date.html new file mode 100644 index 00000000..7a9c9c60 --- /dev/null +++ b/kalendar/templates/kalendar/management/edit_date.html @@ -0,0 +1,30 @@ +{% extends "kalendar/base_kalendar.html" %} +{% load static %} +{% load i18n %} +{% load django_bootstrap5 %} + +{% block set_common_headercontent %}{% trans "Edit date" %}: {{ date }}{% endblock %} + +{% block set_common_content %} +
    + {% csrf_token %} + +
    +
    + {% bootstrap_form form %} +
    +
    + + {% trans "Cancel" %} + +
    +{% endblock %} diff --git a/kalendar/templates/kalendar/management/list/chronological.html b/kalendar/templates/kalendar/management/list/chronological.html new file mode 100644 index 00000000..9704cba2 --- /dev/null +++ b/kalendar/templates/kalendar/management/list/chronological.html @@ -0,0 +1,92 @@ +{% extends "kalendar/base_kalendar.html" %} +{% load i18n %} +{% load django_bootstrap5 %} + +{% block head %} + +{% endblock %} + +{% block set_common_headercontent %}{% trans "List of dates" %}{% endblock %} + +{% block set_common_content %} +
    + + + + + {% if show_past %}{% endif %} + + {% if show_past %}{% endif %} + + + + {% if show_past %}{% endif %} + + +
    {% trans "Event" %}{% trans "Event lies in the past" %}{% trans "Task" %}{% trans "Task lies in the past" %}
    {% trans "Guidedtour" %}{% trans "Guidedtour lies in the past" %}
    +
    +
    + + + + + + + + + + + + + + + {% for date in dates %} + + + + + + {% if date.group.location %} + + + + {% else %} + + + + {% endif %} + + + {% endfor %} + +
    #{% trans "Name" %}{% trans "Date/Time" %}{% trans "Probable length" %}{% trans "short/simplified Address" %}{% trans "(Street)map-address" %}{% trans "Room" %}{% trans "Actions" %}
    {{ forloop.counter }} + + {{ date.group.super_group.name }} + + + {{ date.date }} + {{ date.probable_length }}m{{ date.group.location.shortname|default:"-" }}{{ date.group.location.address|default:"-" }}{{ date.group.location.room|default:"-" }}--- + + +
    +
    +{% endblock %} diff --git a/kalendar/templates/kalendar/management/list/grouped.html b/kalendar/templates/kalendar/management/list/grouped.html new file mode 100644 index 00000000..e3059166 --- /dev/null +++ b/kalendar/templates/kalendar/management/list/grouped.html @@ -0,0 +1,172 @@ +{% extends "kalendar/base_kalendar.html" %} +{% load i18n %} +{% load django_bootstrap5 %} + +{% block set_common_headercontent %}{% trans "List of events" %}{% endblock %} + +{% block set_common_content_overide %} +{% if not events %}{% trans "No Events exist" %}{% endif %} +
    + {% trans "no meeting point specified" as no_meeting_point %} + {% for event in events %} +
    +

    + +

    +
    +
    +

    {% trans "Event Data" %}

    + + + + + + + + + + + +
    {% trans "Description:" %}
    {{ event.description }}
    + + + + + + + + + {% if event.associated_meetings.location %} + + + + + {% else %} + + {% endif %} + + +
    {% trans "Location:" %}
    {% trans "short/simplified Address" %}: {{ event.associated_meetings.location.shortname|default:"-" }}{% trans "(Street)map-address" %}: {{ event.associated_meetings.location.address|default:"-" }}{% trans "Room" %}: {{ event.associated_meetings.location.room|default:"-" }}{% trans "Comment" %}: {{ event.associated_meetings.location.comment|default:"-" }}{{ no_meeting_point }}
    + {% if event.associated_meetings.date_set.exists %} + + + + + + + + {% for date in event.associated_meetings.date_set.all %} + + + + {% endfor %} + +
    {% trans "Dates:" %}
    {{ date.date }} ({{ date.probable_length }} minutes)
    + {% endif %} + +

    {% trans "Associated Tasks" %}

    + {% if event.task_set.all %} +
    + {% trans "no meeting point specified" as no_meeting_point %} + {% for task in event.task_set.all %} +
    +

    + +

    +
    +
    + + + + + + + + + + + +
    {% trans "Description:" %}
    {{ task.description }}
    + + + + + + + + + {% if task.associated_meetings.location %} + + + + + {% else %} + + {% endif %} + + +
    {% trans "Location:" %}
    {% trans "short/simplified Address" %}: {{ task.associated_meetings.location.shortname|default:"-" }}{% trans "(Street)map-address" %}: {{ task.associated_meetings.location.address|default:"-" }}{% trans "Room" %}: {{ task.associated_meetings.location.room|default:"-" }}{% trans "Comment" %}: {{ task.associated_meetings.location.comment|default:"-" }}{{ no_meeting_point }}
    + {% if task.associated_meetings.date_set.exists %} + + + + + + + + {% for date in task.associated_meetings.date_set.all %} + + + + {% endfor %} + +
    {% trans "Dates:" %}
    {{ date.date }} ({{ date.probable_length }} minutes)
    + {% endif %} +
    +
    +
    + {% endfor %} +
    + {% else %} + {% trans "no Tasks exist" %} + {% endif %} +
    +
    +
    + {% endfor %} +
    +{% endblock %} diff --git a/kalendar/templates/kalendar/management/view_date.html b/kalendar/templates/kalendar/management/view_date.html new file mode 100644 index 00000000..88aad4d9 --- /dev/null +++ b/kalendar/templates/kalendar/management/view_date.html @@ -0,0 +1,66 @@ +{% extends "kalendar/base_kalendar.html" %} +{% load i18n %} +{% load django_bootstrap5 %} + +{% block set_common_headercontent %}{% trans "Date details" %}{% endblock %} + +{% block set_common_content %} +
    +
    +

    + {% if date.group.is_group %}{% trans "Event-information" %} + {% else %}{% trans "Task-information" %} + {% endif %} +

    + + + + + + + + + +
    {% trans "Name" %}{{ date.group.super_group.name }}
    {% trans "Description" %}{{ date.group.super_group.description }}
    +
    +
    +

    {% trans "Date-information" %}

    + + + + + + + + + +
    {% trans "Date" %}{{ date.date }}
    {% trans "probable length in minutes" %}{{ date.probable_length }}
    +
    +
    +

    {% trans "Location-information" %}

    + + + + {% trans "no meeting point specified" as no_meeting_point %} + + +
    {% trans "Location" %}{{ date.group.location|default:no_meeting_point }}
    +
    +
    +{% trans "Back" %} +{% trans "Edit date" %} +{% trans "Delete date" %} +{% trans "Add date to the same group" %} +{% endblock %} diff --git a/kalendar/templates/kalendar/management/view_date_public.html b/kalendar/templates/kalendar/management/view_date_public.html new file mode 100644 index 00000000..0ceeaebe --- /dev/null +++ b/kalendar/templates/kalendar/management/view_date_public.html @@ -0,0 +1,54 @@ +{% extends "kalendar/base_kalendar.html" %} +{% load i18n %} +{% load django_bootstrap5 %} + +{% block set_common_headercontent %}{% trans "Date details" %}{% endblock %} + +{% block set_common_content %} +
    +
    +

    + {% if date.group.is_group %}{% trans "Event-information" %} + {% else %}{% trans "Task-information" %} + {% endif %} +

    + + + + + + + + + +
    {% trans "Name" %}{{ date.group.super_group.name }}
    {% trans "Description" %}{{ date.group.super_group.description }}
    +
    +
    +

    {% trans "Date-information" %}

    + + + + + + + + + +
    {% trans "Date" %}{{ date.date }}
    {% trans "probable length in minutes" %}{{ date.probable_length }}
    +
    +
    +

    {% trans "Location-information" %}

    + + + + {% trans "no meeting point specified" as no_meeting_point %} + + +
    {% trans "Location" %}{{ date.group.location|default:no_meeting_point }}
    +
    +
    +{% trans "Back" %} +{% endblock %} diff --git a/kalendar/tests/__init__.py b/kalendar/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/kalendar/translation.py b/kalendar/translation.py new file mode 100644 index 00000000..f5e39931 --- /dev/null +++ b/kalendar/translation.py @@ -0,0 +1,11 @@ +from modeltranslation.translator import TranslationOptions, translator + +from kalendar.models import Location + + +class LocationTranslationOptions(TranslationOptions): + fields = ("shortname", "address") + required_languages = ("en", "de") + + +translator.register(Location, LocationTranslationOptions) diff --git a/kalendar/urls.py b/kalendar/urls.py new file mode 100644 index 00000000..b4268b32 --- /dev/null +++ b/kalendar/urls.py @@ -0,0 +1,45 @@ +from django.urls import include, path +from django.views.generic import RedirectView + +from . import views +from .feeds import PersonalMeetingFeed + +app_name = "kalendar" +urlpatterns = [ + path("", RedirectView.as_view(pattern_name="kalendar:dashboard"), name="main_index"), + path("dashboard/", views.dashboard, name="dashboard"), + path( + "user/", + include( + [ + path( + "matching/", + include([]), + ), + path("/ical/", PersonalMeetingFeed(), name="ical_personal"), + path("/view//", views.view_date_public, name="view_date_public"), + ], + ), + ), + path( + "management/", + include( + [ + path( + "list/", + include( + [ + path("future/", views.list_future_dates, name="list_future_dates"), + path("chronologically/", views.list_dates, name="list_dates"), + path("grouped/", views.list_dates_grouped, name="list_dates_grouped"), + ], + ), + ), + path("add//", views.add_date, name="add_date"), + path("edit//", views.edit_date, name="edit_date"), + path("delete//", views.del_date, name="del_date"), + path("view//", views.view_date, name="view_date"), + ], + ), + ), +] diff --git a/kalendar/views.py b/kalendar/views.py new file mode 100644 index 00000000..f04c1322 --- /dev/null +++ b/kalendar/views.py @@ -0,0 +1,110 @@ +from typing import Any +from uuid import UUID + +from django import forms +from django.contrib import messages +from django.contrib.auth.decorators import login_required, permission_required +from django.core.handlers.wsgi import WSGIRequest +from django.db.models import QuerySet +from django.http import Http404, HttpResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.utils.translation import gettext_lazy as _ + +from kalendar.forms import DateForm +from kalendar.models import Date, DateGroup, DateGroupSubscriber, DateSubscriber +from settool_common.models import get_semester, Semester +from tutors.models import Event, Tutor + + +@login_required +def dashboard(request: WSGIRequest) -> HttpResponse: + context: dict[str, Any] = {} + return render(request, "kalendar/dashboard.html", context) + + +@permission_required("tutors.edit_tutors") +def list_future_dates(request: WSGIRequest) -> HttpResponse: + dates: list[Date] = [date for date in Date.objects.order_by("date").all() if date.is_in_future] + context = {"dates": dates} + return render(request, "kalendar/management/list/chronological.html", context) + + +@permission_required("tutors.edit_tutors") +def list_dates(request: WSGIRequest) -> HttpResponse: + dates: QuerySet[Date] = Date.objects.order_by("date").all() + context = {"dates": dates, "show_past": True} + return render(request, "kalendar/management/list/chronological.html", context) + + +@permission_required("tutors.edit_tutors") +def list_dates_grouped(request: WSGIRequest) -> HttpResponse: + semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) + events = Event.objects.filter(semester=semester).all() + context = {"events": events} + return render(request, "kalendar/management/list/grouped.html", context) + + +@permission_required("tutors.edit_tutors") +def add_date(request: WSGIRequest, date_group_pk: UUID) -> HttpResponse: + date_group: DateGroup = get_object_or_404(DateGroup, pk=date_group_pk) + + form = DateForm(request.POST or None, date_group=date_group) + if form.is_valid(): + date: Date = form.save() + messages.success(request, _("Successfully added date {date}.").format(date=date)) + return redirect("kalendar:main_index") + + context = {"date_group": date_group, "form": form} + return render(request, "kalendar/management/add_date.html", context) + + +@permission_required("tutors.edit_tutors") +def edit_date(request: WSGIRequest, date_pk: UUID) -> HttpResponse: + date: Date = get_object_or_404(Date, pk=date_pk) + + form = DateForm(request.POST or None, instance=date, date_group=date.group) + if form.is_valid(): + edited_date: Date = form.save() + messages.success(request, _("Successfully edited date {date}.").format(date=edited_date)) + return redirect("kalendar:main_index") + + context = {"date": date, "form": form} + return render(request, "kalendar/management/edit_date.html", context) + + +@permission_required("tutors.edit_tutors") +def del_date(request: WSGIRequest, date_pk: UUID) -> HttpResponse: + date: Date = get_object_or_404(Date, pk=date_pk) + + form = forms.Form(request.POST or None) + if form.is_valid(): + date.delete() + messages.success(request, _("Deleted date {date}.").format(date=date)) + return redirect("kalendar:main_index") + + context = {"date": date, "form": form} + return render(request, "kalendar/management/delete_date.html", context) + + +@permission_required("tutors.edit_tutors") +def view_date(request: WSGIRequest, date_pk: UUID) -> HttpResponse: + date: Date = get_object_or_404(Date, pk=date_pk) + + context = {"date": date} + return render(request, "kalendar/management/view_date.html", context) + + +def view_date_public(request: WSGIRequest, tutor_uuid: UUID, date_pk: UUID) -> HttpResponse: + tutor: Tutor = get_object_or_404(Tutor, pk=tutor_uuid) + date: Date = get_object_or_404(Date, pk=date_pk) + if ( + not DateSubscriber.objects.filter(tutor=tutor, date=date).exists() + and not DateGroupSubscriber.objects.filter(tutor=tutor, date=date.group).exists() + ): + raise Http404() + + context = { + "date": date, + "tutor": tutor, + } + return render(request, "kalendar/user/view_date_public.html", context) diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 46b7ad66..387ec6f7 100644 Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index cf35a7ae..b162f2e5 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -31,6 +31,10 @@ msgstr "SET-Tutoren" msgid "Guided tours of the institutes" msgstr "Institutsführungen" +#: templates/base.html +msgid "SET-Calendar" +msgstr "SET-Kalender" + #: templates/base.html msgid "Settings" msgstr "Einstellungen" @@ -269,6 +273,9 @@ msgstr "Keine" msgid "Unknown" msgstr "Unbekannt" +#~ msgid "Description:" +#~ msgstr "Beschreibung" + #~ msgid "May the SET be ever in yor favour" #~ msgstr "May the SET be ever in yor favour" diff --git a/package-lock.json b/package-lock.json index 2b049cf7..40ad68ad 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "AGPL-3.0-or-later", "dependencies": { - "@popperjs/core": "^2.11.2", + "@popperjs/core": "^2.11.4", "bootstrap": "^5.1.3", "bootstrap-icons": "^1.8.1", "bootstrap-switch-button": "^1.1.0", @@ -31,9 +31,9 @@ } }, "node_modules/@popperjs/core": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", - "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==", + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz", + "integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==", "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -136,9 +136,9 @@ } }, "@popperjs/core": { - "version": "2.11.2", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.2.tgz", - "integrity": "sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==" + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.4.tgz", + "integrity": "sha512-q/ytXxO5NKvyT37pmisQAItCFqA7FD/vNb8dgaJy3/630Fsc+Mz9/9f2SziBoIZ30TJooXyTwZmhi1zjXmObYg==" }, "bootstrap": { "version": "5.1.3", diff --git a/package.json b/package.json index 435a4e7c..62733578 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "author": "SET-Referat FSMPI", "license": "AGPL-3.0-or-later", "dependencies": { - "@popperjs/core": "^2.11.2", + "@popperjs/core": "^2.11.4", "bootstrap": "^5.1.3", "bootstrap-icons": "^1.8.1", "bootstrap-switch-button": "^1.1.0", diff --git a/requirements.txt b/requirements.txt index 9dddeeda..b231b4ac 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,8 +6,10 @@ django-bootstrap-modal-forms~=2.2.0 django-bootstrap5~=21.3 django-compref-keycloak~=1.2.0 django-crontab~=0.7.1 +django-ical~=1.8.3 django-modeltranslation~=0.17.5 django-tex~= 1.1.10 -Pillow~=9.0.1 +icalendar~=4.0.9 +Pillow~=9.1.0 python-dateutil~=2.8.2 qrcode~=7.3 diff --git a/requirements_dev.txt b/requirements_dev.txt index fd2ddfc0..57747b93 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,8 +1,8 @@ coverage~=6.3 -django-stubs~=1.9.0 +django-stubs~=1.10.1 lorem~=0.1.1 -mypy~=0.931 -pre-commit~=2.17.0 -pylint~=2.12.2 -pylint-django~=2.5.2 -types-python-dateutil~=2.8.9 +mypy~=0.942 +pre-commit~=2.18.1 +pylint~=2.13.4 +pylint-django~=2.5.3 +types-python-dateutil~=2.8.10 diff --git a/settool/settings/base_settings.py b/settool/settings/base_settings.py index a29fc90b..a75c799b 100644 --- a/settool/settings/base_settings.py +++ b/settool/settings/base_settings.py @@ -51,6 +51,7 @@ "bags", "fahrt", "tutors", + "kalendar", ] MIDDLEWARE = [ diff --git a/settool/urls.py b/settool/urls.py index a0e23c11..fd43844b 100644 --- a/settool/urls.py +++ b/settool/urls.py @@ -25,7 +25,9 @@ # SET-Fahrt path("fahrt/", include("fahrt.urls")), path("f/", RedirectView.as_view(pattern_name="fahrt:signup"), name="short_fahrt_signup"), - # Tutoren + # kalendar (typo to avoid name-clash) + path("calendar/", include("kalendar.urls")), + # tutors path("tutors/", include("tutors.urls")), path("c/", RedirectView.as_view(pattern_name="tutors:collaborator_signup"), name="short_collaborator_signup"), path("t/", RedirectView.as_view(pattern_name="tutors:tutor_signup"), name="short_tutor_signup"), diff --git a/settool_common/cron.py b/settool_common/cron.py index f5450884..281025e7 100644 --- a/settool_common/cron.py +++ b/settool_common/cron.py @@ -6,6 +6,7 @@ from django.conf import settings from django.core.mail import send_mail +from django.db import IntegrityError from django.db.models import QuerySet from django.db.models.query_utils import Q from django.utils import timezone @@ -13,6 +14,7 @@ import fahrt.models as m_fahrt import guidedtours.models as m_guidedtours +import kalendar.models as m_kalendar import settool_common.models as m_common import tutors.models as m_tutors from settool_common.models import AnonymisationLog, current_semester, Semester @@ -40,15 +42,15 @@ def tutor_reminder(semester: Semester, today: date) -> None: if tutor_settings and tutor_settings.mail_reminder: lookup_day = today + timedelta(days=max(tutor_settings.reminder_tour_days_count, 0)) task: m_tutors.Task - for task in m_tutors.Task.objects.filter( - Q(semester=semester) - & Q(begin__day=lookup_day.day) # begin is datetime - & Q(begin__month=lookup_day.month) - & Q(begin__year=lookup_day.year), - ): - tutor: m_tutors.Tutor - for tutor in list(task.tutors.all()): - tutor_settings.mail_reminder.send_mail_task(tutor, task) + for task in m_tutors.Task.objects.filter(semester=semester): + if not task.associated_meetings: + raise IntegrityError(f"task {task.id} has no associated_meetings") + meeting_date: m_kalendar.Date + for meeting_date in task.associated_meetings.date_set.all(): + if meeting_date.date.date() == lookup_day: + tutor: m_tutors.Tutor + for tutor in list(task.tutors.all()): + tutor_settings.mail_reminder.send_mail_task(tutor, task) def fahrt_date_reminder(semester: Semester, today: date) -> None: @@ -57,7 +59,7 @@ def fahrt_date_reminder(semester: Semester, today: date) -> None: lookup_day = today + timedelta(days=max(current_fahrt.reminder_tour_days_count, 0)) if current_fahrt.date == lookup_day: participant: m_fahrt.Participant - for participant in semester.fahrt_participant.filter(Q(semester=semester) & Q(status="confirmed")): + for participant in m_fahrt.Participant.objects.filter(semester=semester, status="confirmed"): current_fahrt.mail_reminder.send_mail_participant(participant) @@ -66,8 +68,10 @@ def fahrt_payment_reminder(semester: Semester, today: date) -> None: if current_fahrt and current_fahrt.mail_payment_deadline: lookup_day = today + timedelta(days=max(current_fahrt.reminder_payment_deadline_days_count, 0)) participant: m_fahrt.Participant - for participant in semester.fahrt_participant.filter( - Q(status="confirmed") & Q(payment_deadline=lookup_day), + for participant in m_fahrt.Participant.objects.filter( + semester=semester, + status="confirmed", + payment_deadline=lookup_day, ): current_fahrt.mail_payment_deadline.send_mail_participant(participant) @@ -105,7 +109,8 @@ def anonymize_fahrt(semester: Semester, log_name: str) -> bool: semester_participants.exclude(status=m_fahrt.Participant.STATUS_CONFIRMED).delete() participant: m_fahrt.Participant for participant in semester_participants.all(): - participant.registration_time = timezone.now() + participant.created_at = timezone.now() + participant.updated_at = timezone.now() participant.firstname = f"f {participant.pk}" participant.surname = f"l {participant.pk}" participant.birthday = timezone.now().date() @@ -124,23 +129,30 @@ def anonymize_fahrt(semester: Semester, log_name: str) -> bool: def anonymize_guidedtours(semester: Semester, log_name: str) -> bool: semester_tours: QuerySet[m_guidedtours.Tour] = m_guidedtours.Tour.objects.filter(semester=semester) most_recent_tour: Optional[m_guidedtours.Tour] = semester_tours.order_by("date").last() - if most_recent_tour and date_is_too_old(most_recent_tour.date, log_name): - # guidedtours is save to anonymize for this semester - participant: m_guidedtours.Participant - for participant in m_guidedtours.Participant.objects.filter(tour__semester=semester).all(): - participant.firstname = f"f {participant.pk}" - participant.surname = f"l {participant.pk}" - participant.email = f"{participant.pk}@example.com" - participant.phone = f"-1 000 {participant.pk}" - participant.time = timezone.now() - participant.save() - return True - return False + if not most_recent_tour or not most_recent_tour.associated_meetings: + return False + successfully_anonimised = False + for date_obj in most_recent_tour.associated_meetings.dates: + if date_obj and date_is_too_old(date_obj, log_name): + # guidedtours is save to anonymize for this semester + participant: m_guidedtours.Participant + for participant in m_guidedtours.Participant.objects.filter(tour__semester=semester).all(): + participant.firstname = f"f {participant.pk}" + participant.surname = f"l {participant.pk}" + participant.email = f"{participant.pk}@example.com" + participant.phone = f"-1 000 {participant.pk}" + participant.updated_at = timezone.now() + participant.created_at = timezone.now() + participant.save() + successfully_anonimised = True + return successfully_anonimised # noqa: R504 def anonymize_tutors(semester: Semester, log_name: str) -> bool: - most_recent_task: Optional[m_tutors.Task] = m_tutors.Task.objects.filter(semester=semester).order_by("end").last() - if most_recent_task and date_is_too_old(most_recent_task.end, log_name): + task_datetimes: list[real_datetime.datetime] = [ + task.last_datetime for task in m_tutors.Task.objects.filter(semester=semester) + ] + if task_datetimes and date_is_too_old(max(task_datetimes), log_name): # guidedtours is save to anonymize for this semester m_tutors.Answer.objects.filter(tutor__semester=semester).delete() m_tutors.MailTutorTask.objects.filter(tutor__semester=semester).delete() @@ -151,7 +163,8 @@ def anonymize_tutors(semester: Semester, log_name: str) -> bool: tutor.first_name = f"f {tutor.pk}" tutor.last_name = f"l {tutor.pk}" tutor.email = f"{tutor.pk}@example.com" - tutor.registration_time = timezone.now() + tutor.created_at = timezone.now() + tutor.updated_at = timezone.now() tutor.birthday = timezone.now() tutor.matriculation_number = str(tutor.pk).zfill(8)[:8] tutor.comment = "" diff --git a/settool_common/fixtures/showroom_fixture.py b/settool_common/fixtures/showroom_fixture.py index d7576564..f4bee47e 100644 --- a/settool_common/fixtures/showroom_fixture.py +++ b/settool_common/fixtures/showroom_fixture.py @@ -11,6 +11,7 @@ import bags.models import fahrt.models import guidedtours.models +import kalendar.models import settool_common.models import tutors.models from settool_common.fixtures.test_fixture import generate_semesters @@ -59,11 +60,104 @@ def showroom_fixture_state_no_confirmation(): tutors_questions = _generate_questions(common_semesters) _generate_answers(tutors_questions, tutors_list) tutors_events = _generate_events(common_semesters, common_subjects) - _generate_tasks_tutorasignemt(tutors_events, tutors_list, tutors_questions) + _generate_tasks_tutorasignent(tutors_events, tutors_list, tutors_questions) _generate_tutor_settings(common_semesters) # TODO tutors_mailtutortask # TODO tutors_subjecttutorcountassignment + # app kalendar + _generate_kalendar_locations() + _generate_kalendar_date_groups() + _generate_kalendar_dates() + _generate_kalendar_subscriptions() + + +def _generate_kalendar_subscriptions(): # nosec: this is only used in a fixture + actual_tutors = list(tutors.models.Tutor.objects.filter(status=tutors.models.Tutor.STATUS_ACCEPTED).all()) + collaborators = list(tutors.models.Tutor.objects.filter(status=tutors.models.Tutor.STATUS_EMPLOYEE).all()) + dates = list(kalendar.models.Date.objects.all()) + date_groups = list(kalendar.models.DateGroup.objects.all()) + for tutor in actual_tutors: + if random.choice((True, True, True, False)): + dates_for_tutor = random.randint(1, len(dates) // len(actual_tutors) + 1) + selected_dates = random.sample(dates, dates_for_tutor) + for date in selected_dates: + kalendar.models.DateSubscriber.objects.create(date=date, tutor=tutor) + for collaborator in collaborators: + # 90% get a normal amount, 5% get nothing, 5% get all + if random.randint(1, 100) > 10: + date_groups_for_collaborator = random.randint(1, len(dates) // len(collaborators) + 1) + selected_date_groups = random.sample(date_groups, date_groups_for_collaborator) + for date_group in selected_date_groups: + kalendar.models.DateGroupSubscriber.objects.create(date=date_group, tutor=collaborator) + elif random.choice((True, False)): + for date_group in date_groups: + kalendar.models.DateGroupSubscriber.objects.create(date=date_group, tutor=collaborator) + + +def _generate_kalendar_locations(): + locations = [ + "Fachschaftsbüro", + "Serverraum", + "LRZ", + "Chemie-gebäude", + "Interrims-Höhrsaal", + "MI-Magistrale", + "MI-Vorplazt", + "Grillareal", + "Galileo", + "MW2001", + "MW0001", + ] + for shortname in locations: + address = random.choice( + ( + f"Boltzmannstr. {random.randint(1, 30)}, 85748 Garching", + random.choice(("Arcisstraße 17, 80333 München", "")), + ), + ) + kalendar.models.Location.objects.create( + shortname=shortname, + shortname_de=shortname, + shortname_en=shortname, + address=address, + address_de=address, + address_en=address, + room=random.choice(("Magistrale", "FS-Büro", "MW0001", "MW2001", "007", "")), + comment=lorem.sentence()[:200] if random.choice((True, False, False, False)) else "", + ) + + +def _generate_kalendar_date_groups(): + locations: list[kalendar.models.Location] = list(kalendar.models.Location.objects.all()) + date_groups: list[kalendar.models.DateGroup] = list(kalendar.models.DateGroup.objects.all()) + for date_group in date_groups: + if random.choice((True, True, True, False)): + date_group.location = random.choice(locations) + if random.choice((True, False, False, False)): + date_group.comment = lorem.sentence()[:200] + date_group.save() + + +def _generate_kalendar_dates(): + date_groups: list[kalendar.models.DateGroup] = list(kalendar.models.DateGroup.objects.all()) + for date_group in date_groups: + if random.choice((True, True, False)): + for _ in range(random.choice((1, 1, 2, 2, 4, 4, 7, 7))): + kalendar.models.Date.objects.create( + group=date_group, + date=django.utils.timezone.make_aware( + datetime.today() + + timedelta(days=random.randint(1, 60)) + + timedelta( + hours=random.randint(0, 24), + minutes=random.randint(0, 60), + ) + - timedelta(days=random.randint(1, 30)), + ), + probable_length=random.choice((60, 60, 60, 120, 240, 0, 1, 20)), + ) + def _generate_tutor_settings(common_semesters): all_mail_by_set_tutor = tutors.models.TutorMail.objects.all() @@ -193,7 +287,7 @@ def _generate_transportation(fahrt_participants): return_departure_time=random.choice( [participant.semester.fahrt.date + timedelta(hours=random.randint(-5, 5)), None], ), - deparure_place=random.choice([lorem.sentence(), "", ""]), + departure_place=random.choice([lorem.sentence(), "", ""]), places=places_count, ) participant.transportation = trans @@ -374,12 +468,13 @@ def _generate_giveaways( ) -def _generate_tasks_tutorasignemt( +def _generate_tasks_tutorasignent( events, tutors_list, questions, ): tasks = [] + all_tutors = list(tutors.models.Tutor.objects.all()) for event in events: tutors_current_semester = [tutor for tutor in tutors_list if tutor.semester == event.semester] number1 = random.randint(0, len(tutors_current_semester)) @@ -387,22 +482,17 @@ def _generate_tasks_tutorasignemt( filtered_questions = [question for question in questions if question.semester == event.semester] event_subjects = list(event.subjects.all()) for i in range(0, random.randint(0, 4)): + person_1, person_2 = random.sample(all_tutors, 2) + person_1 = random.choice((None, person_1, person_1, person_2, event.meeting_chairperson)) + person_2 = random.choice((None, person_2, person_2, event.event_leader)) task = tutors.models.Task.objects.create( semester=event.semester, name_en=f"Task {i}", name_de=f"Task {i}", description_en=lorem.paragraph(), description_de=lorem.paragraph(), - begin=django.utils.timezone.make_aware( - datetime.today().replace(day=1, month=1) - + timedelta(days=random.randint(0, 30), minutes=random.randint(1, 1000)), - ), - end=django.utils.timezone.make_aware( - datetime.today().replace(day=1, month=12) - - timedelta(days=random.randint(0, 30), minutes=random.randint(1, 1000)), - ), - meeting_point_en=lorem.sentence()[: random.randint(0, 49)], - meeting_point_de=lorem.sentence()[: random.randint(0, 49)], + meeting_chairperson=person_1, + task_leader=person_2, event=event, min_tutors=min(number1, number2), max_tutors=max(number1, number2), @@ -430,7 +520,11 @@ def _generate_tasks_tutorasignemt( def _generate_events(semesters, subjects): events = [] + all_tutors = list(tutors.models.Tutor.objects.all()) for i in range(random.randint(10, 20)): + person_1, person_2 = random.sample(all_tutors, 2) + person_1 = random.choice((None, person_1)) + person_2 = random.choice((None, person_2)) event = tutors.models.Event.objects.create( semester=random.choice(semesters), name=f"Event {i}", @@ -438,16 +532,8 @@ def _generate_events(semesters, subjects): name_de=f"Event {i}", description_en=lorem.paragraph(), description_de=lorem.paragraph(), - begin=django.utils.timezone.make_aware( - datetime.today().replace(day=1, month=1) - + timedelta(days=random.randint(0, 30), minutes=random.randint(1, 1000)), - ), - end=django.utils.timezone.make_aware( - datetime.today().replace(day=1, month=12) - - timedelta(days=random.randint(0, 30), minutes=random.randint(1, 1000)), - ), - meeting_point_en=lorem.sentence(), - meeting_point_de=lorem.sentence(), + meeting_chairperson=person_1, + event_leader=person_2, ) filtered_subjects = random.sample(subjects, random.randint(0, len(subjects))) event.subjects.set(filtered_subjects) diff --git a/settool_common/locale/de/LC_MESSAGES/django.po b/settool_common/locale/de/LC_MESSAGES/django.po index 1047c0af..fb63e31b 100644 --- a/settool_common/locale/de/LC_MESSAGES/django.po +++ b/settool_common/locale/de/LC_MESSAGES/django.po @@ -24,6 +24,10 @@ msgstr "" msgid "I accept the terms and conditions of the following privacy policy:" msgstr "Ich stimme der folgenden Datenschutzerklärung zu:" +#: settool_common/models.py +msgid "Semester" +msgstr "Semester" + #: settool_common/models.py msgid "SET" msgstr "SET" @@ -84,10 +88,6 @@ msgstr "Wintersemester" msgid "Summer semester" msgstr "Sommersemester" -#: settool_common/models.py -msgid "Semester" -msgstr "Semester" - #: settool_common/models.py msgid "Year" msgstr "Jahr" diff --git a/settool_common/migrations/0011_remove_mail_semester.py b/settool_common/migrations/0011_remove_mail_semester.py index 09d7ff71..6c25899b 100644 --- a/settool_common/migrations/0011_remove_mail_semester.py +++ b/settool_common/migrations/0011_remove_mail_semester.py @@ -18,8 +18,5 @@ class Migration(migrations.Migration): operations = [ migrations.RunPython(subject_includes_semester), - migrations.RemoveField( - model_name="mail", - name="semester", - ), + migrations.RemoveField(model_name="mail", name="semester"), ] diff --git a/settool_common/migrations/0017_anonymisationlog_created_at_and_more.py b/settool_common/migrations/0017_anonymisationlog_created_at_and_more.py new file mode 100644 index 00000000..da533976 --- /dev/null +++ b/settool_common/migrations/0017_anonymisationlog_created_at_and_more.py @@ -0,0 +1,80 @@ +# Generated by Django 4.0.3 on 2022-04-01 13:10 + +import django.utils.timezone +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("settool_common", "0016_alter_coursebundle_options_alter_subject_options_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="anonymisationlog", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="anonymisationlog", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="coursebundle", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="coursebundle", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="mail", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="mail", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="qrcode", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="qrcode", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="semester", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="semester", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + migrations.AddField( + model_name="subject", + name="created_at", + field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), + preserve_default=False, + ), + migrations.AddField( + model_name="subject", + name="updated_at", + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/settool_common/migrations/0018_alter_anonymisationlog_semester.py b/settool_common/migrations/0018_alter_anonymisationlog_semester.py new file mode 100644 index 00000000..f0dd14ce --- /dev/null +++ b/settool_common/migrations/0018_alter_anonymisationlog_semester.py @@ -0,0 +1,23 @@ +# Generated by Django 4.0.3 on 2022-04-01 16:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("settool_common", "0017_anonymisationlog_created_at_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="anonymisationlog", + name="semester", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="settool_common.semester", + verbose_name="Semester", + ), + ), + ] diff --git a/settool_common/models.py b/settool_common/models.py index ad250ebd..2a220105 100644 --- a/settool_common/models.py +++ b/settool_common/models.py @@ -1,8 +1,9 @@ import datetime import os import re +import uuid from io import BytesIO -from typing import Any, List, Optional, Union +from typing import Any, Optional, Union import qrcode from django.conf import settings @@ -20,7 +21,29 @@ from .settings import SEMESTER_SESSION_KEY -class Mail(models.Model): +class UUIDModelBase(models.Model): + class Meta: + abstract = True + + id = models.UUIDField(unique=True, primary_key=True, default=uuid.uuid4) + + +class SemesterModelBase(models.Model): + class Meta: + abstract = True + + semester = models.ForeignKey("settool_common.Semester", verbose_name=_("Semester"), on_delete=models.CASCADE) + + +class LoggedModelBase(models.Model): + class Meta: + abstract = True + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + +class Mail(LoggedModelBase): SET = "SET-Team " SET_FAHRT = "SET-Fahrt-Team " SET_TUTOR = "SET-Tutor-Team " @@ -44,12 +67,7 @@ class Mail(models.Model): def check_perm(cls, user): return any(user.has_perm(perm) for perm in cls.required_perm) - sender = models.CharField( - max_length=100, - choices=FROM_CHOICES, - default=SET, - verbose_name=_("From"), - ) + sender = models.CharField(max_length=100, choices=FROM_CHOICES, default=SET, verbose_name=_("From")) subject = models.CharField( _("Email subject"), @@ -57,17 +75,9 @@ def check_perm(cls, user): help_text=_("You may use placeholders for the subject."), ) - text = models.TextField( - _("Text"), - help_text=_("You may use placeholders for the text."), - ) + text = models.TextField(_("Text"), help_text=_("You may use placeholders for the text.")) - comment = models.CharField( - _("Comment"), - max_length=200, - default="", - blank=True, - ) + comment = models.CharField(_("Comment"), max_length=200, default="", blank=True) def __str__(self) -> str: if self.comment: @@ -89,8 +99,8 @@ def get_mail(self, context: Union[Context, dict[str, Any], None]) -> tuple[str, def send_mail( self, context: Union[Context, dict[str, Any], None], - recipients: Union[List[str], str], - attachments: Optional[Union[HttpResponse, list[tuple[str, Any, str]]]] = None, + recipients: Union[list[str], str], + attachments: Optional[list[Union[HttpResponse, tuple[str, Any, str]]]] = None, ) -> bool: if isinstance(recipients, str): recipients = [recipients] @@ -112,7 +122,9 @@ def send_mail( send_mail(subject, text, self.sender, recipients, fail_silently=False) else: mail = EmailMessage(subject, text, self.sender, recipients) - for (filename, content, mimetype) in [clean_attachable(attach) for attach in attachments]: + attach: Union[HttpResponse, tuple[str, Any, str]] + for attach in attachments: + (filename, content, mimetype) = clean_attachable(attach) mail.attach(filename, content, mimetype) mail.send(fail_silently=False) return True @@ -127,7 +139,7 @@ def clean_attachable(response: Union[HttpResponse, tuple[str, Any, str]]) -> tup @deconstructible -class Semester(models.Model): +class Semester(LoggedModelBase): class Meta: unique_together = (("semester", "year"),) ordering = ["year", "semester"] @@ -146,10 +158,9 @@ class Meta: verbose_name=_("Semester"), ) - year = models.PositiveIntegerField( - verbose_name=_("Year"), - ) + year = models.PositiveIntegerField(verbose_name=_("Year")) + @property def short_form(self) -> str: return f"{self.semester}{str(self.year)[2:]}" @@ -157,7 +168,7 @@ def __str__(self) -> str: return f"{self.get_semester_display()} {self.year:4}" -class CourseBundle(models.Model): +class CourseBundle(LoggedModelBase): class Meta: ordering = ["name"] @@ -171,7 +182,7 @@ def __str__(self) -> str: return self.name -class Subject(models.Model): +class Subject(LoggedModelBase): class Meta: unique_together = (("degree", "subject"),) ordering = ["degree", "course_bundle", "subject"] @@ -179,11 +190,7 @@ class Meta: BACHELOR = "BA" MASTER = "MA" - course_bundle = models.ForeignKey( - CourseBundle, - on_delete=models.CASCADE, - verbose_name=_("Course-bundle"), - ) + course_bundle = models.ForeignKey(CourseBundle, on_delete=models.CASCADE, verbose_name=_("Course-bundle")) degree = models.CharField( max_length=2, @@ -195,10 +202,7 @@ class Meta: verbose_name=_("Degree"), ) - subject = models.CharField( - max_length=100, - verbose_name=_("Subject"), - ) + subject = models.CharField(max_length=100, verbose_name=_("Subject")) def __str__(self) -> str: return f"{self.get_degree_display()} {self.subject} ({self.course_bundle})" @@ -227,7 +231,7 @@ def get_semester(request: HttpRequest) -> int: return sem # noqa: R504 -class QRCode(models.Model): +class QRCode(LoggedModelBase): content = models.CharField(max_length=200, unique=True) qr_code = models.ImageField(upload_to="qr_codes", blank=True) @@ -286,8 +290,7 @@ def auto_delete_qr_code_on_delete(sender, instance, **_kwargs): os.remove(instance.qr_code.path) -class AnonymisationLog(models.Model): - semester = models.ForeignKey(Semester, on_delete=models.CASCADE) +class AnonymisationLog(LoggedModelBase, SemesterModelBase): anon_log_str = models.CharField(max_length=10) def __str__(self) -> str: diff --git a/settool_common/tests/test_email.py b/settool_common/tests/test_email.py index 1f462502..d586b13d 100644 --- a/settool_common/tests/test_email.py +++ b/settool_common/tests/test_email.py @@ -13,6 +13,7 @@ import fahrt.models as fahrt_models import guidedtours.models as tour_models +import kalendar.models as kalendar_m import settool_common.fixtures.showroom_fixture import settool_common.models as common_models import tutors.models as tutor_models @@ -154,12 +155,16 @@ def setUpTestData(cls) -> None: subject_en="Operatopms Recearch", course_bundle=course_bundle_info, ) + date_group = kalendar_m.DateGroup.objects.create() + kalendar_m.Date.objects.create( + group=date_group, + date=cls.todatetime, + probable_length=60, + ) event = tutor_models.Event.objects.create( semester=common_models.current_semester(), name="enent", - begin=cls.todatetime, - end=cls.todatetime, - meeting_point="-", + associated_meetings=date_group, ) for i in range(2): tutor_models.Tutor.objects.create( @@ -174,12 +179,16 @@ def setUpTestData(cls) -> None: ) tutors = list(tutor_models.Tutor.objects.all()) for i in range(10): + date_group = kalendar_m.DateGroup.objects.create() + kalendar_m.Date.objects.create( + group=date_group, + date=cls.todatetime, + probable_length=60, + ) task = tutor_models.Task.objects.create( semester=common_models.current_semester(), name=f"task {i}", - begin=cls.todatetime, - end=cls.todatetime, - meeting_point="-", + associated_meetings=date_group, event=event, ) for tutor in tutors: @@ -199,15 +208,12 @@ def test_tutor_reminder(self): lookup_day = self.today + timedelta(days=days_until) task: tutor_models.Task - for task in tutor_models.Task.objects.filter( - Q(semester=common_models.current_semester()) - & Q(begin__day=lookup_day.day) # begin is datetime - & Q(begin__month=lookup_day.month) - & Q(begin__year=lookup_day.year), - ): - tutor: tutor_models.Tutor - for tutor in list(task.tutors.all()): - self.setting.mail_reminder.send_mail_task(tutor, task) + for task in tutor_models.Task.objects.filter(semester=common_models.current_semester()): + first_datetime = task.first_datetime + if first_datetime.date() == lookup_day: + tutor: tutor_models.Tutor + for tutor in list(task.tutors.all()): + self.setting.mail_reminder.send_mail_task(tutor, task) expected = serialise_outbox() cron.tutor_reminder(common_models.current_semester(), self.today) curr = serialise_outbox() @@ -244,14 +250,21 @@ def setUpTestData(cls) -> None: course_bundle=course_bundle_info, ) for i in range(4): - tour_models.Tour.objects.create( + tour = tour_models.Tour.objects.create( semester=common_models.current_semester(), - name="tour today", - date=cls.today + timedelta(days=i), + name=f"tour {i}", capacity=10, open_registration=cls.todatetime, close_registration=cls.todatetime, ) + meeting = tour.associated_meetings + if not meeting: + raise ValueError("No associated_meeting for Tour") + kalendar_m.Date.objects.create( + group=meeting, + date=cls.today + timedelta(days=i), + probable_length=60, + ) for tour in tour_models.Tour.objects.all(): for i in range(10): tour_models.Participant.objects.create( diff --git a/templates/base.html b/templates/base.html index 833f23ad..27f5efee 100644 --- a/templates/base.html +++ b/templates/base.html @@ -102,6 +102,10 @@ >{% trans "Guided tours of the institutes" %} {% endif %} {% if user.is_authenticated %} + {% trans "SET-Calendar" %}
    {% endblock %} + {% block set_common_content_overide %}
    {% block set_common_headercontent %}{% endblock %} @@ -28,6 +29,7 @@ {% block set_common_content %}{% endblock %}
    + {% endblock %} {% block navigation-override-2 %}
    diff --git a/tutors/forms.py b/tutors/forms.py index 109696f2..ba831d8d 100644 --- a/tutors/forms.py +++ b/tutors/forms.py @@ -32,16 +32,13 @@ def clean(self): super().clean() mail = self.cleaned_data.get("email") if mail: - tutors = Tutor.objects.filter(email=mail, semester=self.semester) + tutors = Tutor.objects.filter(semester=self.semester, email=mail) if tutors.count() > 0 and tutors.first().id != self.instance.id: self.add_error("email", _("The email address is already in use.")) matriculation_number = self.cleaned_data.get("matriculation_number") if matriculation_number: - tutors = Tutor.objects.filter( - matriculation_number=matriculation_number, - semester=self.semester, - ) + tutors = Tutor.objects.filter(semester=self.semester, matriculation_number=matriculation_number) if tutors.count() > 0 and tutors.first().id != self.instance.id: self.add_error( "matriculation_number", @@ -94,11 +91,7 @@ def __init__(self, *args, **kwargs): class EventAdminForm(SemesterBasedModelForm): class Meta: model = Event - exclude = ["semester", "name", "description", "meeting_point"] - widgets = { - "begin": DateTimePickerInput(format="%Y-%m-%d %H:%M"), - "end": DateTimePickerInput(format="%Y-%m-%d %H:%M"), - } + exclude = ["semester", "name", "description", "associated_meetings"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -125,26 +118,11 @@ def save(self, commit=True): return instance - def clean(self): - cleaned_data = super().clean() - - begin = cleaned_data.get("begin") - end = cleaned_data.get("end") - if begin and end and end <= begin: - msg = _("Begin time must be before end time.") - self.add_error("begin", msg) - self.add_error("end", msg) - return cleaned_data - class TaskAdminForm(SemesterBasedModelForm): class Meta: model = Task - exclude = ["semester", "name", "description", "meeting_point", "tutors"] - widgets = { - "begin": DateTimePickerInput(format="%Y-%m-%d %H:%M"), - "end": DateTimePickerInput(format="%Y-%m-%d %H:%M"), - } + exclude = ["semester", "name", "description", "tutors", "associated_meetings"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -185,17 +163,6 @@ def save(self, commit=True): return instance - def clean(self): - cleaned_data = super().clean() - - begin = cleaned_data.get("begin") - end = cleaned_data.get("end") - if begin and end and end <= begin: - msg = _("Begin time must be before end time.") - self.add_error("begin", msg) - self.add_error("end", msg) - return cleaned_data - class TaskAssignmentForm(SemesterBasedModelForm): class Meta: @@ -255,7 +222,7 @@ def clean(self): begin = cleaned_data.get("open_registration") end = cleaned_data.get("close_registration") if begin and end and end <= begin: - msg = _("Begin time must be before end time.") + msg = _("open_registration time must be before close_registration time.") self.add_error("open_registration", msg) self.add_error("close_registration", msg) return cleaned_data diff --git a/tutors/locale/de/LC_MESSAGES/django.mo b/tutors/locale/de/LC_MESSAGES/django.mo index 2bb81b13..46ca8596 100644 Binary files a/tutors/locale/de/LC_MESSAGES/django.mo and b/tutors/locale/de/LC_MESSAGES/django.mo differ diff --git a/tutors/locale/de/LC_MESSAGES/django.po b/tutors/locale/de/LC_MESSAGES/django.po index 74f84095..489c7d51 100644 --- a/tutors/locale/de/LC_MESSAGES/django.po +++ b/tutors/locale/de/LC_MESSAGES/django.po @@ -24,8 +24,10 @@ msgid "I want to receive ECTS for my work as a SET-Colaborator." msgstr "Ich möchte ECTS für meine Arbeit als SET-Mitarbeiter erhalten." #: tutors/forms.py -msgid "Begin time must be before end time." -msgstr "Der Startzeitpunkt muss vor dem Endzeitpunkt liegen." +msgid "open_registration time must be before close_registration time." +msgstr "" +"Der open_registration-Zeitpunkt muss vor dem close_registration-Zeitpunkt " +"liegen." #: tutors/forms.py msgid "Tutors (selected have not yet received this email)" @@ -99,10 +101,6 @@ msgstr "" "Sende die Erinnerungs Email automatisch diese menge an Tagen vor dem Task " "(0=gleicher Tag)" -#: tutors/models.py -msgid "Semester" -msgstr "Semester" - #: tutors/models.py tutors/templates/tutors/task/view.html #: tutors/templates/tutors/tutor/delete.html #: tutors/templates/tutors/tutor/list.html @@ -161,10 +159,6 @@ msgstr "T-Shirt Größe" msgid "Tshirt as Girls cut" msgstr "T-Shirt als Girls cut" -#: tutors/models.py -msgid "Registration Time" -msgstr "Zeitpunkt der Registrierung" - #: tutors/models.py msgid "Tutor Answers" msgstr "Antworten des Tutors" @@ -179,59 +173,6 @@ msgstr "Status" msgid "Comment" msgstr "Kommentar" -#: tutors/models.py tutors/templates/tutors/dashboard.html -#: tutors/templates/tutors/event/delete.html -#: tutors/templates/tutors/event/list.html -#: tutors/templates/tutors/event/view.html -#: tutors/templates/tutors/task/delete.html -#: tutors/templates/tutors/task/list.html -#: tutors/templates/tutors/task/view.html -#: tutors/templates/tutors/tutor/delete.html -#: tutors/templates/tutors/tutor/view.html -msgid "Name" -msgstr "Name" - -#: tutors/models.py tutors/templates/tutors/event/delete.html -#: tutors/templates/tutors/event/view.html -#: tutors/templates/tutors/task/delete.html -#: tutors/templates/tutors/task/view.html -msgid "Description" -msgstr "Beschreibung" - -#: tutors/models.py tutors/templates/tutors/event/delete.html -#: tutors/templates/tutors/event/list.html -#: tutors/templates/tutors/event/view.html -#: tutors/templates/tutors/task/delete.html -#: tutors/templates/tutors/task/list.html -#: tutors/templates/tutors/task/view.html -#: tutors/templates/tutors/tutor/delete.html -#: tutors/templates/tutors/tutor/view.html -msgid "Begin" -msgstr "Start" - -#: tutors/models.py tutors/templates/tutors/dashboard.html -#: tutors/templates/tutors/event/delete.html -#: tutors/templates/tutors/event/list.html -#: tutors/templates/tutors/event/view.html -#: tutors/templates/tutors/task/delete.html -#: tutors/templates/tutors/task/list.html -#: tutors/templates/tutors/task/view.html -#: tutors/templates/tutors/tutor/delete.html -#: tutors/templates/tutors/tutor/view.html -msgid "End" -msgstr "Ende" - -#: tutors/models.py tutors/templates/tutors/event/delete.html -#: tutors/templates/tutors/event/list.html -#: tutors/templates/tutors/event/view.html -#: tutors/templates/tutors/task/delete.html -#: tutors/templates/tutors/task/list.html -#: tutors/templates/tutors/task/view.html -#: tutors/templates/tutors/tutor/delete.html -#: tutors/templates/tutors/tutor/view.html -msgid "Meeting Point" -msgstr "Treffpunkt" - #: tutors/models.py tutors/templates/tutors/dashboard.html #: tutors/templates/tutors/event/delete.html #: tutors/templates/tutors/event/view.html @@ -239,14 +180,6 @@ msgstr "Treffpunkt" msgid "Subjects" msgstr "Studiengänge" -#: tutors/models.py -msgid "Task name" -msgstr "Taskname" - -#: tutors/models.py -msgid "Meeting point" -msgstr "Treffpunkt" - #: tutors/models.py tutors/templates/tutors/task/delete.html #: tutors/templates/tutors/task/list.html #: tutors/templates/tutors/task/view.html @@ -263,6 +196,10 @@ msgstr "Zugelassene Studiengänge" msgid "Requirements" msgstr "Voraussetzungen" +#: tutors/models.py +msgid "Assigned tutors" +msgstr "Eingeteilte Tutoren" + #: tutors/models.py msgid "Tutors (min)" msgstr "Tutoren (min)" @@ -271,10 +208,6 @@ msgstr "Tutoren (min)" msgid "Tutors (max)" msgstr "Tutoren (max)" -#: tutors/models.py -msgid "Assigned tutors" -msgstr "Eingeteilte Tutoren" - #: tutors/models.py msgid "absent" msgstr "abwesend" @@ -399,8 +332,40 @@ msgid "Next upcoming 5 events" msgstr "Nächste 5 anstehende Veranstaltungen" #: tutors/templates/tutors/dashboard.html -msgid "Start" -msgstr "Start" +#: tutors/templates/tutors/event/delete.html +#: tutors/templates/tutors/event/list.html +#: tutors/templates/tutors/event/view.html +#: tutors/templates/tutors/task/delete.html +#: tutors/templates/tutors/task/list.html +#: tutors/templates/tutors/task/view.html +#: tutors/templates/tutors/tutor/delete.html +#: tutors/templates/tutors/tutor/view.html +msgid "Name" +msgstr "Name" + +#: tutors/templates/tutors/dashboard.html +#: tutors/templates/tutors/event/delete.html +#: tutors/templates/tutors/event/list.html +#: tutors/templates/tutors/event/view.html +#: tutors/templates/tutors/task/delete.html +#: tutors/templates/tutors/task/list.html +#: tutors/templates/tutors/task/view.html +#: tutors/templates/tutors/tutor/delete.html +#: tutors/templates/tutors/tutor/view.html +msgid "Begin of the first meeting" +msgstr "Beginn des ersten meetings" + +#: tutors/templates/tutors/dashboard.html +#: tutors/templates/tutors/event/delete.html +#: tutors/templates/tutors/event/list.html +#: tutors/templates/tutors/event/view.html +#: tutors/templates/tutors/task/delete.html +#: tutors/templates/tutors/task/list.html +#: tutors/templates/tutors/task/view.html +#: tutors/templates/tutors/tutor/delete.html +#: tutors/templates/tutors/tutor/view.html +msgid "End of the last meeting" +msgstr "Ende des letzen meetings" #: tutors/templates/tutors/dashboard.html msgid "Next upcoming 5 tasks" @@ -436,6 +401,24 @@ msgstr "Event hinzufügen" msgid "Delete event" msgstr "Event löschen" +#: tutors/templates/tutors/event/delete.html +#: tutors/templates/tutors/event/view.html +#: tutors/templates/tutors/task/delete.html +#: tutors/templates/tutors/task/view.html +msgid "Description" +msgstr "Beschreibung" + +#: tutors/templates/tutors/event/delete.html +#: tutors/templates/tutors/event/list.html +#: tutors/templates/tutors/event/view.html +#: tutors/templates/tutors/task/delete.html +#: tutors/templates/tutors/task/list.html +#: tutors/templates/tutors/task/view.html +#: tutors/templates/tutors/tutor/delete.html +#: tutors/templates/tutors/tutor/view.html +msgid "Meeting Point" +msgstr "Treffpunkt" + #: tutors/templates/tutors/event/delete.html #: tutors/templates/tutors/event/view.html #: tutors/templates/tutors/task/delete.html @@ -882,6 +865,10 @@ msgstr "Ablehnen" msgid "Send email to tutor" msgstr "Sende Email an Tutoren" +#: tutors/templates/tutors/tutor/view.html +msgid "Download tutors ical" +msgstr "ical vom Tutor downloaden" + #: tutors/templates/tutors/tutor/view.html msgid "Assigned Tasks" msgstr "Zugewiesene Tasks" @@ -997,6 +984,18 @@ msgstr "" "Batch-Aktionen versenden keine elektronischen Fernbriefe (eMails). Sie " "verändern lediglich den Status der StudentInnen." +#~ msgid "Registration Time" +#~ msgstr "Zeitpunkt der Registrierung" + +#~ msgid "Start" +#~ msgstr "Start" + +#~ msgid "End" +#~ msgstr "Ende" + +#~ msgid "Begin" +#~ msgstr "Start" + #~ msgid "I accept the terms and conditions of the following privacy policy:" #~ msgstr "Ich stimme der folgenden Datenschutzerklärung zu:" diff --git a/tutors/migrations/0009_alter_event_id_alter_event_semester_and_more.py b/tutors/migrations/0009_alter_event_id_alter_event_semester_and_more.py new file mode 100644 index 00000000..1dcefbbf --- /dev/null +++ b/tutors/migrations/0009_alter_event_id_alter_event_semester_and_more.py @@ -0,0 +1,77 @@ +# Generated by Django 4.0.3 on 2022-04-01 16:07 + +import uuid + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("settool_common", "0017_anonymisationlog_created_at_and_more"), + ("tutors", "0008_auto_20220321_1851"), + ] + + operations = [ + migrations.AlterField( + model_name="event", + name="id", + field=models.UUIDField( + default=uuid.uuid4, + primary_key=True, + serialize=False, + unique=True, + ), + ), + migrations.AlterField( + model_name="event", + name="semester", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="settool_common.semester"), + ), + migrations.AlterField( + model_name="question", + name="id", + field=models.UUIDField( + default=uuid.uuid4, + primary_key=True, + serialize=False, + unique=True, + ), + ), + migrations.AlterField( + model_name="question", + name="semester", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="settool_common.semester"), + ), + migrations.AlterField( + model_name="task", + name="id", + field=models.UUIDField( + default=uuid.uuid4, + primary_key=True, + serialize=False, + unique=True, + ), + ), + migrations.AlterField( + model_name="task", + name="semester", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="settool_common.semester"), + ), + migrations.AlterField( + model_name="tutor", + name="id", + field=models.UUIDField( + default=uuid.uuid4, + primary_key=True, + serialize=False, + unique=True, + ), + ), + migrations.AlterField( + model_name="tutor", + name="semester", + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="settool_common.semester"), + ), + ] diff --git a/tutors/migrations/0010_alter_event_semester_alter_question_semester_and_more.py b/tutors/migrations/0010_alter_event_semester_alter_question_semester_and_more.py new file mode 100644 index 00000000..70d8c22b --- /dev/null +++ b/tutors/migrations/0010_alter_event_semester_alter_question_semester_and_more.py @@ -0,0 +1,60 @@ +# Generated by Django 4.0.3 on 2022-04-01 16:16 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("settool_common", "0018_alter_anonymisationlog_semester"), + ("tutors", "0009_alter_event_id_alter_event_semester_and_more"), + ] + + operations = [ + migrations.AlterField( + model_name="event", + name="semester", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="settool_common.semester", + verbose_name="Semester", + ), + ), + migrations.AlterField( + model_name="question", + name="semester", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="settool_common.semester", + verbose_name="Semester", + ), + ), + migrations.AlterField( + model_name="subjecttutorcountassignment", + name="semester", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="settool_common.semester", + verbose_name="Semester", + ), + ), + migrations.AlterField( + model_name="task", + name="semester", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="settool_common.semester", + verbose_name="Semester", + ), + ), + migrations.AlterField( + model_name="tutor", + name="semester", + field=models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to="settool_common.semester", + verbose_name="Semester", + ), + ), + ] diff --git a/tutors/migrations/0011_add_associated_meetings_and_more.py b/tutors/migrations/0011_add_associated_meetings_and_more.py new file mode 100644 index 00000000..8187c2b9 --- /dev/null +++ b/tutors/migrations/0011_add_associated_meetings_and_more.py @@ -0,0 +1,118 @@ +# Generated by Django 4.0.3 on 2022-03-23 00:34 + +import django.db.models.deletion +from django.db import migrations, models + + +def initialise_associated_meetings(apps, _): + DateGroup = apps.get_model("kalendar", "dategroup") + + Task = apps.get_model("tutors", "task") + Event = apps.get_model("tutors", "event") + for instance in list(Event.objects.all()) + list(Task.objects.all()): + date_group = instance.associated_meetings + if not date_group: + date_group = DateGroup.objects.create() + instance.associated_meetings = date_group + instance.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("kalendar", "0001_initial"), + ("tutors", "0010_alter_event_semester_alter_question_semester_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="task", + name="associated_meetings", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="kalendar.dategroup", + verbose_name="Associated Meetings", + ), + ), + migrations.AddField( + model_name="event", + name="associated_meetings", + field=models.OneToOneField( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + to="kalendar.dategroup", + verbose_name="Associated Meetings", + ), + ), + migrations.RunPython(initialise_associated_meetings), + migrations.AddField( + model_name="event", + name="event_leader", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tutors_event_event_leader", + to="tutors.tutor", + ), + ), + migrations.AddField( + model_name="event", + name="meeting_chairperson", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tutors_event_meeting_chairperson", + to="tutors.tutor", + ), + ), + migrations.AddField( + model_name="task", + name="meeting_chairperson", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tutors_task_meeting_chairperson", + to="tutors.tutor", + ), + ), + migrations.AddField( + model_name="task", + name="task_leader", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="tutors_task_task_leader", + to="tutors.tutor", + ), + ), + migrations.AlterField( + model_name="task", + name="name", + field=models.CharField(max_length=250, verbose_name="Name"), + ), + migrations.AlterField( + model_name="task", + name="name_de", + field=models.CharField(max_length=250, null=True, verbose_name="Name"), + ), + migrations.AlterField( + model_name="task", + name="name_en", + field=models.CharField(max_length=250, null=True, verbose_name="Name"), + ), + migrations.AlterField( + model_name="task", + name="tutors", + field=models.ManyToManyField( + blank=True, + related_name="tutors_task_tutors", + through="tutors.TutorAssignment", + to="tutors.tutor", + verbose_name="Assigned tutors", + ), + ), + ] diff --git a/tutors/migrations/0012_migrate_start_end_to_dategroup.py b/tutors/migrations/0012_migrate_start_end_to_dategroup.py new file mode 100644 index 00000000..089f111e --- /dev/null +++ b/tutors/migrations/0012_migrate_start_end_to_dategroup.py @@ -0,0 +1,32 @@ +# Generated by Django 4.0.3 on 2022-03-23 00:35 +from math import ceil + +from django.db import migrations + + +def migrate_start_end_to_dategroup(apps, _): + Date = apps.get_model("kalendar", "date") + + Task = apps.get_model("tutors", "task") + Event = apps.get_model("tutors", "event") + for instance in list(Event.objects.all()) + list(Task.objects.all()): + date_group = instance.associated_meetings + start = instance.begin + end = instance.end + time_length = ceil((end - start).seconds / 60) + Date.objects.create(group=date_group, date=start, probable_length=time_length) + + +class Migration(migrations.Migration): + dependencies = [ + ("kalendar", "0001_initial"), + ("tutors", "0011_add_associated_meetings_and_more"), + ] + + operations = [ + migrations.RunPython(migrate_start_end_to_dategroup), + migrations.RemoveField(model_name="event", name="begin"), + migrations.RemoveField(model_name="event", name="end"), + migrations.RemoveField(model_name="task", name="begin"), + migrations.RemoveField(model_name="task", name="end"), + ] diff --git a/tutors/migrations/0013_migrate_meeting_point_to_locaton.py b/tutors/migrations/0013_migrate_meeting_point_to_locaton.py new file mode 100644 index 00000000..0f03db98 --- /dev/null +++ b/tutors/migrations/0013_migrate_meeting_point_to_locaton.py @@ -0,0 +1,42 @@ +# Generated by Django 4.0.3 on 2022-03-24 15:33 + +from django.db import migrations + + +def migrate_meeting_point_to_locaton(apps, _): + Location = apps.get_model("kalendar", "Location") + + locations: dict[tuple[str, str, str], Location] = {} + + Task = apps.get_model("tutors", "task") + Event = apps.get_model("tutors", "event") + for instance in list(Event.objects.all()) + list(Task.objects.all()): + mp_trans = instance.meeting_point, instance.meeting_point_de, instance.meeting_point_en + date_group = instance.associated_meetings + if mp_trans not in locations: + (meeting_point, meeting_point_de, meeting_point_en) = mp_trans + location = Location.objects.create( + shortname=meeting_point, + shortname_de=meeting_point_de, + shortname_en=meeting_point_en, + ) + locations[mp_trans] = location + date_group.location = locations[mp_trans] + date_group.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("kalendar", "0001_initial"), + ("tutors", "0012_migrate_start_end_to_dategroup"), + ] + + operations = [ + migrations.RunPython(migrate_meeting_point_to_locaton), + migrations.RemoveField(model_name="event", name="meeting_point"), + migrations.RemoveField(model_name="event", name="meeting_point_de"), + migrations.RemoveField(model_name="event", name="meeting_point_en"), + migrations.RemoveField(model_name="task", name="meeting_point"), + migrations.RemoveField(model_name="task", name="meeting_point_de"), + migrations.RemoveField(model_name="task", name="meeting_point_en"), + ] diff --git a/tutors/migrations/0014_remove_tutor_registration_time.py b/tutors/migrations/0014_remove_tutor_registration_time.py new file mode 100644 index 00000000..49574d68 --- /dev/null +++ b/tutors/migrations/0014_remove_tutor_registration_time.py @@ -0,0 +1,14 @@ +# Generated by Django 4.0.3 on 2022-04-05 15:39 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("tutors", "0013_migrate_meeting_point_to_locaton"), + ] + + operations = [ + migrations.RemoveField(model_name="tutor", name="registration_time"), + ] diff --git a/tutors/models.py b/tutors/models.py index 38071e24..0c55619c 100644 --- a/tutors/models.py +++ b/tutors/models.py @@ -1,11 +1,12 @@ -import uuid - from dateutil.relativedelta import relativedelta from django.core.validators import RegexValidator from django.db import models +from django.db.models.signals import post_save +from django.dispatch import receiver from django.utils import timezone from django.utils.translation import gettext_lazy as _ +import kalendar.models import settool_common.models as common_models from settool_common.models import Semester, Subject @@ -58,15 +59,7 @@ def get_mail_task(self, tutor, task): return self.get_mail(context) -class BaseModel(models.Model): - created_at = models.DateTimeField(auto_now_add=True) - updated_at = models.DateTimeField(auto_now=True) - - class Meta: - abstract = True - - -class Settings(BaseModel): +class Settings(common_models.LoggedModelBase): semester = models.OneToOneField(Semester, on_delete=models.CASCADE) open_registration = models.DateTimeField(_("Open registration")) @@ -138,13 +131,10 @@ def log(self, user, text): pass -class Tutor(BaseModel): +class Tutor(common_models.UUIDModelBase, common_models.LoggedModelBase, common_models.SemesterModelBase): class Meta: unique_together = ("semester", "email") - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - semester = models.ForeignKey(Semester, verbose_name=_("Semester"), on_delete=models.CASCADE) - first_name = models.CharField(_("First name"), max_length=30) last_name = models.CharField(_("Last name"), max_length=50) email = models.EmailField(_("Email address")) @@ -179,7 +169,9 @@ class Meta: tshirt_size = models.CharField(_("Tshirt size"), max_length=5, choices=TSHIRT_SIZES) tshirt_girls_cut = models.BooleanField(_("Tshirt as Girls cut")) - registration_time = models.DateTimeField(_("Registration Time"), auto_now_add=True) + @property + def registration_time(self): + return self.created_at answers = models.ManyToManyField("Question", verbose_name=_("Tutor Answers"), through="Answer", blank=True) @@ -212,15 +204,22 @@ def log(self, user, text): pass -class Event(BaseModel): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - semester = models.ForeignKey(Semester, verbose_name=_("Semester"), on_delete=models.CASCADE) +class Event(kalendar.models.BaseDateGroupInstance): + meeting_chairperson = models.ForeignKey( + Tutor, + related_name="tutors_event_meeting_chairperson", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) - name = models.CharField(_("Name"), max_length=250) - description = models.TextField(_("Description"), blank=True) - begin = models.DateTimeField(_("Begin")) - end = models.DateTimeField(_("End")) - meeting_point = models.CharField(_("Meeting Point"), max_length=200) + event_leader = models.ForeignKey( + Tutor, + related_name="tutors_event_event_leader", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) subjects = models.ManyToManyField(Subject, verbose_name=_("Subjects"), blank=True) @@ -233,27 +232,40 @@ def log(self, user, text): pass def __str__(self) -> str: - return f"{self.name} {self.begin.date()}" + return self.name -class Task(BaseModel): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - semester = models.ForeignKey(Semester, verbose_name=_("Semester"), on_delete=models.CASCADE) +class Task(kalendar.models.BaseDateGroupInstance): + event = models.ForeignKey(Event, verbose_name=_("Event"), on_delete=models.CASCADE) + meeting_chairperson = models.ForeignKey( + Tutor, + related_name="tutors_task_meeting_chairperson", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) - name = models.CharField(_("Task name"), max_length=250) - description = models.TextField(_("Description"), blank=True) - begin = models.DateTimeField(_("Begin")) - end = models.DateTimeField(_("End")) - meeting_point = models.CharField(_("Meeting point"), max_length=50) + task_leader = models.ForeignKey( + Tutor, + related_name="tutors_task_task_leader", + null=True, + blank=True, + on_delete=models.SET_NULL, + ) - event = models.ForeignKey(Event, verbose_name=_("Event"), on_delete=models.CASCADE) allowed_subjects = models.ManyToManyField(Subject, verbose_name=_("Allowed Subjects"), blank=True) requirements = models.ManyToManyField("Question", verbose_name=_("Requirements"), blank=True) + + tutors = models.ManyToManyField( + Tutor, + related_name="tutors_task_tutors", + verbose_name=_("Assigned tutors"), + through="TutorAssignment", + blank=True, + ) min_tutors = models.IntegerField(_("Tutors (min)"), blank=True, null=True) max_tutors = models.IntegerField(_("Tutors (max)"), blank=True, null=True) - tutors = models.ManyToManyField(Tutor, verbose_name=_("Assigned tutors"), through="TutorAssignment", blank=True) - def __str__(self) -> str: return str(self.name) @@ -266,7 +278,25 @@ def log(self, user, text): pass -class TutorAssignment(BaseModel): +# pylint: disable=unused-argument +@receiver(post_save, sender=Event) +def create_event_meetings(sender, instance, created, **kwargs): + if not instance.associated_meetings: + instance.associated_meetings = kalendar.models.create_associated_meetings() + instance.save() + + +@receiver(post_save, sender=Task) +def create_task_meetings(sender, instance, created, **kwargs): + if not instance.associated_meetings: + instance.associated_meetings = kalendar.models.create_associated_meetings() + instance.save() + + +# pylint: enable=unused-argument + + +class TutorAssignment(common_models.LoggedModelBase): tutor = models.ForeignKey(Tutor, on_delete=models.CASCADE) task = models.ForeignKey(Task, on_delete=models.CASCADE) absent = models.BooleanField(_("absent"), default=False) @@ -275,10 +305,7 @@ def __str__(self) -> str: return f"{self.tutor}" -class Question(BaseModel): - id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False) - semester = models.ForeignKey(Semester, verbose_name=_("Semester"), on_delete=models.CASCADE) - +class Question(common_models.UUIDModelBase, common_models.LoggedModelBase, common_models.SemesterModelBase): question = models.CharField(_("Question"), max_length=100) def __str__(self) -> str: @@ -293,7 +320,7 @@ def log(self, user, text): pass -class Answer(BaseModel): +class Answer(common_models.LoggedModelBase): class Meta: unique_together = ("tutor", "question") @@ -314,7 +341,7 @@ def __str__(self) -> str: return f"{self.tutor}: {self.question} -> {self.answer}" -class MailTutorTask(BaseModel): +class MailTutorTask(common_models.LoggedModelBase): mail = models.ForeignKey(TutorMail, on_delete=models.CASCADE) tutor = models.ForeignKey(Tutor, on_delete=models.CASCADE) task = models.ForeignKey(Task, on_delete=models.CASCADE, blank=True, null=True) @@ -323,8 +350,7 @@ def __str__(self) -> str: return f"{self.created_at}: {self.tutor} -> {self.mail} - {self.task}" -class SubjectTutorCountAssignment(BaseModel): - semester = models.ForeignKey(Semester, on_delete=models.CASCADE) +class SubjectTutorCountAssignment(common_models.LoggedModelBase, common_models.SemesterModelBase): subject = models.ForeignKey(Subject, on_delete=models.CASCADE) wanted = models.PositiveIntegerField(default=0, null=True, blank=True) waitlist = models.PositiveIntegerField(default=0, null=True, blank=True) diff --git a/tutors/templates/tutors/dashboard.html b/tutors/templates/tutors/dashboard.html index 33f80890..2f70c609 100644 --- a/tutors/templates/tutors/dashboard.html +++ b/tutors/templates/tutors/dashboard.html @@ -27,17 +27,17 @@
    {% trans "Next upcoming 5 events" %}
    # {% trans "Name" %} - {% trans "Start" %} - {% trans "End" %} + {% trans "Begin of the first meeting" %} + {% trans "End of the last meeting" %} - {% for event in events %} + {% for event in first_future_five_events %} {{ forloop.counter }}
    {{ event.name }} - {{ event.begin }} - {{ event.end }} + {{ event.first_datetime }} + {{ event.last_datetime }} {% endfor %} @@ -55,17 +55,17 @@
    {% trans "Next upcoming 5 tasks" %}
    # {% trans "Name" %} - {% trans "Start" %} - {% trans "End" %} + {% trans "Begin of the first meeting" %} + {% trans "End of the last meeting" %} - {% for task in tasks %} + {% for task in first_future_five_tasks %} {{ forloop.counter }} {{ task.name }} - {{ task.begin }} - {{ task.end }} + {{ task.first_datetime }} + {{ task.last_datetime }} {% endfor %} diff --git a/tutors/templates/tutors/event/delete.html b/tutors/templates/tutors/event/delete.html index aa5f8e43..83a70276 100644 --- a/tutors/templates/tutors/event/delete.html +++ b/tutors/templates/tutors/event/delete.html @@ -12,12 +12,12 @@ {{ event.name }} - {% trans "Begin" %} - {{ event.begin }} + {% trans "Begin of the first meeting" %} + {{ event.first_datetime }} - {% trans "End" %} - {{ event.end }} + {% trans "End of the last meeting" %} + {{ event.last_datetime }} {% trans "Description" %} @@ -25,7 +25,7 @@ {% trans "Meeting Point" %} - {{ event.meeting_point }} + {{ event.meeting_point_str }} {% trans "Subjects" %} @@ -53,8 +53,8 @@

    {% trans "This event has the following tasks which will be deleted" %}:

    {% trans "Name" %} - {% trans "Begin" %} - {% trans "End" %} + {% trans "End of the last meeting" %} + {% trans "End of the last meeting" %} {% trans "Meeting Point" %} @@ -62,9 +62,9 @@

    {% trans "This event has the following tasks which will be deleted" %}:

    {% for task in event.task_set.all %} {{ task.name }} - {{ task.begin }} - {{ task.end }} - {{ task.meeting_point }} + {{ task.first_datetime }} + {{ task.last_datetime }} + {{ task.meeting_point_str }} {% endfor %} diff --git a/tutors/templates/tutors/event/list.html b/tutors/templates/tutors/event/list.html index 90a071df..6c3a3f4d 100644 --- a/tutors/templates/tutors/event/list.html +++ b/tutors/templates/tutors/event/list.html @@ -24,8 +24,8 @@ # {% trans "Name" %} - {% trans "Begin" %} - {% trans "End" %} + {% trans "Begin of the first meeting" %} + {% trans "End of the last meeting" %} {% trans "Meeting Point" %} {% trans "Actions" %} @@ -37,9 +37,9 @@ {{ event.name }} - {{ event.begin }} - {{ event.end }} - {{ event.meeting_point }} + {{ event.first_datetime }} + {{ event.last_datetime }} + {{ event.meeting_point_str }} diff --git a/tutors/templates/tutors/event/view.html b/tutors/templates/tutors/event/view.html index a4c21c08..16b14ce3 100644 --- a/tutors/templates/tutors/event/view.html +++ b/tutors/templates/tutors/event/view.html @@ -13,12 +13,12 @@ {{ event.name }} - {% trans "Begin" %} - {{ event.begin }} + {% trans "Begin of the first meeting" %} + {{ event.first_datetime }} - {% trans "End" %} - {{ event.end }} + {% trans "End of the last meeting" %} + {{ event.last_datetime }} {% trans "Description" %} @@ -26,7 +26,7 @@ {% trans "Meeting Point" %} - {{ event.meeting_point }} + {{ event.meeting_point_str }} {% trans "Subjects" %} @@ -72,8 +72,8 @@

    {% trans "This event has the following tasks" %}: # {% trans "Name" %} - {% trans "Begin" %} - {% trans "End" %} + {% trans "Begin of the first meeting" %} + {% trans "End of the last meeting" %} {% trans "Meeting Point" %} @@ -82,9 +82,9 @@

    {% trans "This event has the following tasks" %}: {{ forloop.counter }} {{ task.name }} - {{ task.begin }} - {{ task.end }} - {{ task.meeting_point }} + {{ task.first_datetime }} + {{ task.last_datetime }} + {{ task.meeting_point_str }} {% endfor %} diff --git a/tutors/templates/tutors/task/delete.html b/tutors/templates/tutors/task/delete.html index 3a50fff4..ee81bf4f 100644 --- a/tutors/templates/tutors/task/delete.html +++ b/tutors/templates/tutors/task/delete.html @@ -12,12 +12,12 @@ {{ task.name }} - {% trans "Begin" %} - {{ task.begin }} + {% trans "Begin of the first meeting" %} + {{ task.first_datetime }} - {% trans "End" %} - {{ task.end }} + {% trans "End of the last meeting" %} + {{ task.last_datetime }} {% trans "Description" %} @@ -25,7 +25,7 @@ {% trans "Meeting Point" %} - {{ task.meeting_point }} + {{ task.meeting_point_str }} {% trans "Event" %} diff --git a/tutors/templates/tutors/task/list.html b/tutors/templates/tutors/task/list.html index 13de7b00..20da6afa 100644 --- a/tutors/templates/tutors/task/list.html +++ b/tutors/templates/tutors/task/list.html @@ -26,8 +26,8 @@ # {% trans "Event" %} {% trans "Name" %} - {% trans "Begin" %} - {% trans "End" %} + {% trans "Begin of the first meeting" %} + {% trans "End of the last meeting" %} {% trans "Meeting Point" %} {% trans "# Missing Mails" %} {% trans "# Tutors" %} @@ -44,9 +44,9 @@ {{ task.name }} - {{ task.begin }} - {{ task.end }} - {{ task.meeting_point }} + {{ task.first_datetime }} + {{ task.last_datetime }} + {{ task.meeting_point_str }} {% mail_task_count task %} {{ task.tutors.count }}/{{ task.min_tutors|default_if_none:0 }}-{{ task.max_tutors|default_if_none:0 }} diff --git a/tutors/templates/tutors/task/view.html b/tutors/templates/tutors/task/view.html index b7002b5c..67b6ed1f 100644 --- a/tutors/templates/tutors/task/view.html +++ b/tutors/templates/tutors/task/view.html @@ -13,12 +13,12 @@ {{ task.name }} - {% trans "Begin" %} - {{ task.begin }} + {% trans "Begin of the first meeting" %} + {{ task.first_datetime }} - {% trans "End" %} - {{ task.end }} + {% trans "End of the last meeting" %} + {{ task.last_datetime }} {% trans "Description" %} @@ -26,7 +26,7 @@ {% trans "Meeting Point" %} - {{ task.meeting_point }} + {{ task.meeting_point_str }} {% trans "Event" %} diff --git a/tutors/templates/tutors/tutor/delete.html b/tutors/templates/tutors/tutor/delete.html index 51b05607..1b659358 100644 --- a/tutors/templates/tutors/tutor/delete.html +++ b/tutors/templates/tutors/tutor/delete.html @@ -64,8 +64,8 @@

    {% trans "The tutor is assigned to the following tasks" %}:

    # {% trans "Name" %} - {% trans "Begin" %} - {% trans "End" %} + {% trans "Begin of the first meeting" %} + {% trans "End of the last meeting" %} {% trans "Meeting Point" %} @@ -74,9 +74,9 @@

    {% trans "The tutor is assigned to the following tasks" %}:

    {{ forloop.counter }} {{ assignment.task.name }} - {{ assignment.task.begin }} - {{ assignment.task.end }} - {{ assignment.task.meeting_point }} + {{ assignment.task.first_datetime }} + {{ assignment.task.last_datetime }} + {{ assignment.task.meeting_point_str }} {% endfor %} diff --git a/tutors/templates/tutors/tutor/view.html b/tutors/templates/tutors/tutor/view.html index e47687c8..a59f8b7d 100644 --- a/tutors/templates/tutors/tutor/view.html +++ b/tutors/templates/tutors/tutor/view.html @@ -123,6 +123,10 @@ class="btn btn-warning" href="{% url "tutors:mail_tutor" tutor.id %}" >{% trans "Send email to tutor" %} + {% trans "Download tutors ical" %} {% trans "Assigned Tasks" %}: # {% trans "Name" %} - {% trans "Begin" %} - {% trans "End" %} + {% trans "Begin of the first meeting" %} + {% trans "End of the last meeting" %} {% trans "Meeting Point" %} @@ -171,9 +175,9 @@

    {% trans "Assigned Tasks" %}:

    {{ forloop.counter }}
    {{ assignment.task.name }} - {{ assignment.task.begin }} - {{ assignment.task.end }} - {{ assignment.task.meeting_point }} + {{ assignment.task.first_datetime }} + {{ assignment.task.last_datetime }} + {{ assignment.task.meeting_point_str }} {% endfor %} diff --git a/tutors/tests/test_file_export.py b/tutors/tests/test_file_export.py index 8ca48fd1..c56ac8cc 100644 --- a/tutors/tests/test_file_export.py +++ b/tutors/tests/test_file_export.py @@ -174,7 +174,7 @@ class TaskExportTest(django.test.TestCase): fixtures = ["Tutors.json"] def setUp(self): - self.client = self.client = get_mocked_logged_in_client() + self.client = get_mocked_logged_in_client() def test_pdf_export_task_no_data(self): self.task_pdf_generation("3cd2b4b0-c36f-4348-8a93-b3bb72029f46") diff --git a/tutors/tokens.py b/tutors/tokens.py index 96d8f4ba..844f27c0 100644 --- a/tutors/tokens.py +++ b/tutors/tokens.py @@ -2,8 +2,7 @@ class TokenGenerator(PasswordResetTokenGenerator): - @staticmethod - def _make_hash_value(user, timestamp): + def _make_hash_value(self, user, timestamp): return f"{user.pk}{timestamp}" diff --git a/tutors/translation.py b/tutors/translation.py index 4a167580..5b06f83a 100644 --- a/tutors/translation.py +++ b/tutors/translation.py @@ -4,12 +4,12 @@ class EventTranslationOptions(TranslationOptions): - fields = ("name", "description", "meeting_point") + fields = ("name", "description") required_languages = ("en", "de") class TaskTranslationOptions(TranslationOptions): - fields = ("name", "description", "meeting_point") + fields = ("name", "description") required_languages = ("en", "de") diff --git a/tutors/views.py b/tutors/views.py index 352c8100..7bc67d6b 100644 --- a/tutors/views.py +++ b/tutors/views.py @@ -7,7 +7,7 @@ from django.contrib.auth.decorators import permission_required from django.core.handlers.wsgi import WSGIRequest from django.db import IntegrityError -from django.db.models import Count, Q, QuerySet +from django.db.models import Count, QuerySet from django.forms import forms, modelformset_factory from django.http import Http404, HttpResponse, HttpResponseRedirect from django.shortcuts import get_object_or_404, redirect, render @@ -21,6 +21,7 @@ from django_tex.response import PDFResponse from django_tex.shortcuts import render_to_pdf +from kalendar.models import BaseDateGroupInstance, Date from settool_common import utils from settool_common.models import get_semester, Semester, Subject from tutors.forms import ( @@ -254,7 +255,7 @@ def list_participants(request: WSGIRequest, status: str) -> HttpResponse: request, "tutors/tutor/list.html", { - "tutors": tutors.order_by("registration_time"), + "tutors": tutors.order_by("created_at"), "status": status, "questions": Question.objects.filter(semester=semester), }, @@ -298,7 +299,7 @@ def del_tutor(request: WSGIRequest, uid: UUID) -> HttpResponse: @permission_required("tutors.edit_tutors") def edit_tutor(request: WSGIRequest, uid: UUID) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) - tutor = get_object_or_404(Tutor, pk=uid) + tutor: Tutor = get_object_or_404(Tutor, pk=uid) question_count = Question.objects.filter(semester=semester).count() answers_existing = Answer.objects.filter(tutor=tutor) @@ -333,9 +334,12 @@ def edit_tutor(request: WSGIRequest, uid: UUID) -> HttpResponse: form.save() answer: AnswerForm for answer in answer_formset: - res = answer.save(commit=False) + res: Answer = answer.save(commit=False) res.tutor_id = tutor.id - res.question_id = answer.cleaned_data.get("question").id + res_question: Optional[Question] = answer.cleaned_data.get("question") + if res_question is None: + raise ValueError("Question should never be None") + res.question_id = res_question.id res.save() tutor.log(request.user, "Tutor edited") messages.success(request, f"Saved Tutor {tutor}.") @@ -378,7 +382,7 @@ def edit_event(request: WSGIRequest, uid: UUID) -> HttpResponse: @permission_required("tutors.edit_tutors") def list_event(request: WSGIRequest) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) - events = Event.objects.filter(semester=semester).order_by("begin") + events = Event.sorted_by_semester(semester.id) return render(request, "tutors/event/list.html", {"events": events}) @@ -447,7 +451,7 @@ def edit_task(request: WSGIRequest, uid: UUID) -> HttpResponse: @permission_required("tutors.edit_tutors") def list_task(request: WSGIRequest) -> HttpResponse: - tasks = Task.objects.filter(semester=get_semester(request)).order_by("begin") + tasks = Task.sorted_by_semester(get_semester(request)) return render(request, "tutors/task/list.html", {"tasks": tasks}) @@ -501,11 +505,16 @@ def view_task(request: WSGIRequest, uid: UUID) -> HttpResponse: messages.success(request, f"Saved Task Assignment {task.name}.") assigned_tutors = task.tutors.all().order_by("last_name") - parallel_task_tutors = Tutor.objects.filter( - Q(task__begin__gte=task.begin) | Q(task__end__lte=task.end), - Q(task__end__gt=task.begin), - Q(task__begin__lt=task.end), - ) + + paralel_dates: set[Date] = set() + for test_date in Date.objects.all(): + if not task.associated_meetings: + raise IntegrityError(f"task {task.id} has no associated_meetings") + for orig_date in task.associated_meetings.date_set.all(): + if not orig_date.intersects(test_date): + paralel_dates.add(test_date) + paralel_tasks = Task.objects.filter(associated_meetings__date__in=paralel_dates) + parallel_task_tutors = Tutor.objects.filter(tutorassignment__task__in=paralel_tasks) unassigned_tutors = ( Tutor.objects.filter(semester=semester, status=Tutor.STATUS_ACCEPTED) .exclude(id__in=assigned_tutors.values("id")) @@ -977,7 +986,7 @@ def batch_accept(request: WSGIRequest) -> HttpResponse: if wanted > accepted_count: need: int = wanted - accepted_count tutor: Tutor - for tutor in active_tutors.order_by("registration_time")[:need]: + for tutor in active_tutors.order_by("created_at")[:need]: tutor_ids.append(tutor.id) if subject not in to_be_accepted: @@ -1023,7 +1032,7 @@ def batch_decline(request: WSGIRequest) -> HttpResponse: keep: int = max(wanted - accepted_count + waitlist, 0) - for tutor in active_tutors.order_by("registration_time")[keep:]: + for tutor in active_tutors.order_by("created_at")[keep:]: tutor_ids.append(tutor.id) if subject not in to_be_declined: @@ -1079,6 +1088,18 @@ def _gather_batch_parameters( return semester, tutors_active, tutors_accepted_cnt, assignment_wishes, errors +def get_first_future_five(cls: type[BaseDateGroupInstance], semester_id: int) -> list[type[BaseDateGroupInstance]]: + sorted_instances: list[type[BaseDateGroupInstance]] = cls.sorted_by_semester(semester_id) + + future_instances = [] + for inst in sorted_instances: + if inst.last_datetime and inst.last_datetime < timezone.now(): # type:ignore + future_instances.append(inst) + if len(future_instances) == 5: + break + return future_instances + + @permission_required("tutors.edit_tutors") def dashboard(request: WSGIRequest) -> HttpResponse: semester: Semester = get_object_or_404(Semester, pk=get_semester(request)) @@ -1097,21 +1118,14 @@ def dashboard(request: WSGIRequest) -> HttpResponse: assignment_wish_counter.wanted, ) - events = ( - Event.objects.filter(semester=semester) - .filter(Q(begin__gt=timezone.now()) | Q(end__gt=timezone.now())) - .order_by("begin")[:5] - ) - tasks = ( - Task.objects.filter(semester=semester) - .filter(Q(begin__gt=timezone.now()) | Q(end__gt=timezone.now())) - .order_by("begin")[:5] - ) + first_future_five_events: list[type[BaseDateGroupInstance]] = get_first_future_five(Event, semester.id) + first_future_five_tasks: list[type[BaseDateGroupInstance]] = get_first_future_five(Task, semester.id) missing_mails = 0 + task: Task for task in Task.objects.filter(semester=semester): missing_mails += ( - Tutor.objects.filter(task=task) + Tutor.objects.filter(tutorassignment__task=task) .exclude(id__in=MailTutorTask.objects.filter(task=task).values("tutor_id")) .count() ) @@ -1123,8 +1137,8 @@ def dashboard(request: WSGIRequest) -> HttpResponse: "tutors/dashboard.html", { "subject_counts": count_results, - "events": events, - "tasks": tasks, + "first_future_five_events": first_future_five_events, + "first_future_five_tasks": first_future_five_tasks, "missing_mails": missing_mails, "accepted_tutors": accepted_tutors, "waiting_tutors": waiting_tutors,