From f64a11cb50cb36dfc12a7c30b908bd09fd4564b9 Mon Sep 17 00:00:00 2001 From: mipel Date: Wed, 1 Sep 2021 16:24:49 +0200 Subject: [PATCH 1/8] #18 File upload in certain folders * refactors documents and file upload to be distributed into different subfolders, depending on the type of document (InterventionDocument, RevocationDocument, ...) * refactors Document model into AbstractDocument * subclasses RevocationDocument, InterventionDocument, COmpensationDocument, EmaDocument and EcoAccountDocument from AbstractDocument to provide proper functionality for each * adds new specialized routes for each new document type (opening, removing) * drops generic get and remove routes for documents --- compensation/account_urls.py | 3 +- compensation/comp_urls.py | 3 +- compensation/models.py | 32 ++++++++++- .../compensation/includes/documents.html | 4 +- .../eco_account/includes/documents.html | 4 +- compensation/views/compensation_views.py | 40 ++++++++++++- compensation/views/eco_account_views.py | 40 ++++++++++++- ema/models.py | 19 ++++++- .../ema/detail/includes/documents.html | 4 +- ema/urls.py | 2 + ema/views.py | 40 ++++++++++++- intervention/admin.py | 6 +- intervention/forms.py | 22 ++++--- intervention/models.py | 48 ++++++++++++---- .../detail/includes/documents.html | 4 +- .../detail/includes/revocation.html | 10 ++-- intervention/urls.py | 9 ++- intervention/views.py | 57 ++++++++++++++++++- konova/admin.py | 5 +- konova/forms.py | 27 ++++++--- konova/models.py | 52 ++++++++++++++++- konova/settings.py | 15 ++++- konova/urls.py | 6 +- konova/utils/documents.py | 50 ++++++++++++++++ konova/views.py | 44 +------------- 25 files changed, 437 insertions(+), 109 deletions(-) create mode 100644 konova/utils/documents.py diff --git a/compensation/account_urls.py b/compensation/account_urls.py index 5daa6c87..511ee601 100644 --- a/compensation/account_urls.py +++ b/compensation/account_urls.py @@ -21,8 +21,9 @@ urlpatterns = [ path('/deadline/new', deadline_new_view, name="acc-new-deadline"), # Documents - # Document remove route can be found in konova/urls.py path('/document/new/', new_document_view, name='acc-new-doc'), + path('document/', get_document_view, name='acc-get-doc'), + path('document//remove/', remove_document_view, name='acc-remove-doc'), # Eco-account deductions path('/remove/', deduction_remove_view, name='deduction-remove'), diff --git a/compensation/comp_urls.py b/compensation/comp_urls.py index 1e79319b..84979d89 100644 --- a/compensation/comp_urls.py +++ b/compensation/comp_urls.py @@ -21,8 +21,9 @@ urlpatterns = [ path('/deadline/new', deadline_new_view, name="new-deadline"), # Documents - # Document remove route can be found in konova/urls.py path('/document/new/', new_document_view, name='new-doc'), + path('document/', get_document_view, name='get-doc'), + path('document//remove/', remove_document_view, name='remove-doc'), # Generic state routes path('state//remove', state_remove_view, name='state-remove'), diff --git a/compensation/models.py b/compensation/models.py index 556f1000..c2b47d35 100644 --- a/compensation/models.py +++ b/compensation/models.py @@ -14,7 +14,8 @@ from django.utils.translation import gettext_lazy as _ from codelist.models import KonovaCode from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID from intervention.models import Intervention, ResponsibilityData -from konova.models import BaseObject, BaseResource, Geometry, UuidModel +from konova.models import BaseObject, BaseResource, Geometry, UuidModel, AbstractDocument, \ + generate_document_file_upload_path from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from user.models import UserActionLogEntry @@ -137,7 +138,6 @@ class AbstractCompensation(BaseObject): deadlines = models.ManyToManyField("konova.Deadline", blank=True, related_name="+") geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) - documents = models.ManyToManyField("konova.Document", blank=True) class Meta: abstract = True @@ -198,6 +198,20 @@ class Compensation(AbstractCompensation): ) +class CompensationDocument(AbstractDocument): + """ + Specializes document upload for revocations with certain path + """ + instance = models.ForeignKey( + Compensation, + on_delete=models.CASCADE, + related_name="documents", + ) + file = models.FileField( + upload_to=generate_document_file_upload_path + ) + + class EcoAccount(AbstractCompensation): """ An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled @@ -299,6 +313,20 @@ class EcoAccount(AbstractCompensation): return ret_msgs +class EcoAccountDocument(AbstractDocument): + """ + Specializes document upload for revocations with certain path + """ + instance = models.ForeignKey( + EcoAccount, + on_delete=models.CASCADE, + related_name="documents", + ) + file = models.FileField( + upload_to=generate_document_file_upload_path + ) + + class EcoAccountDeduction(BaseResource): """ A deduction object for eco accounts diff --git a/compensation/templates/compensation/detail/compensation/includes/documents.html b/compensation/templates/compensation/detail/compensation/includes/documents.html index 3801f7c8..3548e940 100644 --- a/compensation/templates/compensation/detail/compensation/includes/documents.html +++ b/compensation/templates/compensation/detail/compensation/includes/documents.html @@ -39,14 +39,14 @@ {% for doc in obj.documents.all %} - + {{ doc.title }} {{ doc.comment }} {% if is_default_member and has_access %} - {% endif %} diff --git a/compensation/templates/compensation/detail/eco_account/includes/documents.html b/compensation/templates/compensation/detail/eco_account/includes/documents.html index 0c4b9ddd..bd371df2 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/documents.html +++ b/compensation/templates/compensation/detail/eco_account/includes/documents.html @@ -39,14 +39,14 @@ {% for doc in obj.documents.all %} - + {{ doc.title }} {{ doc.comment }} {% if is_default_member and has_access %} - {% endif %} diff --git a/compensation/views/compensation_views.py b/compensation/views/compensation_views.py index 0403af29..08091075 100644 --- a/compensation/views/compensation_views.py +++ b/compensation/views/compensation_views.py @@ -5,11 +5,12 @@ from django.shortcuts import render, get_object_or_404 from django.utils.translation import gettext_lazy as _ from compensation.forms import NewStateModalForm, NewDeadlineModalForm, NewActionModalForm -from compensation.models import Compensation, CompensationState, CompensationAction +from compensation.models import Compensation, CompensationState, CompensationAction, CompensationDocument from compensation.tables import CompensationTable from konova.contexts import BaseContext from konova.decorators import * from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm +from konova.utils.documents import get_document, remove_document from konova.utils.user_checks import in_group @@ -163,6 +164,43 @@ def new_document_view(request: HttpRequest, id: str): ) +@login_required +def get_document_view(request: HttpRequest, doc_id: str): + """ Returns the document as downloadable file + + Wraps the generic document fetcher function from konova.utils. + + Args: + request (HttpRequest): The incoming request + doc_id (str): The document id + + Returns: + + """ + doc = get_object_or_404(CompensationDocument, id=doc_id) + return get_document(doc) + + +@login_required +def remove_document_view(request: HttpRequest, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + doc_id (str): The document id + + Returns: + + """ + doc = get_object_or_404(CompensationDocument, id=doc_id) + return remove_document( + request, + doc + ) + + @login_required def state_new_view(request: HttpRequest, id: str): """ Renders a form for adding new states for a compensation diff --git a/compensation/views/eco_account_views.py b/compensation/views/eco_account_views.py index 86677ff5..c53f5349 100644 --- a/compensation/views/eco_account_views.py +++ b/compensation/views/eco_account_views.py @@ -14,13 +14,14 @@ from django.http import HttpRequest, Http404 from django.shortcuts import render, get_object_or_404 from compensation.forms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm -from compensation.models import EcoAccount +from compensation.models import EcoAccount, EcoAccountDocument from compensation.tables import EcoAccountTable from intervention.forms import NewDeductionForm from konova.contexts import BaseContext from konova.decorators import any_group_check, default_group_required, conservation_office_group_required from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm, RecordModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP +from konova.utils.documents import get_document, remove_document from konova.utils.user_checks import in_group @@ -289,6 +290,43 @@ def new_document_view(request: HttpRequest, id: str): ) +@login_required +def get_document_view(request: HttpRequest, doc_id: str): + """ Returns the document as downloadable file + + Wraps the generic document fetcher function from konova.utils. + + Args: + request (HttpRequest): The incoming request + doc_id (str): The document id + + Returns: + + """ + doc = get_object_or_404(EcoAccountDocument, id=doc_id) + return get_document(doc) + + +@login_required +def remove_document_view(request: HttpRequest, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + doc_id (str): The document id + + Returns: + + """ + doc = get_object_or_404(EcoAccountDocument, id=doc_id) + return remove_document( + request, + doc + ) + + @login_required @default_group_required def new_deduction_view(request: HttpRequest, id: str): diff --git a/ema/models.py b/ema/models.py index aab3a86f..c57432bb 100644 --- a/ema/models.py +++ b/ema/models.py @@ -2,7 +2,8 @@ from django.contrib.auth.models import User from django.db import models from compensation.models import AbstractCompensation -from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE +from konova.models import AbstractDocument, generate_document_file_upload_path +from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE, EMA_DOC_PATH from user.models import UserActionLogEntry @@ -83,4 +84,18 @@ class Ema(AbstractCompensation): # ToDo: Add check methods! - return ret_msgs \ No newline at end of file + return ret_msgs + + +class EmaDocument(AbstractDocument): + """ + Specializes document upload for ema with certain path + """ + instance = models.ForeignKey( + Ema, + on_delete=models.CASCADE, + related_name="documents", + ) + file = models.FileField( + upload_to=generate_document_file_upload_path + ) \ No newline at end of file diff --git a/ema/templates/ema/detail/includes/documents.html b/ema/templates/ema/detail/includes/documents.html index 0670bbe7..96a24a67 100644 --- a/ema/templates/ema/detail/includes/documents.html +++ b/ema/templates/ema/detail/includes/documents.html @@ -39,14 +39,14 @@ {% for doc in obj.documents.all %} - + {{ doc.title }} {{ doc.comment }} {% if is_default_member and has_access %} - {% endif %} diff --git a/ema/urls.py b/ema/urls.py index c3f5bd31..860d863b 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -24,6 +24,8 @@ urlpatterns = [ # Documents # Document remove route can be found in konova/urls.py path('/document/new/', document_new_view, name='new-doc'), + path('document/', get_document_view, name='get-doc'), + path('document//remove/', remove_document_view, name='remove-doc'), # Generic state routes path('state//remove', state_remove_view, name='state-remove'), diff --git a/ema/views.py b/ema/views.py index df347deb..30520ca8 100644 --- a/ema/views.py +++ b/ema/views.py @@ -10,9 +10,10 @@ from compensation.forms import NewStateModalForm, NewActionModalForm, NewDeadlin from ema.tables import EmaTable from konova.contexts import BaseContext from konova.decorators import conservation_office_group_required -from ema.models import Ema +from ema.models import Ema, EmaDocument from konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP +from konova.utils.documents import get_document, remove_document from konova.utils.user_checks import in_group @@ -250,6 +251,43 @@ def document_new_view(request: HttpRequest, id: str): ) +@login_required +def get_document_view(request: HttpRequest, doc_id: str): + """ Returns the document as downloadable file + + Wraps the generic document fetcher function from konova.utils. + + Args: + request (HttpRequest): The incoming request + doc_id (str): The document id + + Returns: + + """ + doc = get_object_or_404(EmaDocument, id=doc_id) + return get_document(doc) + + +@login_required +def remove_document_view(request: HttpRequest, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + doc_id (str): The document id + + Returns: + + """ + doc = get_object_or_404(EmaDocument, id=doc_id) + return remove_document( + request, + doc + ) + + @login_required def state_remove_view(request: HttpRequest, id: str): """ Renders a form for removing an EMA state diff --git a/intervention/admin.py b/intervention/admin.py index 2046935e..cf49bc39 100644 --- a/intervention/admin.py +++ b/intervention/admin.py @@ -1,6 +1,7 @@ from django.contrib import admin -from intervention.models import Intervention, ResponsibilityData, LegalData, Revocation +from intervention.models import Intervention, ResponsibilityData, LegalData, Revocation, InterventionDocument +from konova.admin import AbstractDocumentAdmin class InterventionAdmin(admin.ModelAdmin): @@ -12,6 +13,8 @@ class InterventionAdmin(admin.ModelAdmin): "deleted", ] +class InterventionDocumentAdmin(AbstractDocumentAdmin): + pass class ResponsibilityAdmin(admin.ModelAdmin): list_display = [ @@ -47,3 +50,4 @@ admin.site.register(Intervention, InterventionAdmin) admin.site.register(ResponsibilityData, ResponsibilityAdmin) admin.site.register(LegalData, LegalAdmin) admin.site.register(Revocation, RevocationAdmin) +admin.site.register(InterventionDocument, InterventionDocumentAdmin) diff --git a/intervention/forms.py b/intervention/forms.py index 25ff7d36..508dbacb 100644 --- a/intervention/forms.py +++ b/intervention/forms.py @@ -15,9 +15,8 @@ from django.urls import reverse from django.utils.translation import gettext_lazy as _ from compensation.models import EcoAccountDeduction, EcoAccount -from intervention.models import Intervention, Revocation +from intervention.models import Intervention, Revocation, RevocationDocument from konova.forms import BaseForm, BaseModalForm -from konova.models import Document from konova.settings import DEFAULT_LAT, DEFAULT_LON, DEFAULT_ZOOM, ZB_GROUP, ETS_GROUP from konova.utils.messenger import Messenger from konova.utils.user_checks import in_group @@ -372,19 +371,9 @@ class NewRevocationForm(BaseModalForm): user=self.user, action=UserAction.EDITED ) - if self.cleaned_data["file"]: - document = Document.objects.create( - title="revocation_of_{}".format(self.instance.identifier), - date_of_creation=self.cleaned_data["date"], - comment=self.cleaned_data["comment"], - file=self.cleaned_data["file"], - ) - else: - document = None revocation = Revocation.objects.create( date=self.cleaned_data["date"], comment=self.cleaned_data["comment"], - document=document, created=created_action, ) self.instance.modified = edited_action @@ -392,6 +381,15 @@ class NewRevocationForm(BaseModalForm): self.instance.log.add(edited_action) self.instance.legal.revocation = revocation self.instance.legal.save() + + if self.cleaned_data["file"]: + RevocationDocument.objects.create( + title="revocation_of_{}".format(self.instance.identifier), + date_of_creation=self.cleaned_data["date"], + comment=self.cleaned_data["comment"], + file=self.cleaned_data["file"], + instance=revocation + ) return revocation diff --git a/intervention/models.py b/intervention/models.py index a62da795..e5ad1687 100644 --- a/intervention/models.py +++ b/intervention/models.py @@ -13,11 +13,11 @@ from django.utils.translation import gettext_lazy as _ from codelist.models import KonovaCode from codelist.settings import CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_LAW_ID, \ CODELIST_PROCESS_TYPE_ID -from konova.models import BaseObject, Geometry, UuidModel, BaseResource +from konova.models import BaseObject, Geometry, UuidModel, BaseResource, AbstractDocument, \ + generate_document_file_upload_path from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT from konova.utils import generators -from organisation.models import Organisation from user.models import UserActionLogEntry @@ -68,15 +68,32 @@ class Revocation(BaseResource): """ date = models.DateField(null=True, blank=True, help_text="Revocation from") comment = models.TextField(null=True, blank=True) - document = models.ForeignKey("konova.Document", blank=True, null=True, on_delete=models.SET_NULL) - def delete(self): + def delete(self, *args, **kwargs): # Make sure related objects are being removed as well if self.document: - self.document.delete() + self.document.delete(*args, **kwargs) super().delete() +class RevocationDocument(AbstractDocument): + """ + Specializes document upload for revocations with certain path + """ + instance = models.OneToOneField( + Revocation, + on_delete=models.CASCADE, + related_name="document", + ) + file = models.FileField( + upload_to=generate_document_file_upload_path + ) + + @property + def intervention(self): + return self.instance.legaldata.intervention + + class LegalData(UuidModel): """ Holds intervention legal data such as important dates, laws or responsible handler @@ -112,7 +129,7 @@ class LegalData(UuidModel): } ) - revocation = models.ForeignKey(Revocation, null=True, blank=True, help_text="Refers to 'Widerspruch am'", on_delete=models.SET_NULL) + revocation = models.OneToOneField(Revocation, null=True, blank=True, help_text="Refers to 'Widerspruch am'", on_delete=models.SET_NULL) def __str__(self): return "{} | {} | {}".format( @@ -131,7 +148,6 @@ class Intervention(BaseObject): on_delete=models.SET_NULL, null=True, blank=True, - related_name='+', help_text="Holds data on responsible organizations ('Zulassungsbehörde', 'Eintragungsstelle')" ) legal = models.OneToOneField( @@ -139,11 +155,9 @@ class Intervention(BaseObject): on_delete=models.SET_NULL, null=True, blank=True, - related_name='+', help_text="Holds data on legal dates or law" ) geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) - documents = models.ManyToManyField("konova.Document", blank=True) # Checks - Refers to "Genehmigen" but optional checked = models.OneToOneField( @@ -307,4 +321,18 @@ class Intervention(BaseObject): value = localtime(value) on = value.strftime(DEFAULT_DATE_TIME_FORMAT) tooltip = _("Recorded on {} by {}").format(on, self.recorded.user) - return tooltip \ No newline at end of file + return tooltip + + +class InterventionDocument(AbstractDocument): + """ + Specializes document upload for an intervention with certain path + """ + instance = models.ForeignKey( + Intervention, + on_delete=models.CASCADE, + related_name="documents", + ) + file = models.FileField( + upload_to=generate_document_file_upload_path + ) \ No newline at end of file diff --git a/intervention/templates/intervention/detail/includes/documents.html b/intervention/templates/intervention/detail/includes/documents.html index c0bea2b0..54972bf5 100644 --- a/intervention/templates/intervention/detail/includes/documents.html +++ b/intervention/templates/intervention/detail/includes/documents.html @@ -39,14 +39,14 @@ {% for doc in intervention.documents.all %} - + {{ doc.title }} {{ doc.comment }} {% if is_default_member and has_access %} - {% endif %} diff --git a/intervention/templates/intervention/detail/includes/revocation.html b/intervention/templates/intervention/detail/includes/revocation.html index 332b0d76..10a4c44e 100644 --- a/intervention/templates/intervention/detail/includes/revocation.html +++ b/intervention/templates/intervention/detail/includes/revocation.html @@ -31,10 +31,10 @@ {% trans 'From' context 'Revocation' %} - {% trans 'Comment' %} + {% trans 'Document' %} - {% trans 'Document' %} + {% trans 'Comment' %} {% trans 'Action' %} @@ -48,14 +48,14 @@ {{ rev.date }} - {{ rev.comment }} {% if rev.document %} - - {{ rev.document.file }} + + {% trans 'Revocation' %} {% endif %} + {{ rev.comment }} {% if is_default_member and has_access %}