#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
This commit is contained in:
mipel
2021-09-01 16:24:49 +02:00
parent 8eb9d4f9ee
commit 3c9d73533f
25 changed files with 437 additions and 109 deletions

View File

@@ -7,7 +7,7 @@ Created on: 22.07.21
"""
from django.contrib import admin
from konova.models import Geometry, Document, Deadline
from konova.models import Geometry, Deadline
class GeometryAdmin(admin.ModelAdmin):
@@ -17,7 +17,7 @@ class GeometryAdmin(admin.ModelAdmin):
]
class DocumentAdmin(admin.ModelAdmin):
class AbstractDocumentAdmin(admin.ModelAdmin):
list_display = [
"id",
"title",
@@ -36,5 +36,4 @@ class DeadlineAdmin(admin.ModelAdmin):
admin.site.register(Geometry, GeometryAdmin)
admin.site.register(Document, DocumentAdmin)
admin.site.register(Deadline, DeadlineAdmin)

View File

@@ -21,11 +21,11 @@ from django.shortcuts import render
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount
from ema.models import Ema
from intervention.models import Intervention
from compensation.models import EcoAccount, Compensation, EcoAccountDocument, CompensationDocument
from ema.models import Ema, EmaDocument
from intervention.models import Intervention, Revocation, RevocationDocument, InterventionDocument
from konova.contexts import BaseContext
from konova.models import Document, BaseObject
from konova.models import BaseObject
from konova.utils.message_templates import FORM_INVALID
from user.models import UserActionLogEntry, UserAction
@@ -307,7 +307,7 @@ class NewDocumentForm(BaseModalForm):
attrs={
"class": "w-75"
}
)
),
)
comment = forms.CharField(
required=False,
@@ -322,6 +322,13 @@ class NewDocumentForm(BaseModalForm):
}
)
)
document_instance_map = {
Intervention: InterventionDocument,
Compensation: CompensationDocument,
EcoAccount: EcoAccountDocument,
Revocation: RevocationDocument,
Ema: EmaDocument,
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@@ -331,6 +338,12 @@ class NewDocumentForm(BaseModalForm):
self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload
}
self.document_type = self.document_instance_map.get(
self.instance.__class__,
None
)
if not self.document_type:
raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
def save(self):
with transaction.atomic():
@@ -338,14 +351,14 @@ class NewDocumentForm(BaseModalForm):
user=self.user,
action=UserAction.CREATED,
)
doc = Document.objects.create(
doc = self.document_type.objects.create(
created=action,
title=self.cleaned_data["title"],
comment=self.cleaned_data["comment"],
file=self.cleaned_data["file"],
date_of_creation=self.cleaned_data["creation_date"],
instance=self.instance,
)
self.instance.documents.add(doc)
edited_action = UserActionLogEntry.objects.create(
user=self.user,

View File

@@ -18,6 +18,7 @@ from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION
ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH
from ema.settings import EMA_ACCOUNT_IDENTIFIER_LENGTH, EMA_ACCOUNT_IDENTIFIER_TEMPLATE
from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE
from konova.settings import INTERVENTION_REVOCATION_DOC_PATH
from konova.utils.generators import generate_random_string
from user.models import UserActionLogEntry, UserAction
@@ -220,7 +221,47 @@ class Deadline(BaseResource):
return None
class Document(BaseResource):
def generate_document_file_upload_path(instance, filename):
""" Generates the file upload path for certain document instances
Documents derived from AbstractDocument need specific upload paths for their related models.
Args:
instance (): The document instance
filename (): The filename
Returns:
"""
from compensation.models import CompensationDocument, EcoAccountDocument
from ema.models import EmaDocument
from intervention.models import InterventionDocument, RevocationDocument
from konova.settings import ECO_ACCOUNT_DOC_PATH, EMA_DOC_PATH, \
COMPENSATION_DOC_PATH, \
INTERVENTION_DOC_PATH
# Map document types to paths on the hard drive
path_map = {
InterventionDocument: INTERVENTION_DOC_PATH,
CompensationDocument: COMPENSATION_DOC_PATH,
EmaDocument: EMA_DOC_PATH,
RevocationDocument: INTERVENTION_REVOCATION_DOC_PATH,
EcoAccountDocument: ECO_ACCOUNT_DOC_PATH,
}
path = path_map.get(instance.__class__, None)
if path is None:
raise NotImplementedError("Unidentified document type: {}".format(instance.__class__))
# RevocationDocument needs special treatment, since these files need to be stored in a subfolder of the related
# instance's (Revocation) legaldata interventions folder
if instance.__class__ is RevocationDocument:
path = path.format(instance.intervention.id)
else:
path = path.format(instance.instance.id)
return path + filename
class AbstractDocument(BaseResource):
"""
Documents can be attached to compensation or intervention for uploading legal documents or pictures.
"""
@@ -229,6 +270,9 @@ class Document(BaseResource):
file = models.FileField()
comment = models.TextField()
class Meta:
abstract = True
def delete(self, using=None, keep_parents=False):
""" Custom delete function to remove the real file from the hard drive
@@ -239,7 +283,11 @@ class Document(BaseResource):
Returns:
"""
os.remove(self.file.file.name)
try:
os.remove(self.file.file.name)
except FileNotFoundError:
# File seems to missing anyway - continue!
pass
super().delete(using=using, keep_parents=keep_parents)

View File

@@ -55,7 +55,20 @@ DEFAULT_GROUP = "Default"
ZB_GROUP = "Registration office"
ETS_GROUP = "Conservation office"
# Needed to redirect to LANIS
## Values to be inserted are [zoom_level, x_coord, y_coord]
LANIS_LINK_TEMPLATE = "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/index.php?lang=de&zl={}&x={}&y={}&bl=tk_rlp_tms_grau&bo=1&lo=0.8,0.8,0.8,0.6,0.8,0.8,0.8,0.8,0.8&layers=eiv_f,eiv_l,eiv_p,kom_f,kom_l,kom_p,oek_f,ema_f,mae&service=kartendienste_naturschutz"
# ALLOWED FILE UPLOAD DEFINITIONS
# Default: Upload into upper project folder
MEDIA_ROOT = BASE_DIR + "/.."
# DOCUMENT UPLOAD PATHS
# Extends MEDIA_ROOT
## {} is a placeholder for the object's uuid --> each object will have it's own folder
BASE_DOC_PATH = "konova_uploaded_files/"
INTERVENTION_DOC_PATH = BASE_DOC_PATH + "interventions/{}/"
INTERVENTION_REVOCATION_DOC_PATH = BASE_DOC_PATH + "interventions/{}/revocation/"
COMPENSATION_DOC_PATH = BASE_DOC_PATH + "compensations/{}/"
ECO_ACCOUNT_DOC_PATH = BASE_DOC_PATH + "eco_account/{}/"
EMA_DOC_PATH = BASE_DOC_PATH + "ema/{}/"

View File

@@ -22,7 +22,7 @@ from konova.autocompletes import OrganisationAutocomplete, NonOfficialOrganisati
RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
from konova.sso.sso import KonovaSSOClient
from konova.views import logout_view, home_view, get_document_view, remove_document_view, remove_deadline_view
from konova.views import logout_view, home_view, remove_deadline_view
sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY)
urlpatterns = [
@@ -38,10 +38,6 @@ urlpatterns = [
path('news/', include("news.urls")),
path('news/', include("codelist.urls")),
# Generic documents routes
path('document/<id>', get_document_view, name="doc-open"),
path('document/<id>/remove', remove_document_view, name="doc-remove"),
# Generic deadline routes
path('deadline/<id>/remove', remove_deadline_view, name="deadline-remove"),

50
konova/utils/documents.py Normal file
View File

@@ -0,0 +1,50 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.09.21
"""
from django.http import FileResponse, HttpRequest
from django.utils.translation import gettext_lazy as _
from konova.forms import RemoveModalForm
from konova.models import AbstractDocument
def get_document(doc: AbstractDocument):
""" Returns a document as downloadable attachment
Args:
request (HttpRequest): The incoming request
id (str): The document id
Returns:
"""
return FileResponse(doc.file, as_attachment=True)
def remove_document(request: HttpRequest, doc: AbstractDocument):
""" Renders a form for uploading new documents
This function works using a modal. We are not using the regular way, the django bootstrap modal forms are
intended to be used. Instead of View classes we work using the classic way of dealing with forms (see below).
It is important to mention, that modal forms, which should reload the page afterwards, must provide a
'reload_page' bool in the context. This way, the modal may reload the page or not.
For further details see the comments in templates/modal or
https://github.com/trco/django-bootstrap-modal-forms
Args:
request (HttpRequest): The incoming request
Returns:
"""
title = doc.title
form = RemoveModalForm(request.POST or None, instance=doc, user=request.user)
return form.process_request(
request=request,
msg_success=_("Document '{}' deleted").format(title)
)

View File

@@ -17,7 +17,7 @@ from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import any_group_check
from konova.forms import RemoveModalForm
from konova.models import Document, Deadline
from konova.models import Deadline
from news.models import ServerMessage
from konova.settings import SSO_SERVER_BASE
@@ -97,48 +97,6 @@ def home_view(request: HttpRequest):
return render(request, template, context)
@login_required
def get_document_view(request: HttpRequest, id: str):
""" Returns a document as downloadable attachment
Args:
request (HttpRequest): The incoming request
id (str): The document id
Returns:
"""
doc = get_object_or_404(Document, id=id)
return FileResponse(doc.file, as_attachment=True)
@login_required
def remove_document_view(request: HttpRequest, id: str):
""" Renders a form for uploading new documents
This function works using a modal. We are not using the regular way, the django bootstrap modal forms are
intended to be used. Instead of View classes we work using the classic way of dealing with forms (see below).
It is important to mention, that modal forms, which should reload the page afterwards, must provide a
'reload_page' bool in the context. This way, the modal may reload the page or not.
For further details see the comments in templates/modal or
https://github.com/trco/django-bootstrap-modal-forms
Args:
request (HttpRequest): The incoming request
Returns:
"""
doc = get_object_or_404(Document, id=id)
title = doc.title
form = RemoveModalForm(request.POST or None, instance=doc, user=request.user)
return form.process_request(
request=request,
msg_success=_("Document '{}' deleted").format(title)
)
@login_required
def remove_deadline_view(request: HttpRequest, id:str):
""" Renders a modal form for removing a deadline object