From 078400635d74803f2354a70ebbdd1c1d2f7d9f8f 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 15:00:44 +0200 Subject: [PATCH] add registered events view, finished lector view, optimizations --- ...023_alter_lecturelector_avatar_and_more.py | 24 ++++++ lectures/models.py | 4 +- lectures/templates/lectures/view_lector.html | 61 +++++++++++++- lectures/templates/lectures/view_lecture.html | 55 +++++++----- .../templates/lectures/view_registered.html | 11 +++ lectures/urls.py | 6 ++ lectures/views.py | 83 ++++++++++++++++--- requirements/base.txt | 1 + shared/templates/shared/includes/base.html | 17 +++- 9 files changed, 219 insertions(+), 43 deletions(-) create mode 100644 lectures/migrations/0023_alter_lecturelector_avatar_and_more.py create mode 100644 lectures/templates/lectures/view_registered.html diff --git a/lectures/migrations/0023_alter_lecturelector_avatar_and_more.py b/lectures/migrations/0023_alter_lecturelector_avatar_and_more.py new file mode 100644 index 0000000..9b43512 --- /dev/null +++ b/lectures/migrations/0023_alter_lecturelector_avatar_and_more.py @@ -0,0 +1,24 @@ +# 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'), + ] + + 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'), + ), + 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'), + ), + ] diff --git a/lectures/models.py b/lectures/models.py index 8b143d9..687a2fd 100644 --- a/lectures/models.py +++ b/lectures/models.py @@ -176,13 +176,13 @@ class LectureLector(NameStrMixin, models.Model): ), ) - avatar = models.FileField( + avatar = models.ImageField( blank=True, null=True, upload_to=get_file_location, verbose_name="ProfilovĂ˝ obrázek", help_text=( - "VloĹľenĂ˝ soubor má prioritu nad obrázkem synchronizovanĂ˝m " "z Chobotnice." + "VloĹľenĂ˝ soubor má prioritu nad obrázkem synchronizovanĂ˝m z Chobotnice." ), ) diff --git a/lectures/templates/lectures/view_lector.html b/lectures/templates/lectures/view_lector.html index d3d8247..9a1f1bd 100644 --- a/lectures/templates/lectures/view_lector.html +++ b/lectures/templates/lectures/view_lector.html @@ -5,19 +5,72 @@ {% block content %} {% render_bundle "view_group_lectures" %} - {% include "shared/includes/double_heading.html" with heading=header_name subheading=header_desc icon="ico--user" %} + {% if related_lecture_id %} + <div class="flex gap-2 mb-10"> + <i class="ico--chevron-left"></i> + <a + href="{% url "lectures:view_lecture" related_lecture_id %}{% if related_group_id %}?related_group_id={{ related_group_id }}{% endif %}" + >ZpÄ›t na školenĂ</a> + </div> + {% endif %} + <div class="flex gap-4 mb-10 items-center"> + <img + alt="ProfilovĂ˝ obrázek lektora" + {% if lector.avatar %} + src="{{ lector.avatar.url }}" + {% elif lector.username %} + src="https://a.pirati.cz/piratar/300/{{ lector.username }}.jpg" + {% else %} + src="https://a.pirati.cz/piratar/300/default.jpg" + {% endif %} + class="h-20 w-20 md:h-24 md:w-24 rounded-full" + > + <div> + <h1 class="head-alt-md md:head-alt-lg">{{ header_name }}</h1> + <h2 class="head-alt-sm">{{ header_desc }}</h2> + </div> + </div> + {% if lector.description %} + <div class="mb-10 prose max-w-none"> + {{ lector.description|markdownify|safe }} + </div> + {% endif %} - <h2 class="head-alt-md my-8">VytvoĹ™ená školenĂ</h2> + <div class="flex flex-col gap-2 my-4 py-4 border-y border-gray-200 mb-10"> + <div class="flex justify-between gap-2 text-lg text-gray-600"> + <div class="flex gap-2 items-center"> + <i class="ico--globe"></i> + <span>Web</span> + </div> + <a + href="{{ lector.url }}" + class="underline" + > + {{ lector.url }} + </a> + </div> + <div class="flex justify-between gap-2 text-lg text-gray-600"> + <div class="flex gap-2 items-center"> + <i class="ico--book"></i> + <span>PoÄŤet školenĂ</span> + </div> + <div> + {{ lectures|length }} + </div> + </div> + </div> - <ul class="flex flex-col gap-2"> + <h2 class="head-alt-md my-8">VyuÄŤovaná školenĂ</h2> + + <ul class="grid grid-cols-2 gap-2"> {% if lectures %} {% for lecture in lectures %} {% include "lectures/includes/lecture.html" %} {% endfor %} {% else %} - <span class="text-gray-600">Nebyly nalezeny žádnĂ© vĂ˝sledky.</span> + <span class="text-gray-600">Žádná evidovaná školenĂ.</span> {% endif %} </ul> {% endblock %} diff --git a/lectures/templates/lectures/view_lecture.html b/lectures/templates/lectures/view_lecture.html index 722cdbd..f36e41d 100644 --- a/lectures/templates/lectures/view_lecture.html +++ b/lectures/templates/lectures/view_lecture.html @@ -17,17 +17,19 @@ <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> + {% if user.is_authenticated %} + <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> + {% endif %} </div> <script> @@ -139,11 +141,13 @@ <ul class="grid grid-cols-1 md:grid-cols-2 gap-4"> {% for lector in lectors %} <li class="card elevation-3"> - <a - href="{% url "lectures:view_lector" lector.id %}" - class="hover:no-underline flex items-center gap-4 {% if lector.url %}duration-150 hover:bg-gray-100{% endif %} card__body" + <div + class="hover:no-underline flex items-start gap-4 {% if lector.url %}duration-150 hover:bg-gray-100{% endif %} card__body" > - <div class="avatar badge__avatar avatar--sm"> + <a + class="avatar shrink-0 object-contain badge__avatar avatar--sm" + href="{% url "lectures:view_lector" lector.id %}?related_lecture_id={{ lecture.id }}{% if related_group_id %}&related_group_id={{ related_group_id }}{% endif %}" + > <img {% if lector.avatar %} src="{{ lector.avatar.url }}" @@ -152,23 +156,30 @@ {% else %} src="https://a.pirati.cz/piratar/300/default.jpg" {% endif %} + class="w-20 h-20" alt="ProfilovĂ˝ obrázek {{ lector.name }}" > - </div> + </a> <div class="flex flex-col gap-2"> <span class="head-heavy-2xs"> - <div class="content-block--nostyle"> - {{ lector.name }} - </div> + <a + class="content-block--nostyle" + href="{% url "lectures:view_lector" lector.id %}?related_lecture_id={{ lecture.id }}{% if related_group_id %}&related_group_id={{ related_group_id }}{% endif %}" + >{{ lector.name }}</a> </span> + {% if lector.description %} + <div class="prose max-w-none"> + {{ lector.description|markdownify|safe }} + </div> + {% endif %} {% if lector.url %} - <div + <a class="block text-ellipsis overflow-hidden" href="{{ lector.url }}" - >{{ lector.url }}</div> + >{{ lector.url }}</a> {% endif %} </div> - </a> + </div> </li> {% endfor %} </ul> diff --git a/lectures/templates/lectures/view_registered.html b/lectures/templates/lectures/view_registered.html new file mode 100644 index 0000000..d6241c4 --- /dev/null +++ b/lectures/templates/lectures/view_registered.html @@ -0,0 +1,11 @@ +{% extends "shared/includes/base.html" %} +{% load render_bundle from webpack_loader %} +{% load markdownify static %} + +{% block content %} + {% include "shared/includes/double_heading.html" with heading=header_name icon="ico--pin" %} + + <div class="__js-root"> + <ui-year-calendar events='{{ events|safe }}'></ui-year-calendar> + </div> +{% endblock %} diff --git a/lectures/urls.py b/lectures/urls.py index 90954a4..292b455 100644 --- a/lectures/urls.py +++ b/lectures/urls.py @@ -15,6 +15,12 @@ urlpatterns = [ name="download_material_file", ), path("lectures/<int:lecture_id>/rsvp", views.rsvp_lecture, name="rsvp_lecture"), + path("lectors/<int:id>", views.view_lector, name="view_lector"), + path( + "lectures/registered", + views.view_registered, + name="view_registered", + ), path( "search", views.search, diff --git a/lectures/views.py b/lectures/views.py index 06ddf8a..1624c92 100644 --- a/lectures/views.py +++ b/lectures/views.py @@ -5,6 +5,7 @@ from datetime import datetime from itertools import chain from django.conf import settings +from django.contrib.auth.decorators import login_required from django.db import models from django.http import HttpResponseRedirect, JsonResponse from django.shortcuts import get_object_or_404, render @@ -258,14 +259,7 @@ def view_group_lectures(request, group_id: int): ) -def view_lecture(request, lecture_id: int): - is_successful, lecture = get_lectures(request, models.Q(id=lecture_id)) - - if not is_successful: - return lecture - - lecture = lecture.first() - +def get_related_group_id(request): related_group_id = request.GET.get("related_group_id") if related_group_id not in ("", None): @@ -273,7 +267,7 @@ def view_lecture(request, lecture_id: int): raise HTTPExceptions.BAD_REQUEST if not ( - get_objects_for_user(request.user, "lectures.view_lecture") + get_objects_for_user(request.user, "lectures.view_lecturegroup") .filter(id=related_group_id) .exists() ): @@ -281,6 +275,17 @@ def view_lecture(request, lecture_id: int): # just because of the related_group_id being wrong. related_group_id = None + return related_group_id + + +def view_lecture(request, lecture_id: int): + is_successful, lecture = get_lectures(request, models.Q(id=lecture_id)) + + if not is_successful: + return lecture + + lecture = lecture.first() + return render( request, "lectures/view_lecture.html", @@ -290,7 +295,7 @@ def view_lecture(request, lecture_id: int): "description": f"e-LearningovĂ© školenĂ {lecture.name}.", "header_name": lecture.name, "lecture": lecture, - "related_group_id": related_group_id, + "related_group_id": get_related_group_id(request), }, ) @@ -349,6 +354,21 @@ def view_lector(request, id): request, models.Q(id__in=lector.lectures.all()), get_exceptions=False ) + related_lecture_id = request.GET.get("related_lecture_id") + + if related_lecture_id not in ("", None): + if not str(related_lecture_id).isnumeric(): + raise HTTPExceptions.BAD_REQUEST + + if not ( + get_objects_for_user(request.user, "lectures.view_lecture") + .filter(id=related_lecture_id) + .exists() + ): + # Ignore the wrong part of the URL and move on, don't raise exceptions + # just because of the related_lecture_id being wrong. + related_lecture_id = None + return render( request, "lectures/view_lector.html", @@ -356,15 +376,54 @@ def view_lector(request, id): **get_base_context(request), "title": lector.name, "description": f"Informace o lektorovi - {lector.name}.", - "header_name": "Lektor", - "header_desc": lector.name, + "header_name": lector.name, + "header_desc": "Lektor", + "related_lecture_id": related_lecture_id, + "related_group_id": get_related_group_id(request), "lector": lector, "lectures": lectures, }, ) +@login_required +def view_registered(request): + lectures = get_lectures( + request, models.Q(rsvp_users=request.user), get_exceptions=False + ) + + events = [] + + for lecture in lectures: + events.append( + { + "title": lecture.name, + "start": lecture.timestamp.isoformat(), + "url": request.build_absolute_uri( + reverse("lectures:view_lecture", args=(lecture.id,)) + ), + "description": ( + lecture.description if lecture.description is not None else "" + ), + } + ) + + return render( + request, + "lectures/view_registered.html", + { + **get_base_context(request), + "title": "Tvá školenĂ", + "description": "ZobrazenĂ tvĂ˝ch zaregistrovanĂ˝ch školenĂ.", + "header_name": "Tvá školenĂ", + "lectures": lectures, + "events": json.dumps(events), + }, + ) + + @require_POST +@login_required def rsvp_lecture(request, lecture_id: int): is_successful, lecture = get_lecture(request, models.Q(id=lecture_id)) diff --git a/requirements/base.txt b/requirements/base.txt index 97e5804..7056f4a 100644 --- a/requirements/base.txt +++ b/requirements/base.txt @@ -14,3 +14,4 @@ django-environ==0.9.0 django-http-exceptions==1.4.0 django-guardian==2.4.0 PyJWT==2.6.0 +Pillow==9.5.0 diff --git a/shared/templates/shared/includes/base.html b/shared/templates/shared/includes/base.html index b07cf3f..21596f7 100644 --- a/shared/templates/shared/includes/base.html +++ b/shared/templates/shared/includes/base.html @@ -36,13 +36,14 @@ media="all" > - <link - href="https://styleguide.pirati.cz/2.12.x/css/styles.css" + <!-- TODO: Replace with 2.13.x once released --> + <link + href="http://localhost:3000/css/styles.css" rel="stylesheet" media="all" > <link - href="https://styleguide.pirati.cz/2.12.x/css/pattern-scaffolding.css" + href="http://localhost:3000/css/pattern-scaffolding.css" rel="stylesheet" media="all" > @@ -86,6 +87,16 @@ </li> {% endif %} + <li class="navbar-menu__item"> + <a + href="{% url "lectures:view_registered" %}" + data-href="{% url "lectures:view_registered" %}" + class="navbar-menu__link flex items-center gap-2" + > + <i class="ico--pin text-sm"></i>Tvá školenĂ + </a> + </li> + <li class="navbar-menu__item"> <a href="{% url "lectures:search" %}" -- GitLab