From d1a6803bede0361ee8ed16ca28976108c6f0ef9f 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 00:04:11 +0200
Subject: [PATCH] add rsvp option, redirect to auth when no access but resource
 exists

---
 .../0016_alter_lecturegroup_user_groups.py    |  16 ++-
 .../migrations/0017_merge_20230526_0027.py    |   8 +-
 ...turegroup_options_lecturegroup_priority.py |  21 ++-
 ...alter_lecturegroup_user_groups_and_more.py |  58 ++++++++
 lectures/models.py                            |  16 +--
 lectures/templates/lectures/view_lecture.html |  29 +++-
 lectures/templatetags/rsvp.py                 |  11 ++
 lectures/urls.py                              |   3 +-
 lectures/views.py                             | 128 ++++++++++++++----
 package-lock.json                             |  15 ++
 package.json                                  |   2 +
 static_src/view_lecture.js                    |  52 +++++++
 .../0009_alter_user_sso_username.py           |  11 +-
 users/models.py                               |   7 +
 webpack.config.js                             |   4 +
 15 files changed, 321 insertions(+), 60 deletions(-)
 create mode 100644 lectures/migrations/0019_alter_lecturegroup_user_groups_and_more.py
 create mode 100644 lectures/templatetags/rsvp.py
 create mode 100644 static_src/view_lecture.js

diff --git a/lectures/migrations/0016_alter_lecturegroup_user_groups.py b/lectures/migrations/0016_alter_lecturegroup_user_groups.py
index 83febc4..9b4e8b7 100644
--- a/lectures/migrations/0016_alter_lecturegroup_user_groups.py
+++ b/lectures/migrations/0016_alter_lecturegroup_user_groups.py
@@ -4,16 +4,20 @@ from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('auth', '0012_alter_user_first_name_max_length'),
-        ('lectures', '0015_alter_lecture_options_alter_lecture_type_and_more'),
+        ("auth", "0012_alter_user_first_name_max_length"),
+        ("lectures", "0015_alter_lecture_options_alter_lecture_type_and_more"),
     ]
 
     operations = [
         migrations.AlterField(
-            model_name='lecturegroup',
-            name='user_groups',
-            field=models.ManyToManyField(blank=True, help_text='Pokud žádné nedefinuješ, školení ve skupině jsou dostupné všem.', to='auth.group', verbose_name='Uživatelské skupiny'),
+            model_name="lecturegroup",
+            name="user_groups",
+            field=models.ManyToManyField(
+                blank=True,
+                help_text="Pokud žádné nedefinuješ, školení ve skupině jsou dostupné všem.",
+                to="auth.group",
+                verbose_name="Uživatelské skupiny",
+            ),
         ),
     ]
diff --git a/lectures/migrations/0017_merge_20230526_0027.py b/lectures/migrations/0017_merge_20230526_0027.py
index 808d94a..aeb7943 100644
--- a/lectures/migrations/0017_merge_20230526_0027.py
+++ b/lectures/migrations/0017_merge_20230526_0027.py
@@ -4,11 +4,9 @@ from django.db import migrations
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('lectures', '0016_alter_lecture_groups'),
-        ('lectures', '0016_alter_lecturegroup_user_groups'),
+        ("lectures", "0016_alter_lecture_groups"),
+        ("lectures", "0016_alter_lecturegroup_user_groups"),
     ]
 
-    operations = [
-    ]
+    operations = []
diff --git a/lectures/migrations/0018_alter_lecturegroup_options_lecturegroup_priority.py b/lectures/migrations/0018_alter_lecturegroup_options_lecturegroup_priority.py
index 2eb6da0..29a9b87 100644
--- a/lectures/migrations/0018_alter_lecturegroup_options_lecturegroup_priority.py
+++ b/lectures/migrations/0018_alter_lecturegroup_options_lecturegroup_priority.py
@@ -4,20 +4,27 @@ from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('lectures', '0017_merge_20230526_0027'),
+        ("lectures", "0017_merge_20230526_0027"),
     ]
 
     operations = [
         migrations.AlterModelOptions(
-            name='lecturegroup',
-            options={'ordering': ('priority', 'name'), 'verbose_name': 'Výuková skupina', 'verbose_name_plural': 'Výukové skupiny'},
+            name="lecturegroup",
+            options={
+                "ordering": ("priority", "name"),
+                "verbose_name": "Výuková skupina",
+                "verbose_name_plural": "Výukové skupiny",
+            },
         ),
         migrations.AddField(
-            model_name='lecturegroup',
-            name='priority',
-            field=models.IntegerField(default=0, help_text='Čím nižší číslo, tím výš se skupina zobrazí.', verbose_name='Priorita'),
+            model_name="lecturegroup",
+            name="priority",
+            field=models.IntegerField(
+                default=0,
+                help_text="Čím nižší číslo, tím výš se skupina zobrazí.",
+                verbose_name="Priorita",
+            ),
             preserve_default=False,
         ),
     ]
diff --git a/lectures/migrations/0019_alter_lecturegroup_user_groups_and_more.py b/lectures/migrations/0019_alter_lecturegroup_user_groups_and_more.py
new file mode 100644
index 0000000..de1df1c
--- /dev/null
+++ b/lectures/migrations/0019_alter_lecturegroup_user_groups_and_more.py
@@ -0,0 +1,58 @@
+# Generated by Django 4.1.4 on 2023-05-30 19:57
+
+from django.db import migrations, models
+
+import lectures.models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("auth", "0012_alter_user_first_name_max_length"),
+        ("lectures", "0018_alter_lecturegroup_options_lecturegroup_priority"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="lecturegroup",
+            name="user_groups",
+            field=models.ManyToManyField(
+                blank=True,
+                help_text="Pokud žádné nedefinuješ, školení ve skupině jsou dostupná všem.",
+                to="auth.group",
+                verbose_name="Uživatelské skupiny",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="lecturelector",
+            name="url",
+            field=models.URLField(
+                blank=True,
+                help_text='Běžně na <a href="https://lide.pirati.cz">aplikaci Lidé</a>.',
+                max_length=256,
+                null=True,
+                verbose_name="Odkaz",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="lecturematerial",
+            name="file",
+            field=lectures.models.LectureMaterialFileField(
+                blank=True,
+                help_text="Pokud máš vložený soubor, nemůžeš definovat odkaz.",
+                null=True,
+                upload_to=lectures.models.get_lecture_material_file_location,
+                verbose_name="Soubor",
+            ),
+        ),
+        migrations.AlterField(
+            model_name="lecturerecording",
+            name="link",
+            field=models.URLField(
+                blank=True,
+                help_text='Běžně na <a href="https://tv.pirati.cz">Pirátskou TV</a>.',
+                max_length=256,
+                null=True,
+                verbose_name="Odkaz",
+            ),
+        ),
+    ]
diff --git a/lectures/models.py b/lectures/models.py
index 66a2d65..b26acfd 100644
--- a/lectures/models.py
+++ b/lectures/models.py
@@ -1,6 +1,5 @@
 import mimetypes
 import uuid
-
 from datetime import datetime, timedelta
 
 from django.contrib.auth.models import Group
@@ -34,7 +33,7 @@ class LectureGroup(NameStrMixin, models.Model):
 
     priority = models.IntegerField(
         verbose_name="Priorita",
-        help_text="Čím nižší číslo, tím výš se skupina zobrazí."
+        help_text="Čím nižší číslo, tím výš se skupina zobrazí.",
     )
 
     user_groups = models.ManyToManyField(
@@ -118,7 +117,9 @@ class LectureLector(NameStrMixin, models.Model):
         blank=True,
         null=True,
         verbose_name="Odkaz",
-        help_text=mark_safe("Běžně na <a href=\"https://lide.pirati.cz\">aplikaci Lidé</a>.")
+        help_text=mark_safe(
+            'Běžně na <a href="https://lide.pirati.cz">aplikaci Lidé</a>.'
+        ),
     )
 
     username = models.CharField(
@@ -155,7 +156,9 @@ class LectureRecording(NameStrMixin, models.Model):
         blank=True,
         null=True,
         verbose_name="Odkaz",
-        help_text=mark_safe("Běžně na <a href=\"https://tv.pirati.cz\">Pirátskou TV</a>.")
+        help_text=mark_safe(
+            'Běžně na <a href="https://tv.pirati.cz">Pirátskou TV</a>.'
+        ),
     )
 
     class Meta:
@@ -193,10 +196,7 @@ def get_lecture_material_file_location(instance, filename):
 class LectureMaterialFileProxy(FieldFile):
     @property
     def url(self) -> str:
-        return reverse(
-            "lectures:download_material_file",
-            args=(str(self.instance.id),)
-        )
+        return reverse("lectures:download_material_file", args=(str(self.instance.id),))
 
 
 class LectureMaterialFileField(models.FileField):
diff --git a/lectures/templates/lectures/view_lecture.html b/lectures/templates/lectures/view_lecture.html
index 505e8de..02de8cf 100644
--- a/lectures/templates/lectures/view_lecture.html
+++ b/lectures/templates/lectures/view_lecture.html
@@ -1,7 +1,10 @@
 {% extends "shared/includes/base.html" %}
-{% load markdownify %}
+{% load render_bundle from webpack_loader %}
+{% load markdownify rsvp %}
 
 {% block content %}
+    {% render_bundle "view_lecture" %}
+
     {% if related_group_id %}
         <div class="flex gap-2 mb-10">
             <i class="ico--chevron-left"></i>
@@ -11,7 +14,29 @@
         </div>
     {% endif %}
 
-    {% include "shared/includes/double_heading.html" with heading=lecture.name subheading=lecture.get_type_display icon="ico--bookmark" %}
+    <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" %}
+
+        <button
+            id="set-rsvp"
+            class="font-alt text-lg bg-black text-white px-5 py-2 duration-100 hover:bg-white hover:text-black"
+            data-url="{% url "lectures:rsvp_lecture" lecture.id %}"
+        >
+            {% if not user|is_registered:lecture %}
+                Zaregistrovat se
+            {% else %}
+                Zrušit registraci
+            {% endif %}
+        </button>
+    </div>
+
+    <script>
+        window.isRegistered = {% if user|is_registered:lecture %}
+            true
+        {% else %}
+            false
+        {% endif %};
+    </script>
 
     <div class="flex flex-col gap-2 my-4 py-4 border-y border-gray-200">
         <div class="flex justify-between gap-2 text-lg text-gray-600">
diff --git a/lectures/templatetags/rsvp.py b/lectures/templatetags/rsvp.py
new file mode 100644
index 0000000..376f99b
--- /dev/null
+++ b/lectures/templatetags/rsvp.py
@@ -0,0 +1,11 @@
+import html
+
+import markdownx
+from django import template
+
+register = template.Library()
+
+
+@register.filter
+def is_registered(user, notice) -> bool:
+    return user.get_lecture_registered(notice)
diff --git a/lectures/urls.py b/lectures/urls.py
index 5b082c4..4434de4 100644
--- a/lectures/urls.py
+++ b/lectures/urls.py
@@ -13,5 +13,6 @@ urlpatterns = [
         "lectures/materials/<str:pk>/file",
         views.LectureMaterialFileDownloadView.as_view(),
         name="download_material_file",
-    )
+    ),
+    path("lectures/<int:lecture_id>/rsvp", views.rsvp_lecture, name="rsvp_lecture"),
 ]
diff --git a/lectures/views.py b/lectures/views.py
index 722b735..87bc580 100644
--- a/lectures/views.py
+++ b/lectures/views.py
@@ -6,9 +6,11 @@ from itertools import chain
 
 from django.conf import settings
 from django.db import models
+from django.http import HttpResponseRedirect, JsonResponse
 from django.shortcuts import get_object_or_404, render
 from django.urls import reverse
 from django.utils import timezone
+from django.views.decorators.http import require_POST
 from django_downloadview import ObjectDownloadView
 from django_http_exceptions import HTTPExceptions
 from guardian.shortcuts import get_objects_for_user
@@ -28,10 +30,8 @@ class LectureMaterialFileDownloadView(ObjectDownloadView):
             .filter(
                 lecture__groups__in=(
                     get_objects_for_user(
-                        self.current_user,
-                        "lectures.view_lecturegroup"
-                    ).
-                    filter(
+                        self.current_user, "lectures.view_lecturegroup"
+                    ).filter(
                         models.Q(user_groups__in=self.current_user.groups.all())
                         | models.Q(user_groups=None)
                     )
@@ -39,14 +39,6 @@ class LectureMaterialFileDownloadView(ObjectDownloadView):
             )
         )
 
-        print(
-            queryset,
-            get_objects_for_user(
-                self.current_user,
-                "lectures.view_lecturegroup"
-            )
-        )
-
         return queryset
 
     def get(self, request, *args, **kwargs):
@@ -61,12 +53,58 @@ def get_base_context(request) -> dict:
     }
 
 
+def generate_auth_redirect(request) -> HttpResponseRedirect:
+    return HttpResponseRedirect(
+        request.build_absolute_uri(reverse("oidc_authentication_init"))
+        + "?next="
+        + request.path
+    )
+
+
+def get_lecture(request, id: int) -> tuple:
+    lecture = (
+        get_objects_for_user(request.user, "lectures.view_lecture")
+        .filter(id=id)
+        .first()
+    )
+
+    if lecture is None:
+        raise HTTPExceptions.NOT_FOUND
+
+    if not (
+        get_objects_for_user(request.user, "lectures.view_lecturegroup")
+        .filter(
+            models.Q(id__in=lecture.groups.all())
+            & (
+                (
+                    models.Q(user_groups__in=request.user.groups.all())
+                    | models.Q(user_groups=None)
+                )
+                if not request.user.is_superuser
+                else models.Q(id__isnull=False)  # Always True
+            )
+        )
+        .distinct()
+        .exists()
+    ):  # User does not have access to related groups
+        if request.user.is_authenticated:  # The user can log in
+            raise HTTPExceptions.NOT_FOUND
+        else:  # They can log in
+            return False, generate_auth_redirect(request)
+
+    return True, lecture
+
+
 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())
+                | models.Q(user_groups=None)
+            )
+            if not request.user.is_superuser
+            else models.Q(id__isnull=False)  # Always True
         )
         .distinct()
         .all()
@@ -87,11 +125,29 @@ def view_groups(request):
 
 
 def view_group_lectures(request, group_id: int):
-    group = get_object_or_404(
-        get_objects_for_user(request.user, "lectures.view_lecturegroup"),
-        id=group_id,
+    group = get_objects_for_user(request.user, "lectures.view_lecturegroup").filter(
+        id=group_id
     )
 
+    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)
+        )
+
+    if not group.exists():
+        if not group_id_exists:  # Doesn't exist at all
+            raise HTTPExceptions.NOT_FOUND
+        elif group_id_exists:  # Exists without permissions checks
+            if request.user_is_authenticated:  # The user is logged in
+                raise HTTPExceptions.NOT_FOUND
+            else:  # The user can log in
+                return generate_auth_redirect(request)
+
+    group = group.first()
+
     timestamp_starting_separator = timezone.now() - Lecture.is_current_starting_treshold
     timestamp_ending_separator = timezone.now() + Lecture.is_current_ending_treshold
 
@@ -176,8 +232,8 @@ def view_group_lectures(request, group_id: int):
         ].append(lecture)
 
     # locale.setlocale(
-    # locale.LC_ALL,
-    # ""
+    #     locale.LC_ALL,
+    #     ""
     # )
 
     return render(
@@ -200,14 +256,10 @@ def view_group_lectures(request, group_id: int):
 
 
 def view_lecture(request, lecture_id: int):
-    lecture = (
-        get_objects_for_user(request.user, "lectures.view_lecture")
-        .filter(id=lecture_id)
-        .first()
-    )
+    is_successful, lecture = get_lecture(request, lecture_id)
 
-    if lecture is None:
-        raise HTTPExceptions.NOT_FOUND
+    if not is_successful:
+        return lecture
 
     related_group_id = request.GET.get("related_group_id")
 
@@ -232,3 +284,27 @@ def view_lecture(request, lecture_id: int):
             "related_group_id": related_group_id,
         },
     )
+
+
+@require_POST
+def rsvp_lecture(request, lecture_id: int):
+    is_successful, lecture = get_lecture(request, lecture_id)
+
+    if not is_successful:
+        return lecture
+
+    is_registered = request.POST.get("register", "false") == "true"
+
+    if is_registered is request.user.get_lecture_registered(
+        lecture
+    ):  # The RSVP state is the same
+        return JsonResponse({"success": False})
+
+    if is_registered:
+        request.user.rsvp_lectures.add(lecture)
+    else:
+        request.user.rsvp_lectures.remove(lecture)
+
+    request.user.save()
+
+    return JsonResponse({"success": True})
diff --git a/package-lock.json b/package-lock.json
index 84a39cd..b96b016 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,8 +11,10 @@
       "dependencies": {
         "@fullcalendar/core": "^6.1.6",
         "@tailwindcss/typography": "^0.5.9",
+        "alertifyjs": "^1.13.1",
         "css-loader": "^6.7.3",
         "jquery": "^3.6.3",
+        "js-cookie": "^3.0.5",
         "style-loader": "^3.3.1",
         "tailwindcss": "^3.2.4",
         "tippy.js": "^6.3.7",
@@ -428,6 +430,11 @@
         "ajv": "^6.9.1"
       }
     },
+    "node_modules/alertifyjs": {
+      "version": "1.13.1",
+      "resolved": "https://registry.npmjs.org/alertifyjs/-/alertifyjs-1.13.1.tgz",
+      "integrity": "sha512-CckZE2dZDsEEXglOXKxT00vUDV5A6udZom+bn1XHdIWlbSFZgYq7UXCBlwkShhIH3Li/1VxLmr55GOQFQ12WSg=="
+    },
     "node_modules/ansi-regex": {
       "version": "5.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -1090,6 +1097,14 @@
       "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.4.tgz",
       "integrity": "sha512-v28EW9DWDFpzcD9O5iyJXg3R3+q+mET5JhnjJzQUZMHOv67bpSIHq81GEYpPNZHG+XXHsfSme3nxp/hndKEcsQ=="
     },
+    "node_modules/js-cookie": {
+      "version": "3.0.5",
+      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
+      "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/json-parse-even-better-errors": {
       "version": "2.3.1",
       "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz",
diff --git a/package.json b/package.json
index 4ec38d1..59a9884 100644
--- a/package.json
+++ b/package.json
@@ -14,8 +14,10 @@
   "dependencies": {
     "@fullcalendar/core": "^6.1.6",
     "@tailwindcss/typography": "^0.5.9",
+    "alertifyjs": "^1.13.1",
     "css-loader": "^6.7.3",
     "jquery": "^3.6.3",
+    "js-cookie": "^3.0.5",
     "style-loader": "^3.3.1",
     "tailwindcss": "^3.2.4",
     "tippy.js": "^6.3.7",
diff --git a/static_src/view_lecture.js b/static_src/view_lecture.js
new file mode 100644
index 0000000..59800fc
--- /dev/null
+++ b/static_src/view_lecture.js
@@ -0,0 +1,52 @@
+import $ from "jquery";
+
+import alertify from "alertifyjs";
+import "alertifyjs/build/css/alertify.css";
+
+import Cookies from "js-cookie";
+
+
+$(window).ready(
+    () => {
+        $("#set-rsvp").on(
+            "click",
+            async (event) => {
+                const body = new FormData();
+                body.append("register", (!window.isRegistered).toString());
+
+                let response = await fetch(
+                    event.currentTarget.dataset.url,
+                    {
+                        method: "POST",
+                        credentials: "include",
+                        headers: {
+                            "X-CSRFToken": Cookies.get("csrftoken")
+                        },
+                        body: body
+                    }
+                );
+
+                try {
+                    response = await response.json();
+                } catch (e) {
+                    alertify.error("Chyba při registraci - odpověď serveru.");
+                    return;
+                }
+
+                if (response["success"]) {
+                    window.isRegistered = !window.isRegistered;
+
+                    if (window.isRegistered) {
+                        event.currentTarget.innerHTML = "Zrušit registraci";
+                        alertify.success("Školení zaregistrováno.");
+                    } else {
+                        event.currentTarget.innerHTML = "Zaregistrovat se";
+                        alertify.success("Registrace zrušena.");
+                    }
+                } else {
+                    alertify.error("Chyba při registraci - odmítnuta.");
+                }
+            }
+        );
+    }
+);
diff --git a/users/migrations/0009_alter_user_sso_username.py b/users/migrations/0009_alter_user_sso_username.py
index 332f823..e92fc6c 100644
--- a/users/migrations/0009_alter_user_sso_username.py
+++ b/users/migrations/0009_alter_user_sso_username.py
@@ -4,15 +4,16 @@ from django.db import migrations, models
 
 
 class Migration(migrations.Migration):
-
     dependencies = [
-        ('users', '0008_alter_user_rsvp_lectures'),
+        ("users", "0008_alter_user_rsvp_lectures"),
     ]
 
     operations = [
         migrations.AlterField(
-            model_name='user',
-            name='sso_username',
-            field=models.CharField(blank=True, max_length=128, null=True, verbose_name='Username z SSO'),
+            model_name="user",
+            name="sso_username",
+            field=models.CharField(
+                blank=True, max_length=128, null=True, verbose_name="Username z SSO"
+            ),
         ),
     ]
diff --git a/users/models.py b/users/models.py
index c1f55f3..3557edc 100644
--- a/users/models.py
+++ b/users/models.py
@@ -49,6 +49,13 @@ class User(pirates_models.AbstractUser):
 
         return f"{first_name}{self.last_name}"
 
+    def get_lecture_registered(self, lecture) -> bool:
+        from lectures.models import Lecture
+
+        return Lecture.objects.filter(
+            id=lecture.id, id__in=self.rsvp_lectures.all()
+        ).exists()
+
     class Meta:
         app_label = "users"
         verbose_name = "UĹľivatel"
diff --git a/webpack.config.js b/webpack.config.js
index 7141899..ba93f33 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -13,6 +13,10 @@ module.exports = {
       import: path.resolve("static_src", "view_group_lectures.js"),
       dependOn: "shared",
     },
+    view_lecture: {
+      import: path.resolve("static_src", "view_lecture.js"),
+      dependOn: "shared",
+    },
     shared: ["jquery"],
   },
   output: {
-- 
GitLab