#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 17d574e2a5
commit f64a11cb50
25 changed files with 437 additions and 109 deletions

View File

@ -21,8 +21,9 @@ urlpatterns = [
path('<id>/deadline/new', deadline_new_view, name="acc-new-deadline"),
# Documents
# Document remove route can be found in konova/urls.py
path('<id>/document/new/', new_document_view, name='acc-new-doc'),
path('document/<doc_id>', get_document_view, name='acc-get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='acc-remove-doc'),
# Eco-account deductions
path('<id>/remove/<deduction_id>', deduction_remove_view, name='deduction-remove'),

View File

@ -21,8 +21,9 @@ urlpatterns = [
path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
# Documents
# Document remove route can be found in konova/urls.py
path('<id>/document/new/', new_document_view, name='new-doc'),
path('document/<doc_id>', get_document_view, name='get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
# Generic state routes
path('state/<id>/remove', state_remove_view, name='state-remove'),

View File

@ -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

View File

@ -39,14 +39,14 @@
{% for doc in obj.documents.all %}
<tr>
<td class="align-middle">
<a href="{% url 'doc-open' doc.id %}">
<a href="{% url 'compensation:get-doc' doc.id %}">
{{ doc.title }}
</a>
</td>
<td class="align-middle">{{ doc.comment }}</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'doc-remove' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
<button data-form-url="{% url 'compensation:remove-doc' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@ -39,14 +39,14 @@
{% for doc in obj.documents.all %}
<tr>
<td class="align-middle">
<a href="{% url 'doc-open' doc.id %}">
<a href="{% url 'compensation:acc-get-doc' doc.id %}">
{{ doc.title }}
</a>
</td>
<td class="align-middle">{{ doc.comment }}</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'doc-remove' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
<button data-form-url="{% url 'compensation:acc-remove-doc' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@ -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

View File

@ -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):

View File

@ -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
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
)

View File

@ -39,14 +39,14 @@
{% for doc in obj.documents.all %}
<tr>
<td class="align-middle">
<a href="{% url 'doc-open' doc.id %}">
<a href="{% url 'ema:get-doc' doc.id %}">
{{ doc.title }}
</a>
</td>
<td class="align-middle">{{ doc.comment }}</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'doc-remove' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
<button data-form-url="{% url 'ema:remove-doc' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@ -24,6 +24,8 @@ urlpatterns = [
# Documents
# Document remove route can be found in konova/urls.py
path('<id>/document/new/', document_new_view, name='new-doc'),
path('document/<doc_id>', get_document_view, name='get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
# Generic state routes
path('state/<id>/remove', state_remove_view, name='state-remove'),

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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
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
)

View File

@ -39,14 +39,14 @@
{% for doc in intervention.documents.all %}
<tr>
<td class="align-middle">
<a href="{% url 'doc-open' doc.id %}">
<a href="{% url 'intervention:get-doc' doc.id %}">
{{ doc.title }}
</a>
</td>
<td class="align-middle">{{ doc.comment }}</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'doc-remove' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
<button data-form-url="{% url 'intervention:remove-doc' doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove document' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}

View File

@ -31,10 +31,10 @@
{% trans 'From' context 'Revocation' %}
</th>
<th scope="col">
{% trans 'Comment' %}
{% trans 'Document' %}
</th>
<th scope="col">
{% trans 'Document' %}
{% trans 'Comment' %}
</th>
<th scope="col">
{% trans 'Action' %}
@ -48,14 +48,14 @@
<td class="align-middle">
{{ rev.date }}
</td>
<td class="align-middle">{{ rev.comment }}</td>
<td class="align-middle">
{% if rev.document %}
<a href="{% url 'doc-open' rev.document.id %}">
{{ rev.document.file }}
<a href="{% url 'intervention:get-doc-revocation' rev.document.id %}">
{% trans 'Revocation' %}
</a>
{% endif %}
</td>
<td class="align-middle">{{ rev.comment }}</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'intervention:remove-revocation' rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove revocation' %}">

View File

@ -9,13 +9,12 @@ from django.urls import path
from intervention.views import index_view, new_view, open_view, edit_view, remove_view, new_document_view, share_view, \
create_share_view, remove_revocation_view, new_revocation_view, run_check_view, log_view, new_deduction_view, \
record_view
record_view, remove_document_view, get_document_view, get_revocation_view
app_name = "intervention"
urlpatterns = [
path("", index_view, name="index"),
path('new/', new_view, name='new'),
path('<id>/document/new/', new_document_view, name='new-doc'),
path('<id>', open_view, name='open'),
path('<id>/log', log_view, name='log'),
path('<id>/edit', edit_view, name='edit'),
@ -25,10 +24,16 @@ urlpatterns = [
path('<id>/check', run_check_view, name='run-check'),
path('<id>/record', record_view, name='record'),
# Documents
path('<id>/document/new/', new_document_view, name='new-doc'),
path('document/<doc_id>', get_document_view, name='get-doc'),
path('document/<doc_id>/remove/', remove_document_view, name='remove-doc'),
# Deductions
path('<id>/deduction/new', new_deduction_view, name='acc-new-deduction'),
# Revocation routes
path('<id>/revocation/new', new_revocation_view, name='new-revocation'),
path('revocation/<id>/remove', remove_revocation_view, name='remove-revocation'),
path('revocation/<doc_id>', get_revocation_view, name='get-doc-revocation'),
]

View File

@ -6,12 +6,13 @@ from django.shortcuts import render, get_object_or_404
from intervention.forms import NewInterventionForm, EditInterventionForm, ShareInterventionForm, NewRevocationForm, \
RunCheckForm, NewDeductionForm
from intervention.models import Intervention, Revocation
from intervention.models import Intervention, Revocation, InterventionDocument, RevocationDocument
from intervention.tables import InterventionTable
from konova.contexts import BaseContext
from konova.decorators import *
from konova.forms import SimpleGeomForm, NewDocumentForm, RemoveModalForm, RecordModalForm
from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
from konova.utils.documents import remove_document, get_document
from konova.utils.message_templates import FORM_INVALID, INTERVENTION_INVALID
from konova.utils.user_checks import in_group
@ -94,6 +95,60 @@ def new_document_view(request: HttpRequest, id: str):
)
@login_required
def get_revocation_view(request: HttpRequest, doc_id: str):
""" Returns the revocation 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(RevocationDocument, id=doc_id)
return get_document(doc)
@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(InterventionDocument, 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(InterventionDocument, id=doc_id)
return remove_document(
request,
doc
)
@login_required
@any_group_check
def open_view(request: HttpRequest, id: str):

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