Skip to content
Snippets Groups Projects
Commit bcb5472d authored by Tomáš Valenta's avatar Tomáš Valenta
Browse files

add lector views, searching, improve db structure, custom lector avatars / descriptions

parent d1a6803b
No related branches found
No related tags found
No related merge requests found
Pipeline #13077 passed
Showing
with 431 additions and 90 deletions
......@@ -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)
# 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",
),
),
]
# 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é",
),
),
]
# 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",
),
),
]
......@@ -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):
......
{% 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">
......
{% 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 %}
......@@ -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>
......
{% 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 %}
{% 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 %}
......@@ -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",
),
]
......@@ -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()
......
......@@ -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>
......
......@@ -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)
# 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",
),
]
......@@ -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"
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment