From e53b830ec3d1567dbf532933ac9ba1f3e0f573f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Valenta?= <git@imaniti.org>
Date: Tue, 21 Mar 2023 00:35:58 +0100
Subject: [PATCH] handle permissions for files, UI changes

---
 Dockerfile                                    |  1 +
 .../0005_alter_contract_notes_and_more.py     | 29 +++++++++++++++++++
 contracts/models.py                           | 20 +++++++++++++
 .../templates/contracts/includes/tag.html     |  4 +--
 .../templates/contracts/view_contract.html    | 22 +++++++-------
 contracts/urls.py                             |  5 ++++
 contracts/views.py                            | 22 ++++++++++++--
 env.example                                   |  2 ++
 registry/settings/base.py                     | 14 +++++++--
 registry/urls.py                              | 10 +------
 requirements/base.txt                         |  3 +-
 users/models.py                               |  2 +-
 12 files changed, 106 insertions(+), 28 deletions(-)
 create mode 100644 contracts/migrations/0005_alter_contract_notes_and_more.py

diff --git a/Dockerfile b/Dockerfile
index 0632688..60f88a0 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -26,6 +26,7 @@ RUN DATABASE_URL=postgres://x/x \
     DEFAULT_CONTRACTEE_ZIP=x \
     DEFAULT_CONTRACTEE_DISTRICT=x \
     DEFAULT_CONTRACTEE_ICO_NUMBER=x \
+    DOWNLOADVIEW_BACKEND=x \
     python manage.py collectstatic --noinput --settings=registry.settings.production
 
 RUN bash -c "adduser --disabled-login --quiet --gecos app app &&  \
diff --git a/contracts/migrations/0005_alter_contract_notes_and_more.py b/contracts/migrations/0005_alter_contract_notes_and_more.py
new file mode 100644
index 0000000..c4e8e37
--- /dev/null
+++ b/contracts/migrations/0005_alter_contract_notes_and_more.py
@@ -0,0 +1,29 @@
+# Generated by Django 4.1.4 on 2023-03-20 16:11
+
+from django.db import migrations, models
+import markdownx.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('contracts', '0004_alter_contract_options_and_more'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='contract',
+            name='notes',
+            field=markdownx.models.MarkdownxField(blank=True, help_text='Poznámky jsou viditelné pro všechny, kteří mohou smlouvu spravovat a pro tajné čtenáře.', null=True, verbose_name='Poznámky'),
+        ),
+        migrations.AlterField(
+            model_name='contractee',
+            name='address_country',
+            field=models.CharField(default='Česká Republika', max_length=256, verbose_name='Země'),
+        ),
+        migrations.AlterField(
+            model_name='signee',
+            name='address_country',
+            field=models.CharField(default='Česká Republika', max_length=256, verbose_name='Země'),
+        ),
+    ]
diff --git a/contracts/models.py b/contracts/models.py
index 57647fd..a9e1052 100644
--- a/contracts/models.py
+++ b/contracts/models.py
@@ -1,7 +1,10 @@
+import typing
+
 from django.conf import settings
 from django.db import models
 from django.db.models.signals import post_save
 from django.dispatch import receiver
+from django.urls import reverse
 from markdownx.models import MarkdownxField
 
 from shared.models import NameStrMixin
@@ -433,6 +436,16 @@ class Contract(NameStrMixin, models.Model):
             ("view_confidential", "Zobrazit tajné informace"),
         )
 
+    @property
+    def primary_contract_url(self) -> typing.Union[None, str]:
+        if self.primary_contract is None:
+            return
+
+        return reverse(
+            "contracts:view_contract",
+            args=(self.primary_contract.id,),
+        )
+
     def get_public_files(self):
         return ContractFile.objects.filter(
             contract=self,
@@ -569,6 +582,13 @@ class ContractFile(NameStrMixin, models.Model):
         verbose_name="Soubory",
     )
 
+    @property
+    def protected_url(self) -> str:
+        return reverse(
+            "contracts:download_contract_file",
+            args=(self.id,),
+        )
+
     class Meta:
         app_label = "contracts"
 
diff --git a/contracts/templates/contracts/includes/tag.html b/contracts/templates/contracts/includes/tag.html
index dd1de6c..1b19d20 100644
--- a/contracts/templates/contracts/includes/tag.html
+++ b/contracts/templates/contracts/includes/tag.html
@@ -1,4 +1,4 @@
 <a
-    class="p-1.5 rounded-sm text-ellipsis whitespace-nowrap bg-gray-200 duration-100 hover:bg-gray-300 hover:no-underline"
+    class="flex gap-2 items-baseline w-min p-1.5 rounded-sm text-ellipsis whitespace-nowrap bg-gray-200 duration-100 hover:bg-gray-300 hover:no-underline"
     href="{{ url }}"
->{% if icon %}<i class="{{ icon }} mr-2"></i>{% endif %}{{ content }}</a> 
+>{% if icon %}<i class="{{ icon }}"></i>{% endif %}{{ content }}</a> 
diff --git a/contracts/templates/contracts/view_contract.html b/contracts/templates/contracts/view_contract.html
index 0b66ae8..4cbd826 100644
--- a/contracts/templates/contracts/view_contract.html
+++ b/contracts/templates/contracts/view_contract.html
@@ -2,7 +2,9 @@
 {% load subtract markdownify %}
 
 {% block content %}
-    <h1 class="head-alt-lg mb-10"><i class="ico--file-blank mr-4"></i>{{ contract.name }}</h1>
+    <h1 class="head-alt-lg mb-10">
+        <i class="ico--file-blank mr-4"></i>{{ contract.name }}
+    </h1>
 
     <table class="table table-auto w-full table--striped table--bordered mb-7">
         <tbody>
@@ -34,9 +36,7 @@
                 <tr>
                     <td class="w-1/5 !p-2.5">Primární smlouva</td>
                     <td class="w-4/5 !p-2.5">
-                        <a
-                            href="{% url "contracts:view_contract" contract.primary_contract.id %}"
-                        >{{ contract.primary_contract.name }}</a>
+                        {% include "contracts/includes/tag.html" with url=contract.primary_contract_url icon="ico--file-blank" content=contract.primary_contract.name %}
                     </td>
                 </tr>
             {% endif %}
@@ -70,7 +70,7 @@
                     <td class="w-4/5 !p-2.5">
                         {{ contract.cost_amount }} Kč
                         {% if contract.cost_unit != contract.CostUnits.TOTAL %}
-                            / {{ contract.get_cost_unit_display }}
+                            / {{ contract.get_cost_unit_display.lower }}
                         {% else %}
                             celkem
                         {% endif %}
@@ -84,8 +84,8 @@
                         {% if files|length != 0 %}
                             <ul class="flex gap-2">
                                 {% for file in files %}
-                                    <li class="flex">
-                                         {% include "contracts/includes/tag.html" with url=file.file.url icon="ico--attachment" content=file.name %}
+                                    <li>
+                                         {% include "contracts/includes/tag.html" with url=file.protected_url icon="ico--attachment" content=file.name %}
                                     </li>
                                 {% endfor %}
                             </ul>
@@ -112,8 +112,8 @@
                             {% if files|length != 0 %}
                                 <ul class="flex gap-2">
                                     {% for file in files %}
-                                        <li class="flex">
-                                            {% include "contracts/includes/tag.html" with url=file.file.url icon="ico--attachment" content=file.name %}
+                                        <li>
+                                            {% include "contracts/includes/tag.html" with url=file.protected_url icon="ico--attachment" content=file.name %}
                                         </li>
                                     {% endfor %}
                                 </ul>
@@ -218,7 +218,7 @@
                         {% if intents|length != 0 %}
                             <ul class="flex gap-2">
                                 {% for intent in intents %}
-                                    <li class="flex">
+                                    <li>
                                         {% include "contracts/includes/tag.html" with url=intent.url icon="ico--link" content=intent.name %}
                                     </li>
                                 {% endfor %}
@@ -247,7 +247,7 @@
                         <td class="w-4/5 !pl-2.5 !py-1 !pr-1">
                             <ul class="flex gap-2">
                                 {% for issue in issues %}
-                                    <li class="flex">
+                                    <li>
                                          {% include "contracts/includes/tag.html" with url="#TODO" icon="ico--warning" content=issue.name %}
                                     </li>
                                 {% endfor %}
diff --git a/contracts/urls.py b/contracts/urls.py
index 4a97b22..c57bf30 100644
--- a/contracts/urls.py
+++ b/contracts/urls.py
@@ -8,6 +8,11 @@ app_name = "contracts"
 urlpatterns = [
     path("", views.index, name="index"),
     path("contracts/<int:id>", views.view_contract, name="view_contract"),
+    path(
+        "contracts/files/<str:pk>",
+        views.ContractFileDownloadView.as_view(),
+        name="download_contract_file",
+    ),
     path(
         "contracts/autocomplete",
         dal.autocomplete.Select2QuerySetView.as_view(model=models.Contract),
diff --git a/contracts/views.py b/contracts/views.py
index 495b84d..d7bda34 100644
--- a/contracts/views.py
+++ b/contracts/views.py
@@ -1,11 +1,29 @@
 from django.conf import settings
 from django.core.paginator import Paginator
 from django.shortcuts import render
+from django_downloadview import ObjectDownloadView
 from guardian.shortcuts import get_objects_for_user
 
-from .models import Contract
+from .models import Contract, ContractFile
 
-# Create your views here.
+
+class ContractFileDownloadView(ObjectDownloadView):
+    model = ContractFile
+    file_field = "file"
+    attachment = False
+
+    def get_queryset(self, *args, **kwargs):
+        queryset = super().get_queryset(*args, **kwargs)
+
+        if not self.current_user.has_perm("contracts.view_confidential"):
+            queryset = queryset.filter(is_public=True)
+
+        return queryset
+
+    def get(self, request, *args, **kwargs):
+        self.current_user = request.user
+
+        return super().get(request, *args, **kwargs)
 
 
 def index(request):
diff --git a/env.example b/env.example
index 3a72fc2..d492878 100644
--- a/env.example
+++ b/env.example
@@ -15,3 +15,5 @@ DEFAULT_CONTRACTEE_STREET="Na Moráni 360/3"
 DEFAULT_CONTRACTEE_ZIP="128 00"
 DEFAULT_CONTRACTEE_DISTRICT="Praha 2"
 DEFAULT_CONTRACTEE_ICO_NUMBER="71339698"
+
+DOWNLOADVIEW_BACKEND=django_downloadview.nginx.XAccelRedirectMiddleware
diff --git a/registry/settings/base.py b/registry/settings/base.py
index 4804234..e85a7ca 100644
--- a/registry/settings/base.py
+++ b/registry/settings/base.py
@@ -79,6 +79,7 @@ MIDDLEWARE = [
     "django.middleware.clickjacking.XFrameOptionsMiddleware",
     "django_http_exceptions.middleware.ExceptionHandlerMiddleware",
     "django_http_exceptions.middleware.ThreadLocalRequestMiddleware",
+    #"django_downloadview.SmartDownloadMiddleware",
 ]
 
 ROOT_URLCONF = "registry.urls"
@@ -173,7 +174,7 @@ USE_TZ = True
 ## Static files (CSS, JavaScript, Images)
 # https://docs.djangoproject.com/en/4.0/howto/static-files/
 
-STATIC_URL = "static/"
+STATIC_URL = "/static/"
 
 WEBPACK_LOADER = {
     "DEFAULT": {
@@ -188,8 +189,17 @@ WEBPACK_LOADER = {
 
 ## Media files
 
-MEDIA_URL = "media/"
+MEDIA_URL = "/media/"
 
+DEFAULT_FILE_STORAGE = "django_downloadview.storage.SignedFileSystemStorage"
+
+#DOWNLOADVIEW_BACKEND = env.str("DOWNLOADVIEW_BACKEND")
+#DOWNLOADVIEW_RULES = [
+    #{
+        #"source_url": "/media/",
+        #"destination_url": "/media-nginx-optimized/",
+    #},
+#]
 
 ## Server
 
diff --git a/registry/urls.py b/registry/urls.py
index 3424b6f..ed9a38a 100644
--- a/registry/urls.py
+++ b/registry/urls.py
@@ -15,6 +15,7 @@ Including another URLconf
 """
 
 from django.conf import settings
+from django.conf.urls.static import static
 from django.contrib import admin
 from django.urls import include, path, re_path
 from django.views.static import serve
@@ -29,12 +30,3 @@ urlpatterns = [
     path("oidc/", include("oidc.urls")),
     path("admin/", admin.site.urls),
 ] + pirates_urlpatterns
-
-if settings.DEBUG:
-    urlpatterns.append(
-        re_path(
-            r"^media/(?P<path>.*)$",
-            serve,
-            {"document_root": settings.MEDIA_ROOT}
-        ),
-    )
diff --git a/requirements/base.txt b/requirements/base.txt
index b3075f2..166f6b7 100644
--- a/requirements/base.txt
+++ b/requirements/base.txt
@@ -5,7 +5,8 @@ django-admin-interface==0.24.2
 django-admin-rangefilter==0.9.0
 django-autocomplete-light==3.9.4
 django-database-url==1.0.3
-django-fieldsets-with-inlines==0.6 
+django-downloadview==2.3.0 
+django-fieldsets-with-inlines==0.6
 django-import-export==3.1.0
 djhacker==0.2.3
 django-ordered-model==3.7.1
diff --git a/users/models.py b/users/models.py
index 47a4f82..8760310 100644
--- a/users/models.py
+++ b/users/models.py
@@ -36,7 +36,7 @@ class User(pirates_models.AbstractUser):
         from contracts.models import Contract
 
         return Contract.objects.filter(
-            approval_state=Contract.ApprovalStates.NO
+            is_approved=False
         ).count()
 
     class Meta:
-- 
GitLab