diff --git a/Dockerfile b/Dockerfile index 06326882456a9965f376e2deffcd09c81e9bfce0..60f88a048f9cc212b46c12221e83862a357a25a2 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 0000000000000000000000000000000000000000..c4e8e37b1b860903f368552c89d0c35f9d9f3060 --- /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 57647fd0cda15d39efea1686ccd8b3f3a3f3c868..a9e1052a208b882c0e4f4e01d88c271de5c184af 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 dd1de6c7b3d62f2b94987986d2bb79e86661487a..1b19d20c059eedde829399d54494c34faea4fc6f 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 0b66ae8e40870a113d940f89e32f3ef64fcc0363..4cbd826aefad8e01dc44d4b2e931049d4700df4b 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 4a97b2290cd2808664e37ff0783a1da376abd553..c57bf309a22832c6cd96683822464ca0faae07a5 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 495b84dcf2f6560916e18135442e732b18d6c43a..d7bda341d1473662c54e5c0f0753a53d96a47d56 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 3a72fc29c7ada2c915b6b9ef1323870869612732..d4928788ec945aac792d4eb6a3e7b9d110b8b810 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 4804234164d63173462798e1fdd7c95bd5c34c92..e85a7ca7befeee4287f78cbb54865b7cfdf7f3a6 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 3424b6f1343082dd9accc4fca87c71b463e976db..ed9a38ad116e390037f09dc71dbce86ebfb9c4ad 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 b3075f237b9fb4ebb68422eed0d415b4e9305c18..166f6b728b44adb2b12e4cdd9371b201a173a8c0 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 47a4f820c57c0febd13a30622772c2866446b13c..87603106396e168ebf0b8587c31ad302fcc37225 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: