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