diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index d5aca228..4b74a405 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -10,7 +10,7 @@ from django.urls import path from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \ GetCompensationDocumentView, RemoveCompensationDocumentView from compensation.views.compensation.resubmission import CompensationResubmissionView -from compensation.views.compensation.report import report_view +from compensation.views.compensation.report import CompensationReportView from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \ RemoveCompensationDeadlineView from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \ @@ -43,7 +43,7 @@ urlpatterns = [ path('/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"), path('/deadline//edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'), path('/deadline//remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'), - path('/report', report_view, name='report'), + path('/report', CompensationReportView.as_view(), name='report'), path('/resub', CompensationResubmissionView.as_view(), name='resubmission-create'), # Documents diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index c0edc4b3..1825d96a 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -12,7 +12,7 @@ from compensation.views.eco_account.eco_account import new_view, edit_view, remo detail_view, EcoAccountIndexView, EcoAccountIdentifierGeneratorView from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.record import EcoAccountRecordView -from compensation.views.eco_account.report import report_view +from compensation.views.eco_account.report import EcoAccountReportView from compensation.views.eco_account.resubmission import EcoAccountResubmissionView from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \ RemoveEcoAccountStateView @@ -34,7 +34,7 @@ urlpatterns = [ path('', detail_view, name='detail'), path('/log', EcoAccountLogView.as_view(), name='log'), path('/record', EcoAccountRecordView.as_view(), name='record'), - path('/report', report_view, name='report'), + path('/report', EcoAccountReportView.as_view(), name='report'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), path('/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'), diff --git a/compensation/views/compensation/report.py b/compensation/views/compensation/report.py index 96081627..dde16ea0 100644 --- a/compensation/views/compensation/report.py +++ b/compensation/views/compensation/report.py @@ -5,77 +5,48 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render from django.urls import reverse -from django.utils.translation import gettext_lazy as _ from compensation.models import Compensation -from konova.contexts import BaseContext -from konova.decorators import uuid_required -from konova.forms import SimpleGeomForm -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.generators import generate_qr_code +from konova.sub_settings.django_settings import BASE_URL +from konova.utils.qrcode import QrCode +from konova.views.report import BaseReportView -@uuid_required -def report_view(request: HttpRequest, id: str): - """ Renders the public report view - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention +class BaseCompensationReportView(BaseReportView): + def _get_compensation_report_context(self, obj): + # Order states by surface + before_states = obj.before_states.all().order_by("-surface").prefetch_related("biotope_type") + after_states = obj.after_states.all().order_by("-surface").prefetch_related("biotope_type") + actions = obj.actions.all().prefetch_related("action_type") - Returns: - - """ - # Reuse the compensation report template since compensations are structurally identical - template = "compensation/report/compensation/report.html" - comp = get_object_or_404(Compensation, id=id) - - tab_title = _("Report {}").format(comp.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not comp.is_ready_for_publish(): - template = "report/unavailable.html" - context = { - TAB_TITLE_IDENTIFIER: tab_title, + return { + "before_states": before_states, + "after_states": after_states, + "actions": actions, } - context = BaseContext(request, context).context - return render(request, template, context) - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=comp - ) - parcels = comp.get_underlying_parcels() - qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = comp.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) +class CompensationReportView(BaseCompensationReportView): + _MODEL = Compensation + _TEMPLATE = "compensation/report/compensation/report.html" - # Order states by surface - before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type") - after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type") - actions = comp.actions.all().prefetch_related("action_type") + def _get_report_context(self, obj): + report_url = BASE_URL + reverse("compensation:report", args=(obj.id,)) + qrcode_report = QrCode(report_url, 10) + qrcode_lanis = QrCode(obj.get_LANIS_link(), 7) - context = { - "obj": comp, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url, - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, - }, - "is_entry_shared": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + report_context = { + "qrcode": { + "img": qrcode_report.get_img(), + "url": qrcode_report.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "is_entry_shared": False, # disables action buttons during rendering + "tables_scrollable": False, + } + report_context.update(self._get_compensation_report_context(obj)) + return report_context \ No newline at end of file diff --git a/compensation/views/eco_account/report.py b/compensation/views/eco_account/report.py index f61a7bfc..456564da 100644 --- a/compensation/views/eco_account/report.py +++ b/compensation/views/eco_account/report.py @@ -5,85 +5,41 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render from django.urls import reverse -from django.utils.translation import gettext_lazy as _ from compensation.models import EcoAccount -from konova.contexts import BaseContext -from konova.decorators import uuid_required -from konova.forms import SimpleGeomForm -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.generators import generate_qr_code +from compensation.views.compensation.report import BaseCompensationReportView +from konova.sub_settings.django_settings import BASE_URL +from konova.utils.qrcode import QrCode -@uuid_required -def report_view(request: HttpRequest, id: str): - """ Renders the public report view +class EcoAccountReportView(BaseCompensationReportView): + _MODEL = EcoAccount + _TEMPLATE = "compensation/report/eco_account/report.html" - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention + def _get_report_context(self, obj): + report_url = BASE_URL + reverse("compensation:acc:report", args=(obj.id,)) + qrcode_report = QrCode(report_url, 10) + qrcode_lanis = QrCode(obj.get_LANIS_link(), 7) - Returns: + # Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier) + deductions = obj.deductions.all() \ + .distinct("intervention") \ + .select_related("intervention") \ + .values_list("intervention__id", "intervention__identifier", "intervention__title", named=True) - """ - # Reuse the compensation report template since EcoAccounts are structurally identical - template = "compensation/report/eco_account/report.html" - acc = get_object_or_404(EcoAccount, id=id) - - tab_title = _("Report {}").format(acc.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not acc.is_ready_for_publish(): - template = "report/unavailable.html" - context = { - TAB_TITLE_IDENTIFIER: tab_title, + report_context = { + "qrcode": { + "img": qrcode_report.get_img(), + "url": qrcode_report.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "is_entry_shared": False, # disables action buttons during rendering + "deductions": deductions, + "tables_scrollable": False, } - context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=acc - ) - parcels = acc.get_underlying_parcels() - - qrcode_url = request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = acc.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - # Order states by surface - before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent") - after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent") - actions = acc.actions.all().prefetch_related("action_type__parent") - - # Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier) - deductions = acc.deductions.all()\ - .distinct("intervention")\ - .select_related("intervention")\ - .values_list("intervention__id", "intervention__identifier", "intervention__title", named=True) - - context = { - "obj": acc, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url, - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, - }, - "is_entry_shared": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - "deductions": deductions, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + report_context.update(self._get_compensation_report_context(obj)) + return report_context diff --git a/ema/urls.py b/ema/urls.py index 5fad0959..f4f8f79a 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -14,7 +14,7 @@ from ema.views.ema import new_view, detail_view, edit_view, remove_view, EmaInde EmaIdentifierGeneratorView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView -from ema.views.report import report_view +from ema.views.report import EmaReportView from ema.views.resubmission import EmaResubmissionView from ema.views.share import EmaShareFormView, EmaShareByTokenView from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView @@ -29,7 +29,7 @@ urlpatterns = [ path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), path('/record', EmaRecordView.as_view(), name='record'), - path('/report', report_view, name='report'), + path('/report', EmaReportView.as_view(), name='report'), path('/resub', EmaResubmissionView.as_view(), name='resubmission-create'), path('/state/new', NewEmaStateView.as_view(), name='new-state'), diff --git a/ema/views/report.py b/ema/views/report.py index 93af6211..8eb2a23d 100644 --- a/ema/views/report.py +++ b/ema/views/report.py @@ -5,77 +5,36 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +from compensation.views.compensation.report import BaseCompensationReportView from ema.models import Ema -from konova.contexts import BaseContext -from konova.decorators import uuid_required -from konova.forms import SimpleGeomForm -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.generators import generate_qr_code +from konova.sub_settings.django_settings import BASE_URL +from konova.utils.qrcode import QrCode -@uuid_required -def report_view(request:HttpRequest, id: str): - """ Renders the public report view - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention +class EmaReportView(BaseCompensationReportView): + _TEMPLATE = "ema/report/report.html" + _MODEL = Ema - Returns: + def _get_report_context(self, obj): + report_url = BASE_URL + reverse("ema:report", args=(obj.id,)) + qrcode_report = QrCode(report_url, 10) + qrcode_lanis = QrCode(obj.get_LANIS_link(), 7) - """ - # Reuse the compensation report template since EMAs are structurally identical - template = "ema/report/report.html" - ema = get_object_or_404(Ema, id=id) + generic_compensation_report_context = self._get_compensation_report_context(obj) - tab_title = _("Report {}").format(ema.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not ema.is_ready_for_publish(): - template = "report/unavailable.html" - context = { - TAB_TITLE_IDENTIFIER: tab_title, + report_context = { + "qrcode": { + "img": qrcode_report.get_img(), + "url": qrcode_report.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "is_entry_shared": False, # disables action buttons during rendering + "tables_scrollable": False, } - context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=ema, - ) - parcels = ema.get_underlying_parcels() - - qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = ema.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - # Order states by surface - before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type") - after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type") - actions = ema.actions.all().prefetch_related("action_type") - - context = { - "obj": ema, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url - }, - "is_entry_shared": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + report_context.update(generic_compensation_report_context) + return report_context \ No newline at end of file diff --git a/intervention/urls.py b/intervention/urls.py index b88fdfa8..db5829fd 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -18,7 +18,7 @@ from intervention.views.intervention import new_view, detail_view, edit_view, re InterventionIndexView, InterventionIdentifierGeneratorView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView -from intervention.views.report import report_view +from intervention.views.report import InterventionReportView from intervention.views.resubmission import InterventionResubmissionView from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \ get_revocation_view @@ -37,7 +37,7 @@ urlpatterns = [ path('/share', InterventionShareFormView.as_view(), name='share-form'), path('/check', check_view, name='check'), path('/record', InterventionRecordView.as_view(), name='record'), - path('/report', report_view, name='report'), + path('/report', InterventionReportView.as_view(), name='report'), path('/resub', InterventionResubmissionView.as_view(), name='resubmission-create'), # Compensations diff --git a/intervention/views/report.py b/intervention/views/report.py index 6bdd8252..2d676b1d 100644 --- a/intervention/views/report.py +++ b/intervention/views/report.py @@ -5,72 +5,41 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render from django.urls import reverse -from django.utils.translation import gettext_lazy as _ from intervention.models import Intervention -from konova.contexts import BaseContext -from konova.decorators import uuid_required -from konova.forms import SimpleGeomForm -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.generators import generate_qr_code +from konova.sub_settings.django_settings import BASE_URL +from konova.utils.qrcode import QrCode +from konova.views.report import BaseReportView -@uuid_required -def report_view(request: HttpRequest, id: str): - """ Renders the public report view +class InterventionReportView(BaseReportView): + _TEMPLATE = 'intervention/report/report.html' + _MODEL = Intervention - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention + def _get_report_context(self, obj: Intervention): + """ Returns the specific context needed for an intervention report - Returns: + Args: + obj (Intervention): The object for the report - """ - template = "intervention/report/report.html" - intervention = get_object_or_404(Intervention, id=id) + Returns: + dict: The object specific context for rendering the report + """ + distinct_deductions = obj.deductions.all().distinct("account") + report_url = BASE_URL + reverse("intervention:report", args=(obj.id,)) + qrcode_report = QrCode(report_url, 10) + qrcode_lanis = QrCode(obj.get_LANIS_link(), 7) - tab_title = _("Report {}").format(intervention.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not intervention.is_ready_for_publish(): - template = "report/unavailable.html" - context = { - TAB_TITLE_IDENTIFIER: tab_title, + return { + "deductions": distinct_deductions, + "qrcode": { + "img": qrcode_report.get_img(), + "url": qrcode_report.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "tables_scrollable": False, } - context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=intervention - ) - parcels = intervention.get_underlying_parcels() - - distinct_deductions = intervention.deductions.all().distinct( - "account" - ) - qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = intervention.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - context = { - "obj": intervention, - "deductions": distinct_deductions, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url, - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, - }, - "geom_form": geom_form, - "parcels": parcels, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) diff --git a/konova/utils/generators.py b/konova/utils/generators.py index 78e075ad..d4aa9441 100644 --- a/konova/utils/generators.py +++ b/konova/utils/generators.py @@ -7,10 +7,6 @@ Created on: 09.11.20 """ import random import string -import qrcode -import qrcode.image.svg - -from io import BytesIO def generate_token() -> str: @@ -42,23 +38,3 @@ def generate_random_string(length: int, use_numbers: bool = False, use_letters_l ret_val = "".join(random.choice(elements) for i in range(length)) return ret_val - -def generate_qr_code(content: str, size: int = 20) -> str: - """ Generates a qr code from given content - - Args: - content (str): The content for the qr code - size (int): The image size - - Returns: - qrcode_svg (str): The qr code as svg - """ - qrcode_factory = qrcode.image.svg.SvgImage - qrcode_img = qrcode.make( - content, - image_factory=qrcode_factory, - box_size=size - ) - stream = BytesIO() - qrcode_img.save(stream) - return stream.getvalue().decode() diff --git a/konova/utils/qrcode.py b/konova/utils/qrcode.py new file mode 100644 index 00000000..75aa3a2a --- /dev/null +++ b/konova/utils/qrcode.py @@ -0,0 +1,47 @@ +""" +Author: Michel Peltriaux +Created on: 17.10.25 + +""" +from io import BytesIO + +import qrcode +import qrcode.image.svg as svg + + +class QrCode: + """ A wrapping class for creating a qr code with content + + """ + _content = None + _img = None + + def __init__(self, content: str, size: int): + self._content = content + self._img = self._generate_qr_code(content, size) + + def _generate_qr_code(self, content: str, size: int = 20) -> str: + """ Generates a qr code from given content + + Args: + content (str): The content for the qr code + size (int): The image size + + Returns: + qrcode_svg (str): The qr code as svg + """ + img_factory = svg.SvgImage + qrcode_img = qrcode.make( + content, + image_factory=img_factory, + box_size=size + ) + stream = BytesIO() + qrcode_img.save(stream) + return stream.getvalue().decode() + + def get_img(self): + return self._img + + def get_content(self): + return self._content diff --git a/konova/views/report.py b/konova/views/report.py new file mode 100644 index 00000000..2dca0d15 --- /dev/null +++ b/konova/views/report.py @@ -0,0 +1,106 @@ +""" +Author: Michel Peltriaux +Created on: 17.10.25 + +""" +from abc import abstractmethod +from uuid import UUID + +from django.http import HttpRequest, Http404 +from django.shortcuts import get_object_or_404, render +from django.utils.translation import gettext_lazy as _ + +from konova.contexts import BaseContext +from konova.forms import SimpleGeomForm +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.views.base import BaseView + + +class BaseReportView(BaseView): + _TEMPLATE = None + _TAB_TITLE = _("Report {}") + _MODEL = None + + class Meta: + abstract = True + + def dispatch(self, request, *args, **kwargs): + # If the given id is not a uuid we act as the result was not found + try: + UUID(kwargs.get('id')) + except ValueError: + raise Http404() + return super().dispatch(request, *args, **kwargs) + + def _return_unpublishable_content_response(self, request: HttpRequest, tab_title: str): + """ Handles HttpResponse return in case the object is not ready for publish + + Args: + request (): + tab_title (): + + Returns: + + """ + template = "report/unavailable.html" + context = { + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) + + def get(self, request: HttpRequest, id: str): + """ Renders the public report view + + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention + + Returns: + + """ + obj = get_object_or_404(self._MODEL, id=id) + tab_title = self._TAB_TITLE.format(obj.identifier) + + # If object is not recorded we need to render another template without any data + if not obj.is_ready_for_publish(): + return self._return_unpublishable_content_response(request, tab_title) + + # First get specific report context for different types of objects due to inheritance + report_context = self._get_report_context(obj) + + # Then generate and add default report context (the same for all models) + geom_form = SimpleGeomForm(instance=obj) + parcels = obj.get_underlying_parcels() + report_context.update( + { + TAB_TITLE_IDENTIFIER: tab_title, + "parcels": parcels, + "geom_form": geom_form, + "obj": obj + } + ) + + # Then generate the general context based on the report specific data + context = BaseContext(request, report_context).context + return render(request, self._TEMPLATE, context) + + @abstractmethod + def _get_report_context(self, obj): + """ Returns the specific context needed for this report view + + Args: + obj (RecordableObjectMixin): The object for the report + + Returns: + dict: The object specific context for rendering the report + """ + raise NotImplementedError + + def _user_has_permission(self, user): + # Reports do not need specific permissions to be callable + return True + + def _user_has_shared_access(self, user, **kwargs): + # Reports do not need specific share states to be callable + return True