From 737198f437174147c3eb586d1c531963b4a38a43 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Valenta?= <git@imaniti.org>
Date: Fri, 26 May 2023 18:15:43 +0200
Subject: [PATCH] authenticated file views

---
 lectures/models.py        | 62 +++++++++++++++++++++++++++++++++++++--
 lectures/urls.py          |  5 ++++
 lectures/views.py         | 43 +++++++++++++++++++++++++--
 media_server/__init__.py  |  0
 media_server/admin.py     |  3 ++
 media_server/apps.py      |  6 ++++
 media_server/models.py    |  3 ++
 media_server/tests.py     |  3 ++
 media_server/urls.py      | 12 ++++++++
 media_server/views.py     | 27 +++++++++++++++++
 requirements/base.txt     |  1 +
 ucebnice/settings/base.py |  1 +
 ucebnice/urls.py          |  1 +
 13 files changed, 162 insertions(+), 5 deletions(-)
 create mode 100644 media_server/__init__.py
 create mode 100644 media_server/admin.py
 create mode 100644 media_server/apps.py
 create mode 100644 media_server/models.py
 create mode 100644 media_server/tests.py
 create mode 100644 media_server/urls.py
 create mode 100644 media_server/views.py

diff --git a/lectures/models.py b/lectures/models.py
index 02314de..66a2d65 100644
--- a/lectures/models.py
+++ b/lectures/models.py
@@ -1,9 +1,15 @@
-from datetime import timedelta
+import mimetypes
+import uuid
+
+from datetime import datetime, timedelta
 
 from django.contrib.auth.models import Group
 from django.core.exceptions import ValidationError
 from django.db import models
+from django.db.models.fields.files import FieldFile
+from django.urls import reverse
 from django.utils import timezone
+from django.utils.safestring import mark_safe
 from markdownx.models import MarkdownxField
 
 from shared.models import NameStrMixin
@@ -35,7 +41,7 @@ class LectureGroup(NameStrMixin, models.Model):
         Group,
         blank=True,
         verbose_name="Uživatelské skupiny",
-        help_text="Pokud žádné nedefinuješ, školení ve skupině jsou dostupné všem.",
+        help_text="Pokud žádné nedefinuješ, školení ve skupině jsou dostupná všem.",
     )
 
     class Meta:
@@ -112,6 +118,7 @@ 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>.")
     )
 
     username = models.CharField(
@@ -148,6 +155,7 @@ 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>.")
     )
 
     class Meta:
@@ -155,6 +163,46 @@ class LectureRecording(NameStrMixin, models.Model):
         verbose_name_plural = "Nahrávky"
 
 
+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}"
+    )
+
+
+class LectureMaterialFileProxy(FieldFile):
+    @property
+    def url(self) -> str:
+        return reverse(
+            "lectures:download_material_file",
+            args=(str(self.instance.id),)
+        )
+
+
+class LectureMaterialFileField(models.FileField):
+    attr_class = LectureMaterialFileProxy
+
+
 class LectureMaterial(NameStrMixin, models.Model):
     lecture = models.ForeignKey(
         "Lecture",
@@ -176,13 +224,21 @@ class LectureMaterial(NameStrMixin, models.Model):
         help_text="Pokud máš zadaný odkaz, nemůžeš definovat soubor.",
     )
 
-    file = models.FileField(
+    file = LectureMaterialFileField(
         blank=True,
         null=True,
+        upload_to=get_lecture_material_file_location,
         verbose_name="Soubor",
         help_text="Pokud máš vložený soubor, nemůžeš definovat odkaz.",
     )
 
+    @property
+    def protected_file_url(self) -> str:
+        return reverse(
+            "lectures:download_material_file",
+            args=(self.id,),
+        )
+
     def clean(self) -> None:
         BOTH_FILE_AND_LINK_DEFINED = (
             "Definuj prosím pouze odkaz, nebo soubor. Nemůžeš mít oboje najednou."
diff --git a/lectures/urls.py b/lectures/urls.py
index 27da58f..5b082c4 100644
--- a/lectures/urls.py
+++ b/lectures/urls.py
@@ -9,4 +9,9 @@ urlpatterns = [
         "groups/<int:group_id>", views.view_group_lectures, name="view_group_lectures"
     ),
     path("lectures/<int:lecture_id>", views.view_lecture, name="view_lecture"),
+    path(
+        "lectures/materials/<str:pk>/file",
+        views.LectureMaterialFileDownloadView.as_view(),
+        name="download_material_file",
+    )
 ]
diff --git a/lectures/views.py b/lectures/views.py
index 673ccba..722b735 100644
--- a/lectures/views.py
+++ b/lectures/views.py
@@ -1,4 +1,5 @@
 # import calendar
+# import locale
 import json
 from datetime import datetime
 from itertools import chain
@@ -8,12 +9,50 @@ from django.db import models
 from django.shortcuts import get_object_or_404, render
 from django.urls import reverse
 from django.utils import timezone
+from django_downloadview import ObjectDownloadView
 from django_http_exceptions import HTTPExceptions
 from guardian.shortcuts import get_objects_for_user
 
-from .models import Lecture, LectureGroup
+from .models import Lecture, LectureGroup, LectureMaterial
+
+
+class LectureMaterialFileDownloadView(ObjectDownloadView):
+    model = LectureMaterial
+    file_field = "file"
+    attachment = False
+
+    def get_queryset(self, *args, **kwargs):
+        queryset = (
+            super()
+            .get_queryset(*args, **kwargs)
+            .filter(
+                lecture__groups__in=(
+                    get_objects_for_user(
+                        self.current_user,
+                        "lectures.view_lecturegroup"
+                    ).
+                    filter(
+                        models.Q(user_groups__in=self.current_user.groups.all())
+                        | models.Q(user_groups=None)
+                    )
+                )
+            )
+        )
 
-# import locale
+        print(
+            queryset,
+            get_objects_for_user(
+                self.current_user,
+                "lectures.view_lecturegroup"
+            )
+        )
+
+        return queryset
+
+    def get(self, request, *args, **kwargs):
+        self.current_user = request.user
+
+        return super().get(request, *args, **kwargs)
 
 
 def get_base_context(request) -> dict:
diff --git a/media_server/__init__.py b/media_server/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/media_server/admin.py b/media_server/admin.py
new file mode 100644
index 0000000..8c38f3f
--- /dev/null
+++ b/media_server/admin.py
@@ -0,0 +1,3 @@
+from django.contrib import admin
+
+# Register your models here.
diff --git a/media_server/apps.py b/media_server/apps.py
new file mode 100644
index 0000000..4aff1ea
--- /dev/null
+++ b/media_server/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class MediaServerConfig(AppConfig):
+    default_auto_field = "django.db.models.BigAutoField"
+    name = "media_server"
diff --git a/media_server/models.py b/media_server/models.py
new file mode 100644
index 0000000..71a8362
--- /dev/null
+++ b/media_server/models.py
@@ -0,0 +1,3 @@
+from django.db import models
+
+# Create your models here.
diff --git a/media_server/tests.py b/media_server/tests.py
new file mode 100644
index 0000000..7ce503c
--- /dev/null
+++ b/media_server/tests.py
@@ -0,0 +1,3 @@
+from django.test import TestCase
+
+# Create your tests here.
diff --git a/media_server/urls.py b/media_server/urls.py
new file mode 100644
index 0000000..23bb64a
--- /dev/null
+++ b/media_server/urls.py
@@ -0,0 +1,12 @@
+from django.urls import path
+
+from . import views
+
+app_name = "media_server"
+urlpatterns = [
+    path(
+        "<path:path>",
+        views.view_media,
+        name="view_media",
+    ),
+]
diff --git a/media_server/views.py b/media_server/views.py
new file mode 100644
index 0000000..370803d
--- /dev/null
+++ b/media_server/views.py
@@ -0,0 +1,27 @@
+import os
+
+from django.core.files.storage import FileSystemStorage
+from django_downloadview import StorageDownloadView
+from django_http_exceptions import HTTPExceptions
+
+# Create your views here.
+
+storage = FileSystemStorage()
+
+
+class MediaView(StorageDownloadView):
+    attachment = False
+
+    def get_path(self, *args, **kwargs) -> str:
+        path = super().get_path(*args, **kwargs)
+
+        # Make sure path is clean
+        path = os.path.normpath(path)
+
+        if path.startswith("_"):  # Private path
+            raise HTTPExceptions.NOT_FOUND
+
+        return path
+
+
+view_media = MediaView.as_view(storage=storage)
diff --git a/requirements/base.txt b/requirements/base.txt
index 8fb7229..97e5804 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -3,6 +3,7 @@ django-admin-index==2.0.2
 django-admin-interface==0.24.2
 django-database-url==1.0.3
 django-dbsettings==1.3.0
+django-downloadview==2.3.0
 django-markdownx==4.0.0b1
 django-ordered-model==3.7.1
 psycopg2-binary==2.9.5
diff --git a/ucebnice/settings/base.py b/ucebnice/settings/base.py
index 0e22973..adf529f 100644
--- a/ucebnice/settings/base.py
+++ b/ucebnice/settings/base.py
@@ -57,6 +57,7 @@ INSTALLED_APPS = [
     "pirates",
     "webpack_loader",
     "shared",
+    "media_server",
     "oidc",
     "users",
     "lectures",
diff --git a/ucebnice/urls.py b/ucebnice/urls.py
index 4f40d78..8ed67ab 100644
--- a/ucebnice/urls.py
+++ b/ucebnice/urls.py
@@ -27,5 +27,6 @@ urlpatterns = [
     path("", include("lectures.urls")),
     path("markdownx/", include("markdownx.urls")),
     path("settings/", include("dbsettings.urls")),
+    path("media/", include("media_server.urls")),
     path("admin/", admin.site.urls),
 ] + pirates_urlpatterns
-- 
GitLab