From 7bec9db10f6c1b06ac6b531886a589c38b10d24e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Valenta?= <git@imaniti.org>
Date: Wed, 31 May 2023 18:43:21 +0200
Subject: [PATCH] custom required levels for each defined group, calendar
 views, colors

---
 lectures/admin.py                             | 11 ++-
 ...023_alter_lecturelector_avatar_and_more.py | 28 ++++--
 ...ure_groups_remove_lecture_type_and_more.py | 67 ++++++++++++++
 ...5_alter_lecturegrouptype_group_and_more.py | 33 +++++++
 lectures/models.py                            | 89 ++++++++++++-------
 .../templates/lectures/includes/lecture.html  | 10 ++-
 .../lectures/includes/lecture_type_color.html |  5 ++
 .../lectures/view_group_lectures.html         | 14 ++-
 lectures/templates/lectures/view_lecture.html | 22 +++--
 lectures/templatetags/lecture_group_types.py  |  8 ++
 lectures/views.py                             | 49 +++++-----
 shared/templates/shared/includes/base.html    |  6 +-
 12 files changed, 269 insertions(+), 73 deletions(-)
 create mode 100644 lectures/migrations/0024_remove_lecture_groups_remove_lecture_type_and_more.py
 create mode 100644 lectures/migrations/0025_alter_lecturegrouptype_group_and_more.py
 create mode 100644 lectures/templates/lectures/includes/lecture_type_color.html
 create mode 100644 lectures/templatetags/lecture_group_types.py

diff --git a/lectures/admin.py b/lectures/admin.py
index ff04fa8..faa8401 100644
--- a/lectures/admin.py
+++ b/lectures/admin.py
@@ -5,6 +5,7 @@ from shared.admin import MarkdownxGuardedModelAdmin
 from .models import (
     Lecture,
     LectureGroup,
+    LectureGroupType,
     LectureLector,
     LectureMaterial,
     LectureRecording,
@@ -35,13 +36,20 @@ class LectureMaterialInline(admin.StackedInline):
     extra = 1
 
 
+class LectureGroupTypeInline(admin.StackedInline):
+    model = LectureGroupType
+    autocomplete_fields = ("group",)
+    extra = 1
+
+
 class LectureAdmin(MarkdownxGuardedModelAdmin):
     inlines = (
+        LectureGroupTypeInline,
         LectureRecordingInline,
         LectureMaterialInline,
     )
 
-    autocomplete_fields = ("groups", "lectors", "rsvp_users")
+    autocomplete_fields = ("lectors", "rsvp_users")
     search_fields = ("name", "description")
     readonly_fields = ("rsvp_users",)
 
@@ -58,6 +66,7 @@ class LectureLectorAdmin(MarkdownxGuardedModelAdmin):
 for model in (
     LectureMaterial,
     LectureRecording,
+    LectureGroupType,
 ):
     admin.site.register(model, IndexHiddenModelAdmin)
 
diff --git a/lectures/migrations/0023_alter_lecturelector_avatar_and_more.py b/lectures/migrations/0023_alter_lecturelector_avatar_and_more.py
index 9b43512..c50b8d6 100644
--- a/lectures/migrations/0023_alter_lecturelector_avatar_and_more.py
+++ b/lectures/migrations/0023_alter_lecturelector_avatar_and_more.py
@@ -1,24 +1,36 @@
 # Generated by Django 4.1.4 on 2023-05-31 13:01
 
 from django.db import migrations, models
+
 import lectures.models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('lectures', '0022_lecturelector_avatar_lecturelector_description_and_more'),
+        ("lectures", "0022_lecturelector_avatar_lecturelector_description_and_more"),
     ]
 
     operations = [
         migrations.AlterField(
-            model_name='lecturelector',
-            name='avatar',
-            field=models.ImageField(blank=True, help_text='Vložený soubor má prioritu nad obrázkem synchronizovaným z Chobotnice.', null=True, upload_to=lectures.models.get_file_location, verbose_name='Profilový obrázek'),
+            model_name="lecturelector",
+            name="avatar",
+            field=models.ImageField(
+                blank=True,
+                help_text="Vložený soubor má prioritu nad obrázkem synchronizovaným z Chobotnice.",
+                null=True,
+                upload_to=lectures.models.get_file_location,
+                verbose_name="Profilový obrázek",
+            ),
         ),
         migrations.AlterField(
-            model_name='lecturelector',
-            name='username',
-            field=models.CharField(blank=True, help_text='Např. na fóru, nebo v Chobotnici. Užívá se k synchronizaci profilového obrázku, v budoucnu i dalších informací.', max_length=128, null=True, verbose_name='Uživatelské jméno'),
+            model_name="lecturelector",
+            name="username",
+            field=models.CharField(
+                blank=True,
+                help_text="Např. na fóru, nebo v Chobotnici. Užívá se k synchronizaci profilového obrázku, v budoucnu i dalších informací.",
+                max_length=128,
+                null=True,
+                verbose_name="Uživatelské jméno",
+            ),
         ),
     ]
diff --git a/lectures/migrations/0024_remove_lecture_groups_remove_lecture_type_and_more.py b/lectures/migrations/0024_remove_lecture_groups_remove_lecture_type_and_more.py
new file mode 100644
index 0000000..fd81a12
--- /dev/null
+++ b/lectures/migrations/0024_remove_lecture_groups_remove_lecture_type_and_more.py
@@ -0,0 +1,67 @@
+# Generated by Django 4.1.4 on 2023-05-31 15:04
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("lectures", "0023_alter_lecturelector_avatar_and_more"),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name="lecture",
+            name="groups",
+        ),
+        migrations.RemoveField(
+            model_name="lecture",
+            name="type",
+        ),
+        migrations.CreateModel(
+            name="LectureGroupType",
+            fields=[
+                (
+                    "id",
+                    models.BigAutoField(
+                        auto_created=True,
+                        primary_key=True,
+                        serialize=False,
+                        verbose_name="ID",
+                    ),
+                ),
+                (
+                    "type",
+                    models.CharField(
+                        choices=[
+                            ("required", "Silně doporučené školení"),
+                            ("recommended", "Doporučené školení"),
+                            ("voluntary", "Dobrovolné školení"),
+                        ],
+                        max_length=11,
+                        verbose_name="PoĹľadovanost",
+                    ),
+                ),
+                (
+                    "group",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="lectures.lecturegroup",
+                        verbose_name="Výuková skupina",
+                    ),
+                ),
+                (
+                    "lecture",
+                    models.ForeignKey(
+                        on_delete=django.db.models.deletion.CASCADE,
+                        to="lectures.lecture",
+                        verbose_name="Ĺ kolenĂ­",
+                    ),
+                ),
+            ],
+            options={
+                "verbose_name": "Úroveň požadovanosti pro skupinu",
+                "verbose_name_plural": "Úroveň požadovanosti pro skupiny",
+            },
+        ),
+    ]
diff --git a/lectures/migrations/0025_alter_lecturegrouptype_group_and_more.py b/lectures/migrations/0025_alter_lecturegrouptype_group_and_more.py
new file mode 100644
index 0000000..c66d099
--- /dev/null
+++ b/lectures/migrations/0025_alter_lecturegrouptype_group_and_more.py
@@ -0,0 +1,33 @@
+# Generated by Django 4.1.4 on 2023-05-31 15:36
+
+import django.db.models.deletion
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("lectures", "0024_remove_lecture_groups_remove_lecture_type_and_more"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="lecturegrouptype",
+            name="group",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="lecture_group_types",
+                to="lectures.lecturegroup",
+                verbose_name="Výuková skupina",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="lecturegrouptype",
+            name="lecture",
+            field=models.ForeignKey(
+                on_delete=django.db.models.deletion.CASCADE,
+                related_name="lecture_group_types",
+                to="lectures.lecture",
+                verbose_name="Ĺ kolenĂ­",
+            ),
+        ),
+    ]
diff --git a/lectures/models.py b/lectures/models.py
index 687a2fd..72b0cf7 100644
--- a/lectures/models.py
+++ b/lectures/models.py
@@ -56,10 +56,10 @@ class LectureGroup(NameStrMixin, models.Model):
         blank=True,
         verbose_name="Popis",
         help_text=mark_safe(
-            'Můžeš použít <a href="'
-            "https://cs.wikipedia.org/wiki/Markdown"
-            '#P%C5%99%C3%ADklad_u%C5%BEit%C3%AD"'
-            ">Markdown</a>."
+            r'Můžeš použít <a href="'
+            r"https://cs.wikipedia.org/wiki/Markdown"
+            r'#P%C5%99%C3%ADklad_u%C5%BEit%C3%AD"'
+            r">Markdown</a>."
         ),
     )
 
@@ -81,6 +81,36 @@ class LectureGroup(NameStrMixin, models.Model):
         ordering = ("priority", "name")
 
 
+class LectureGroupType(models.Model):
+    lecture = models.ForeignKey(
+        "Lecture",
+        on_delete=models.CASCADE,
+        related_name="lecture_group_types",
+        verbose_name="Ĺ kolenĂ­",
+    )
+    group = models.ForeignKey(
+        "LectureGroup",
+        on_delete=models.CASCADE,
+        related_name="lecture_group_types",
+        verbose_name="Výuková skupina",
+    )
+
+    class TypeChoices(models.TextChoices):
+        REQUIRED = "required", "Silně doporučené školení"
+        RECOMMENDED = "recommended", "Doporučené školení"
+        VOLUNTARY = "voluntary", "Dobrovolné školení"
+
+    type = models.CharField(
+        choices=TypeChoices.choices,
+        max_length=11,
+        verbose_name="PoĹľadovanost",
+    )
+
+    class Meta:
+        verbose_name = "Úroveň požadovanosti pro skupinu"
+        verbose_name_plural = "Úroveň požadovanosti pro skupiny"
+
+
 class Lecture(NameStrMixin, models.Model):
     is_current_starting_treshold = timedelta(hours=8)
     is_current_ending_treshold = timedelta(days=60)
@@ -91,24 +121,6 @@ class Lecture(NameStrMixin, models.Model):
         null=True,
     )  # If undefined, assume this event was in the past
 
-    class TypeChoices(models.TextChoices):
-        RECOMMENDED = "recommended", "Doporučené školení"
-        OPTIONAL = "optional", "Volitelné školení"
-
-    groups = models.ManyToManyField(
-        LectureGroup,
-        blank=True,
-        related_name="lectures",
-        verbose_name="Výukové skupiny",
-        help_text="Pokud nevybereš žádné skupiny, školení je dostupné všem.",
-    )
-
-    type = models.CharField(
-        choices=TypeChoices.choices,
-        max_length=11,
-        verbose_name="Typ",
-    )
-
     name = models.CharField(
         max_length=128,
         verbose_name="Název",
@@ -119,10 +131,10 @@ class Lecture(NameStrMixin, models.Model):
         null=True,
         verbose_name="Popis",
         help_text=mark_safe(
-            'Můžeš použít <a href="'
-            "https://cs.wikipedia.org/wiki/Markdown"
-            '#P%C5%99%C3%ADklad_u%C5%BEit%C3%AD"'
-            ">Markdown</a>."
+            r'Můžeš použít <a href="'
+            r"https://cs.wikipedia.org/wiki/Markdown"
+            r'#P%C5%99%C3%ADklad_u%C5%BEit%C3%AD"'
+            r">Markdown</a>."
         ),
     )
 
@@ -141,6 +153,23 @@ class Lecture(NameStrMixin, models.Model):
 
     settings = app_settings.LectureSettings("NastavenĂ­")
 
+    def get_current_group_type(self, request, group=None):
+        if group is not None:
+            return LectureGroupType.objects.filter(lecture=self, group=group).first()
+        else:
+            return LectureGroupType.objects.filter(
+                lecture=self,
+                group__in=(
+                    LectureGroup.filter(
+                        models.Q(user_groups__in=request.user.groups.all())
+                        if not request.user.is_superuser
+                        else models.Q(id__isnull=False)  # Always True
+                    )
+                    .distinct()
+                    .all()
+                ),
+            ).first()
+
     class Meta:
         verbose_name = "Ĺ kolenĂ­"
         verbose_name_plural = verbose_name
@@ -159,10 +188,10 @@ class LectureLector(NameStrMixin, models.Model):
         null=True,
         verbose_name="Popis",
         help_text=mark_safe(
-            'Můžeš použít <a href="'
-            "https://cs.wikipedia.org/wiki/Markdown"
-            '#P%C5%99%C3%ADklad_u%C5%BEit%C3%AD"'
-            ">Markdown</a>."
+            r'Můžeš použít <a href="'
+            r"https://cs.wikipedia.org/wiki/Markdown"
+            r'#P%C5%99%C3%ADklad_u%C5%BEit%C3%AD"'
+            r">Markdown</a>."
         ),
     )
 
diff --git a/lectures/templates/lectures/includes/lecture.html b/lectures/templates/lectures/includes/lecture.html
index feda986..eb8469b 100644
--- a/lectures/templates/lectures/includes/lecture.html
+++ b/lectures/templates/lectures/includes/lecture.html
@@ -1,4 +1,4 @@
-{% load markdownify %}
+{% load markdownify lecture_group_types %}
 
 <li>
     <a
@@ -8,7 +8,13 @@
         <div class="card elevation-6">
             <div class="card__body p-5 hover:bg-gray-100 duration-100">
                 <h2 class="head-alt-sm mb-4">
-                    <i class="ico--bookmark mr-2"></i>
+                    <i
+                        class="
+                            ico--bookmark mr-2
+                            {% get_lecture_type lecture request group as type %}
+                            {% include "lectures/includes/lecture_type_color.html" %}
+                        "
+                    ></i>
                     {{ lecture.name }}
                 </h2>
 
diff --git a/lectures/templates/lectures/includes/lecture_type_color.html b/lectures/templates/lectures/includes/lecture_type_color.html
new file mode 100644
index 0000000..527b73a
--- /dev/null
+++ b/lectures/templates/lectures/includes/lecture_type_color.html
@@ -0,0 +1,5 @@
+{% if type.type == type.TypeChoices.REQUIRED.value %}
+    text-red-700
+{% elif type.type == type.TypeChoices.RECOMMENDED.value %}
+    text-amber-500
+{% endif %}
diff --git a/lectures/templates/lectures/view_group_lectures.html b/lectures/templates/lectures/view_group_lectures.html
index 2d835f3..9c32e83 100644
--- a/lectures/templates/lectures/view_group_lectures.html
+++ b/lectures/templates/lectures/view_group_lectures.html
@@ -1,6 +1,6 @@
 {% extends "shared/includes/base.html" %}
 {% load render_bundle from webpack_loader %}
-{% load markdownify static %}
+{% load markdownify static lecture_group_types %}
 
 {% block content %}
     {% render_bundle "view_group_lectures" %}
@@ -90,7 +90,17 @@
                                                     {% for lecture in lectures %}
                                                         <li>
                                                             <a
-                                                                class="inline-block px-2.5 py-1.5 bg-gray-200 rounded-sm duration-100 hover:no-underline hover:bg-gray-300"
+                                                                class="
+                                                                    inline-block px-2.5 py-1.5 rounded-sm duration-100 hover:no-underline
+                                                                    {% get_lecture_type lecture request group as type %}
+                                                                    {% if type.type == type.TypeChoices.REQUIRED.value %}
+                                                                        bg-red-700 hover:bg-red-800 text-white
+                                                                    {% elif type.type == type.TypeChoices.RECOMMENDED.value %}
+                                                                        bg-amber-500 hover:bg-amber-600
+                                                                    {% else %}
+                                                                        bg-gray-200 hover:bg-gray-300
+                                                                    {% endif %}
+                                                                "
                                                                 href="{% url "lectures:view_lecture" lecture.id %}?related_group_id={{ group.id }}"
                                                             >{{ lecture.name }}</a>
                                                         </li>
diff --git a/lectures/templates/lectures/view_lecture.html b/lectures/templates/lectures/view_lecture.html
index f36e41d..932a2f0 100644
--- a/lectures/templates/lectures/view_lecture.html
+++ b/lectures/templates/lectures/view_lecture.html
@@ -1,6 +1,6 @@
 {% extends "shared/includes/base.html" %}
 {% load render_bundle from webpack_loader %}
-{% load markdownify rsvp %}
+{% load markdownify rsvp lecture_group_types %}
 
 {% block content %}
     {% render_bundle "view_lecture" %}
@@ -15,7 +15,19 @@
     {% endif %}
 
     <div class="flex justify-between items-start gap-3 flex-col md:flex-row">
-        {% include "shared/includes/double_heading.html" with heading=lecture.name subheading=lecture.get_type_display icon="ico--bookmark" %}
+        <div class="flex gap-4 mb-10">
+            {% get_lecture_type lecture request group as type %}
+            <i
+                class="
+                    ico--bookmark text-6xl md:text-7xl
+                    {% include "lectures/includes/lecture_type_color.html" %}
+                "
+            ></i>
+            <div>
+                <h1 class="head-alt-md md:head-alt-lg">{{ lecture.name }}</h1>
+                <h2 class="head-alt-sm">{{ type.get_type_display }}</h2>
+            </div>
+        </div>
 
         {% if user.is_authenticated %}
             <button
@@ -56,12 +68,12 @@
                 <span>Skupiny</span>
             </div>
             <ul class="flex gap-2">
-                {% for group in lecture.groups.all %}
+                {% for group_type in lecture.lecture_group_types.all %}
                     <li>
                         <a
                             class="px-1.5 py-1 bg-gray-200 rounded-sm duration-150 hover:no-underline hover:bg-gray-300"
-                            href="{% url "lectures:view_group_lectures" group.id %}"
-                        >{{ group.name }}</a>
+                            href="{% url "lectures:view_group_lectures" group_type.group.id %}"
+                        >{{ group_type.group.name }}</a>
                     </li>
                 {% endfor %}
             </ul>
diff --git a/lectures/templatetags/lecture_group_types.py b/lectures/templatetags/lecture_group_types.py
new file mode 100644
index 0000000..aa2f69b
--- /dev/null
+++ b/lectures/templatetags/lecture_group_types.py
@@ -0,0 +1,8 @@
+from django import template
+
+register = template.Library()
+
+
+@register.simple_tag
+def get_lecture_type(lecture, request, group=None):
+    return lecture.get_current_group_type(request, group)
diff --git a/lectures/views.py b/lectures/views.py
index 1624c92..db438c0 100644
--- a/lectures/views.py
+++ b/lectures/views.py
@@ -32,10 +32,7 @@ class LectureMaterialFileDownloadView(ObjectDownloadView):
                 lecture__groups__in=(
                     get_objects_for_user(
                         self.current_user, "lectures.view_lecturegroup"
-                    ).filter(
-                        models.Q(user_groups__in=self.current_user.groups.all())
-                        | models.Q(user_groups=None)
-                    )
+                    ).filter(models.Q(user_groups__in=self.current_user.groups.all()))
                 )
             )
         )
@@ -74,12 +71,15 @@ def get_lectures(request, filter=None, get_exceptions: bool = True) -> tuple:
     if not (
         get_objects_for_user(request.user, "lectures.view_lecturegroup")
         .filter(
-            models.Q(id__in=(LectureGroup.objects.filter(lectures__in=lectures)))
-            & (
-                (
-                    models.Q(user_groups__in=request.user.groups.all())
-                    | models.Q(user_groups=None)
+            models.Q(
+                id__in=(
+                    LectureGroup.objects.filter(
+                        lecture_group_types__lecture__in=lectures
+                    )
                 )
+            )
+            & (
+                models.Q(user_groups__in=request.user.groups.all())
                 if not request.user.is_superuser
                 else models.Q(id__isnull=False)  # Always True
             )
@@ -103,10 +103,7 @@ def view_groups(request):
     lecture_groups = (
         get_objects_for_user(request.user, "lectures.view_lecturegroup")
         .filter(
-            (
-                models.Q(user_groups__in=request.user.groups.all())
-                | models.Q(user_groups=None)
-            )
+            models.Q(user_groups__in=request.user.groups.all())
             if not request.user.is_superuser
             else models.Q(id__isnull=False)  # Always True
         )
@@ -136,10 +133,7 @@ def view_group_lectures(request, group_id: int):
     group_id_exists = group.exists()
 
     if not request.user.is_superuser:
-        group = group.filter(
-            models.Q(user_groups__in=request.user.groups.all())
-            | models.Q(user_groups=None)
-        )
+        group = group.filter(models.Q(user_groups__in=request.user.groups.all()))
 
     if not group.exists():
         if not group_id_exists:  # Doesn't exist at all
@@ -158,7 +152,7 @@ def view_group_lectures(request, group_id: int):
     past_lectures = (
         get_objects_for_user(request.user, "lectures.view_lecture")
         .filter(
-            groups=group,
+            lecture_group_types__group=group,
             timestamp__lt=timestamp_starting_separator,
         )
         .all()
@@ -166,7 +160,7 @@ def view_group_lectures(request, group_id: int):
     current_lectures = (
         get_objects_for_user(request.user, "lectures.view_lecture")
         .filter(
-            groups=group,
+            lecture_group_types__group=group,
             timestamp__gte=timestamp_starting_separator,
             timestamp__lte=timestamp_ending_separator,
         )
@@ -180,7 +174,10 @@ def view_group_lectures(request, group_id: int):
             current_lectures,
             (
                 get_objects_for_user(request.user, "lectures.view_lecture")
-                .filter(groups=group, timestamp__gte=timestamp_ending_separator)
+                .filter(
+                    lecture_group_types__group=group,
+                    timestamp__gte=timestamp_ending_separator,
+                )
                 .all()
             ),
         )
@@ -286,6 +283,13 @@ def view_lecture(request, lecture_id: int):
 
     lecture = lecture.first()
 
+    related_group_id = get_related_group_id(request)
+    group = (
+        LectureGroup.objects.get(id=related_group_id)
+        if related_group_id is not None
+        else None
+    )
+
     return render(
         request,
         "lectures/view_lecture.html",
@@ -295,7 +299,8 @@ def view_lecture(request, lecture_id: int):
             "description": f"e-Learningové školení {lecture.name}.",
             "header_name": lecture.name,
             "lecture": lecture,
-            "related_group_id": get_related_group_id(request),
+            "related_group_id": related_group_id,
+            "group": group,
         },
     )
 
@@ -425,7 +430,7 @@ def view_registered(request):
 @require_POST
 @login_required
 def rsvp_lecture(request, lecture_id: int):
-    is_successful, lecture = get_lecture(request, models.Q(id=lecture_id))
+    is_successful, lecture = get_lectures(request, models.Q(id=lecture_id))
 
     if not is_successful:
         return lecture
diff --git a/shared/templates/shared/includes/base.html b/shared/templates/shared/includes/base.html
index a302923..c8d20bb 100644
--- a/shared/templates/shared/includes/base.html
+++ b/shared/templates/shared/includes/base.html
@@ -38,12 +38,12 @@
 
         <!-- TODO: Replace with 2.13.x once released -->
         <link
-            href="http://localhost:3000/css/styles.css"
+            href="https://styleguide.pirati.cz/2.13.x/css/styles.css"
             rel="stylesheet"
             media="all"
         >
         <link
-            href="http://localhost:3000/css/pattern-scaffolding.css"
+            href="https://styleguide.pirati.cz/2.13.x/css/pattern-scaffolding.css"
             rel="stylesheet"
             media="all"
         >
@@ -214,7 +214,7 @@
         </footer>
 
         <script
-            src="http://localhost:3000/js/main.bundle.js"
+            src="https://styleguide.pirati.cz/2.13.x/js/main.bundle.js"
         ></script>
     </body>
 </html>
-- 
GitLab