From bcb5472dd6ec6da2bd83c5a4a66add4060525931 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 12:02:01 +0200 Subject: [PATCH] add lector views, searching, improve db structure, custom lector avatars / descriptions --- lectures/admin.py | 17 ++- ...e_lecturelector_lecture_lecture_lectors.py | 25 ++++ .../migrations/0021_lecture_rsvp_users.py | 24 ++++ ...atar_lecturelector_description_and_more.py | 57 ++++++++ lectures/models.py | 110 ++++++++++----- .../templates/lectures/includes/lecture.html | 5 +- lectures/templates/lectures/view_lector.html | 23 ++++ lectures/templates/lectures/view_lecture.html | 19 ++- lectures/templates/lectures/view_search.html | 28 ++++ .../lectures/view_search_results.html | 39 ++++++ lectures/urls.py | 5 + lectures/views.py | 130 ++++++++++++++---- shared/templates/shared/includes/base.html | 10 ++ users/admin.py | 2 +- .../0010_remove_user_rsvp_lectures.py | 16 +++ users/models.py | 11 +- 16 files changed, 431 insertions(+), 90 deletions(-) create mode 100644 lectures/migrations/0020_remove_lecturelector_lecture_lecture_lectors.py create mode 100644 lectures/migrations/0021_lecture_rsvp_users.py create mode 100644 lectures/migrations/0022_lecturelector_avatar_lecturelector_description_and_more.py create mode 100644 lectures/templates/lectures/view_lector.html create mode 100644 lectures/templates/lectures/view_search.html create mode 100644 lectures/templates/lectures/view_search_results.html create mode 100644 users/migrations/0010_remove_user_rsvp_lectures.py diff --git a/lectures/admin.py b/lectures/admin.py index 552b6cf..ff04fa8 100644 --- a/lectures/admin.py +++ b/lectures/admin.py @@ -25,12 +25,7 @@ class LectureGroupAdmin(MarkdownxGuardedModelAdmin): list_display = ("name", "priority") -class LectureLectorInline(admin.StackedInline): - model = LectureLector - extra = 1 - - -class LectureRecordingInline(admin.TabularInline): +class LectureRecordingInline(admin.StackedInline): model = LectureRecording extra = 1 @@ -42,13 +37,13 @@ class LectureMaterialInline(admin.StackedInline): class LectureAdmin(MarkdownxGuardedModelAdmin): inlines = ( - LectureLectorInline, LectureRecordingInline, LectureMaterialInline, ) - autocomplete_fields = ("groups",) + autocomplete_fields = ("groups", "lectors", "rsvp_users") search_fields = ("name", "description") + readonly_fields = ("rsvp_users",) list_display = ( "name", @@ -56,12 +51,16 @@ class LectureAdmin(MarkdownxGuardedModelAdmin): ) +class LectureLectorAdmin(MarkdownxGuardedModelAdmin): + search_fields = ("name", "username") + + for model in ( - LectureLector, LectureMaterial, LectureRecording, ): admin.site.register(model, IndexHiddenModelAdmin) +admin.site.register(LectureLector, LectureLectorAdmin) admin.site.register(Lecture, LectureAdmin) admin.site.register(LectureGroup, LectureGroupAdmin) diff --git a/lectures/migrations/0020_remove_lecturelector_lecture_lecture_lectors.py b/lectures/migrations/0020_remove_lecturelector_lecture_lecture_lectors.py new file mode 100644 index 0000000..b5798c9 --- /dev/null +++ b/lectures/migrations/0020_remove_lecturelector_lecture_lecture_lectors.py @@ -0,0 +1,25 @@ +# Generated by Django 4.1.4 on 2023-05-31 06:19 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("lectures", "0019_alter_lecturegroup_user_groups_and_more"), + ] + + operations = [ + migrations.RemoveField( + model_name="lecturelector", + name="lecture", + ), + migrations.AddField( + model_name="lecture", + name="lectors", + field=models.ManyToManyField( + related_name="lectures", + to="lectures.lecturelector", + verbose_name="Lektoři", + ), + ), + ] diff --git a/lectures/migrations/0021_lecture_rsvp_users.py b/lectures/migrations/0021_lecture_rsvp_users.py new file mode 100644 index 0000000..930a00d --- /dev/null +++ b/lectures/migrations/0021_lecture_rsvp_users.py @@ -0,0 +1,24 @@ +# Generated by Django 4.1.4 on 2023-05-31 06:55 + +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("lectures", "0020_remove_lecturelector_lecture_lecture_lectors"), + ] + + operations = [ + migrations.AddField( + model_name="lecture", + name="rsvp_users", + field=models.ManyToManyField( + blank=True, + related_name="rsvp_lectures", + to=settings.AUTH_USER_MODEL, + verbose_name="Zaregistrovaní uživatelé", + ), + ), + ] diff --git a/lectures/migrations/0022_lecturelector_avatar_lecturelector_description_and_more.py b/lectures/migrations/0022_lecturelector_avatar_lecturelector_description_and_more.py new file mode 100644 index 0000000..a33f32c --- /dev/null +++ b/lectures/migrations/0022_lecturelector_avatar_lecturelector_description_and_more.py @@ -0,0 +1,57 @@ +# Generated by Django 4.1.4 on 2023-05-31 09:53 + +import markdownx.models +from django.db import migrations, models + +import lectures.models + + +class Migration(migrations.Migration): + dependencies = [ + ("lectures", "0021_lecture_rsvp_users"), + ] + + operations = [ + migrations.AddField( + model_name="lecturelector", + name="avatar", + field=models.FileField( + 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.AddField( + model_name="lecturelector", + name="description", + field=markdownx.models.MarkdownxField( + blank=True, + help_text='Můžeš použít <a href="https://cs.wikipedia.org/wiki/Markdown#P%C5%99%C3%ADklad_u%C5%BEit%C3%AD">Markdown</a>.', + max_length=512, + null=True, + verbose_name="Popis", + ), + ), + migrations.AlterField( + model_name="lecture", + name="description", + field=markdownx.models.MarkdownxField( + blank=True, + help_text='Můžeš použít <a href="https://cs.wikipedia.org/wiki/Markdown#P%C5%99%C3%ADklad_u%C5%BEit%C3%AD">Markdown</a>.', + null=True, + verbose_name="Popis", + ), + ), + migrations.AlterField( + model_name="lecturegroup", + name="description", + field=markdownx.models.MarkdownxField( + blank=True, + help_text='Můžeš použít <a href="https://cs.wikipedia.org/wiki/Markdown#P%C5%99%C3%ADklad_u%C5%BEit%C3%AD">Markdown</a>.', + null=True, + verbose_name="Popis", + ), + ), + ] diff --git a/lectures/models.py b/lectures/models.py index b26acfd..8b143d9 100644 --- a/lectures/models.py +++ b/lectures/models.py @@ -18,6 +18,33 @@ from . import settings as app_settings # Create your models here. +def get_file_location(instance, filename, path_prefix="public"): + mimetypes_instance = mimetypes.MimeTypes() + + current_time = datetime.today() + guessed_type = mimetypes_instance.guess_type(filename, strict=False)[0] + + extension = "" + + if guessed_type is not None: + for mapper in mimetypes_instance.types_map_inv: + if guessed_type not in mapper: + continue + + extension = mapper[guessed_type] + + if isinstance(extension, list): + extension = extension[0] + + break + + return ( + f"{path_prefix}/" + f"{current_time.year}/{current_time.month}/{current_time.day}/" + f"{uuid.uuid4()}{extension}" + ) + + class LectureGroup(NameStrMixin, models.Model): name = models.CharField( max_length=128, @@ -28,7 +55,12 @@ class LectureGroup(NameStrMixin, models.Model): null=True, blank=True, verbose_name="Popis", - help_text="Můžeš použít Markdown.", + 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>." + ), ) priority = models.IntegerField( @@ -86,7 +118,23 @@ class Lecture(NameStrMixin, models.Model): blank=True, null=True, verbose_name="Popis", - help_text="Můžeš použít Markdown.", + 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>." + ), + ) + + lectors = models.ManyToManyField( + "LectureLector", related_name="lectures", verbose_name="Lektoři" + ) + + rsvp_users = models.ManyToManyField( + "users.User", + blank=True, + related_name="rsvp_lectures", + verbose_name="Zaregistrovaní uživatelé", ) # Settings @@ -100,18 +148,24 @@ class Lecture(NameStrMixin, models.Model): class LectureLector(NameStrMixin, models.Model): - lecture = models.ForeignKey( - "Lecture", - on_delete=models.CASCADE, - related_name="lectors", - verbose_name="Školení", - ) - name = models.CharField( max_length=128, verbose_name="Jméno", ) + description = MarkdownxField( + max_length=512, + blank=True, + 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>." + ), + ) + url = models.URLField( max_length=256, blank=True, @@ -122,6 +176,16 @@ class LectureLector(NameStrMixin, models.Model): ), ) + avatar = models.FileField( + 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." + ), + ) + username = models.CharField( max_length=128, blank=True, @@ -129,7 +193,8 @@ class LectureLector(NameStrMixin, models.Model): verbose_name="Uživatelské jméno", help_text=( "Např. na fóru, nebo v Chobotnici. Užívá se " - "k synchronizaci profilového obrázku." + "k synchronizaci profilového obrázku, v budoucnu " + "i dalších informací." ), ) @@ -167,30 +232,7 @@ class LectureRecording(NameStrMixin, models.Model): def get_lecture_material_file_location(instance, filename): - mimetypes_instance = mimetypes.MimeTypes() - - current_time = datetime.today() - guessed_type = mimetypes_instance.guess_type(filename, strict=False)[0] - - extension = "" - - if guessed_type is not None: - for mapper in mimetypes_instance.types_map_inv: - if guessed_type not in mapper: - continue - - extension = mapper[guessed_type] - - if isinstance(extension, list): - extension = extension[0] - - break - - return ( - "_private/" - f"{current_time.year}/{current_time.month}/{current_time.day}/" - f"{uuid.uuid4()}{extension}" - ) + return get_file_location(instane, filename, path_prefix="_private") class LectureMaterialFileProxy(FieldFile): diff --git a/lectures/templates/lectures/includes/lecture.html b/lectures/templates/lectures/includes/lecture.html index 15aa277..feda986 100644 --- a/lectures/templates/lectures/includes/lecture.html +++ b/lectures/templates/lectures/includes/lecture.html @@ -1,7 +1,10 @@ {% load markdownify %} <li> - <a href="{% url "lectures:view_lecture" lecture.id %}?related_group_id={{ group.id }}" class="hover:no-underline"> + <a + href="{% url "lectures:view_lecture" lecture.id %}{% if group %}?related_group_id={{ group.id }}{% endif %}" + class="hover:no-underline" + > <div class="card elevation-6"> <div class="card__body p-5 hover:bg-gray-100 duration-100"> <h2 class="head-alt-sm mb-4"> diff --git a/lectures/templates/lectures/view_lector.html b/lectures/templates/lectures/view_lector.html new file mode 100644 index 0000000..d3d8247 --- /dev/null +++ b/lectures/templates/lectures/view_lector.html @@ -0,0 +1,23 @@ +{% extends "shared/includes/base.html" %} +{% load render_bundle from webpack_loader %} +{% load markdownify static %} + +{% block content %} + {% render_bundle "view_group_lectures" %} + + {% include "shared/includes/double_heading.html" with heading=header_name subheading=header_desc icon="ico--user" %} + + + + <h2 class="head-alt-md my-8">Vytvořená školení</h2> + + <ul class="flex flex-col 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> + {% endif %} + </ul> +{% endblock %} diff --git a/lectures/templates/lectures/view_lecture.html b/lectures/templates/lectures/view_lecture.html index 02de8cf..722cdbd 100644 --- a/lectures/templates/lectures/view_lecture.html +++ b/lectures/templates/lectures/view_lecture.html @@ -139,16 +139,19 @@ <ul class="grid grid-cols-1 md:grid-cols-2 gap-4"> {% for lector in lectors %} <li class="card elevation-3"> - {% if lector.url %} - <a href="{{ lector.url }}" - {% else %} - <div - {% endif %} + <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="avatar badge__avatar avatar--sm"> <img - src="https://a.pirati.cz/piratar/300/{% if lector.username %}{{ lector.username }}{% else %}default{% endif %}.jpg" + {% 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 %} alt="Profilový obrázek {{ lector.name }}" > </div> @@ -165,11 +168,7 @@ >{{ lector.url }}</div> {% endif %} </div> - {% if lector.url %} </a> - {% else %} - </div> - {% endif %} </li> {% endfor %} </ul> diff --git a/lectures/templates/lectures/view_search.html b/lectures/templates/lectures/view_search.html new file mode 100644 index 0000000..70fe084 --- /dev/null +++ b/lectures/templates/lectures/view_search.html @@ -0,0 +1,28 @@ +{% extends "shared/includes/base.html" %} +{% load render_bundle from webpack_loader %} +{% load markdownify static %} + +{% block content %} + {% render_bundle "view_group_lectures" %} + + {% include "shared/includes/double_heading.html" with heading=header_name subheading=header_desc icon="ico--search" %} + + <form + class="flex flex-row justify-center" + method="get" + > + <input + name="q" + class="bg-gray-200 w-56 lg:w-80 h-10 px-4 text-lg xl:h-14 xl:px-5" + type="text" + placeholder="Hledat školení..." + aria-label="Vyhledávací box" + > + <button + class="text-lg bg-black text-white px-5 py-2 duration-100 h-10 w-12 min-h-0 min-w-0 xl:h-14 xl:w-14 hover:bg-white hover:text-black" + aria-label="Vyhledat" + > + <i class="ico--search"></i> + </button> + </div> +{% endblock %} diff --git a/lectures/templates/lectures/view_search_results.html b/lectures/templates/lectures/view_search_results.html new file mode 100644 index 0000000..7bb34be --- /dev/null +++ b/lectures/templates/lectures/view_search_results.html @@ -0,0 +1,39 @@ +{% extends "shared/includes/base.html" %} +{% load render_bundle from webpack_loader %} +{% load markdownify static %} + +{% block content %} + {% render_bundle "view_group_lectures" %} + + {% include "shared/includes/double_heading.html" with heading=header_name subheading=header_desc icon="ico--search" %} + + <form + class="flex flex-row justify-center mb-8" + method="get" + > + <input + name="q" + class="bg-gray-200 w-56 lg:w-80 h-10 px-4 text-lg xl:h-14 xl:px-5" + type="text" + placeholder="Hledat školení..." + value="{{ query }}" + aria-label="Vyhledávací box" + > + <button + class="text-lg bg-black text-white px-5 py-2 duration-100 h-10 w-12 min-h-0 min-w-0 xl:h-14 xl:w-14 hover:bg-white hover:text-black" + aria-label="Vyhledat" + > + <i class="ico--search"></i> + </button> + </form> + + <ul class="flex flex-col 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> + {% endif %} + </ul> +{% endblock %} diff --git a/lectures/urls.py b/lectures/urls.py index 4434de4..90954a4 100644 --- a/lectures/urls.py +++ b/lectures/urls.py @@ -15,4 +15,9 @@ urlpatterns = [ name="download_material_file", ), path("lectures/<int:lecture_id>/rsvp", views.rsvp_lecture, name="rsvp_lecture"), + path( + "search", + views.search, + name="search", + ), ] diff --git a/lectures/views.py b/lectures/views.py index 87bc580..06ddf8a 100644 --- a/lectures/views.py +++ b/lectures/views.py @@ -61,20 +61,19 @@ def generate_auth_redirect(request) -> HttpResponseRedirect: ) -def get_lecture(request, id: int) -> tuple: - lecture = ( - get_objects_for_user(request.user, "lectures.view_lecture") - .filter(id=id) - .first() - ) +def get_lectures(request, filter=None, get_exceptions: bool = True) -> tuple: + lectures = get_objects_for_user(request.user, "lectures.view_lecture") - if lecture is None: + if filter is not None: + lectures = lectures.filter(filter) + + if get_exceptions and not lectures.exists(): 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(id__in=(LectureGroup.objects.filter(lectures__in=lectures))) & ( ( models.Q(user_groups__in=request.user.groups.all()) @@ -87,12 +86,16 @@ def get_lecture(request, id: int) -> tuple: .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) + if get_exceptions: + 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 + if get_exceptions: + return True, lectures + else: + return lectures def view_groups(request): @@ -256,21 +259,27 @@ def view_group_lectures(request, group_id: int): def view_lecture(request, lecture_id: int): - is_successful, lecture = get_lecture(request, lecture_id) + is_successful, lecture = get_lectures(request, models.Q(id=lecture_id)) if not is_successful: return lecture + lecture = lecture.first() + related_group_id = request.GET.get("related_group_id") - if related_group_id is not None and not ( - get_objects_for_user(request.user, "lectures.view_lecture") - .filter(id=related_group_id) - .exists() - ): - # Ignore the wrong part of the URL and move on, don't raise exceptions - # just because of the related_group_id being wrong. - related_group_id = None + if related_group_id not in ("", None): + if not str(related_group_id).isnumeric(): + raise HTTPExceptions.BAD_REQUEST + + if not ( + get_objects_for_user(request.user, "lectures.view_lecture") + .filter(id=related_group_id) + .exists() + ): + # Ignore the wrong part of the URL and move on, don't raise exceptions + # just because of the related_group_id being wrong. + related_group_id = None return render( request, @@ -286,13 +295,84 @@ def view_lecture(request, lecture_id: int): ) +def search(request): + query = request.GET.get("q") + + if query: + queryset = get_lectures(request, get_exceptions=False) + + queryset = queryset.annotate( + name_lower=models.Func(models.F("name"), function="LOWER"), + description_lower=models.Func(models.F("description"), function="LOWER"), + ) + + formatted_query = query.lower() + + lectures = queryset.filter( + models.Q(name_lower__contains=formatted_query) + | models.Q(description_lower__contains=formatted_query) + ).all() + + return render( + request, + "lectures/view_search_results.html", + { + **get_base_context(request), + "title": f"Vyhledávání - {query}", + "description": "Vyhledávání školení v Pirátském e-Learningu.", + "header_name": "Výsledky vyhledávání", + "header_desc": f"„{query}“", + "query": query, + "lectures": lectures, + }, + ) + else: + return render( + request, + "lectures/view_search.html", + { + **get_base_context(request), + "title": "Vyhledávání", + "description": "Vyhledávání školení v Pirátském e-Learningu.", + "header_name": "Vyhledávání", + "header_desc": "školení", + }, + ) + + +def view_lector(request, id): + lector = get_object_or_404( + get_objects_for_user(request.user, "lectures.view_lecturelector"), id=id + ) + + lectures = get_lectures( + request, models.Q(id__in=lector.lectures.all()), get_exceptions=False + ) + + return render( + request, + "lectures/view_lector.html", + { + **get_base_context(request), + "title": lector.name, + "description": f"Informace o lektorovi - {lector.name}.", + "header_name": "Lektor", + "header_desc": lector.name, + "lector": lector, + "lectures": lectures, + }, + ) + + @require_POST def rsvp_lecture(request, lecture_id: int): - is_successful, lecture = get_lecture(request, lecture_id) + is_successful, lecture = get_lecture(request, models.Q(id=lecture_id)) if not is_successful: return lecture + lecture = lecture.first() + is_registered = request.POST.get("register", "false") == "true" if is_registered is request.user.get_lecture_registered( @@ -301,9 +381,9 @@ def rsvp_lecture(request, lecture_id: int): return JsonResponse({"success": False}) if is_registered: - request.user.rsvp_lectures.add(lecture) + lecture.rsvp_users.add(request.user) else: - request.user.rsvp_lectures.remove(lecture) + lecture.rsvp_users.remove(request.user) request.user.save() diff --git a/shared/templates/shared/includes/base.html b/shared/templates/shared/includes/base.html index 087da92..b07cf3f 100644 --- a/shared/templates/shared/includes/base.html +++ b/shared/templates/shared/includes/base.html @@ -85,6 +85,16 @@ </a> </li> {% endif %} + + <li class="navbar-menu__item"> + <a + href="{% url "lectures:search" %}" + data-href="{% url "lectures:search" %}" + class="navbar-menu__link flex items-center gap-2" + > + <i class="ico--search text-sm"></i>Hledat + </a> + </li> </ul> </div> diff --git a/users/admin.py b/users/admin.py index ecd2efa..128fc42 100644 --- a/users/admin.py +++ b/users/admin.py @@ -6,7 +6,7 @@ from .models import User class UserAdmin(MarkdownxGuardedModelAdmin): - autocomplete_fields = ("rsvp_lectures",) + search_fields = ("first_name", "last_name", "email") admin.site.register(User, UserAdmin) diff --git a/users/migrations/0010_remove_user_rsvp_lectures.py b/users/migrations/0010_remove_user_rsvp_lectures.py new file mode 100644 index 0000000..c7a3d95 --- /dev/null +++ b/users/migrations/0010_remove_user_rsvp_lectures.py @@ -0,0 +1,16 @@ +# Generated by Django 4.1.4 on 2023-05-31 06:55 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ("users", "0009_alter_user_sso_username"), + ] + + operations = [ + migrations.RemoveField( + model_name="user", + name="rsvp_lectures", + ), + ] diff --git a/users/models.py b/users/models.py index 3557edc..49a91f1 100644 --- a/users/models.py +++ b/users/models.py @@ -30,13 +30,6 @@ class User(pirates_models.AbstractUser): verbose_name="E-mailová adresa", ) - rsvp_lectures = models.ManyToManyField( - "lectures.Lecture", - blank=True, - related_name="rsvp_users", - verbose_name="Zaregistrovaná školení", - ) - def set_unusable_password(self) -> None: # Purely for compatibility with Guardian pass @@ -52,9 +45,7 @@ class User(pirates_models.AbstractUser): 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() + return Lecture.objects.filter(id=lecture.id, rsvp_users=self).exists() class Meta: app_label = "users" -- GitLab