Compare commits

...

5 Commits

Author SHA1 Message Date
mipel
615b7bf5ea EMA
* fixes created timestamp in detail views where modified needs to be displayed
* adds fallback timestamp if data has not been edited, yet --> show created timestamp
* fixes bug where deleting of certain data didn't redirect to the index view
* adds quality_check() method for EMA, needed for recording
* adds all functions which are provided for compensations to EMA
* adds/updates translations
2021-08-19 13:44:06 +02:00
mipel
d1f43f8c64 EMA
* adds Ema model (basically Compensation inherited)
* adds index view for EMAs
* fixes drop-down link bug for menu 'More' in navbar
* refactors some more forms to use process_request()
* adds modified attribute to BaseResource for easy last_modified check
* adds setting of modified attribute in all places where UserAction.EDITED is added to log
* adds EMA_ACCOUNT_IDENTIFIER_LENGTH and EMA_ACCOUNT_IDENTIFIER_TEMPLATE to ema/settings.py
* adds EmaAdmin to ema/admin.py
* fixes wrong title in intervention detail view html for revocations
* adds support for subtitle variable to BaseTable and generic_index.html
* drops next_version attribute from models
* adds/updates translations
* adds default ordering for UserActionLogEntry
   * removes extra ordering in log modal rendering
2021-08-19 13:02:31 +02:00
mipel
03fe293cd8 Favicon
* adds favicon
2021-08-19 09:12:13 +02:00
mipel
198462b051 Modal forms
* adds new POST functionality in NewStateModalForm, which does not use the super classes process_request() method
* adds explaining comments on the is_ajax() call
* updates translations
2021-08-19 09:06:35 +02:00
mipel
7dbf4d5b91 Mobile optimization
* adds table-responsive class to BaseClass
* adds html meta attributes for proper small device rendering
2021-08-19 08:44:47 +02:00
45 changed files with 1415 additions and 344 deletions

View File

@ -5,11 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.12.20 Created on: 04.12.20
""" """
from bootstrap_modal_forms.utils import is_ajax
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages
from django.db import transaction from django.db import transaction
from django.http import HttpRequest from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import redirect, render from django.shortcuts import render
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.models import Payment, CompensationState, CompensationAction, UnitChoices from compensation.models import Payment, CompensationState, CompensationAction, UnitChoices
@ -88,6 +89,8 @@ class NewPaymentForm(BaseModalForm):
intervention=self.intervention, intervention=self.intervention,
) )
self.intervention.log.add(edited_action) self.intervention.log.add(edited_action)
self.intervention.modified = edited_action
self.intervention.save()
return pay return pay
@ -121,6 +124,8 @@ class NewStateModalForm(BaseModalForm):
comment=_("Added state") comment=_("Added state")
) )
self.instance.log.add(user_action) self.instance.log.add(user_action)
self.instance.modified = user_action
self.instance.save()
state = CompensationState.objects.create( state = CompensationState.objects.create(
biotope_type=self.cleaned_data["biotope_type"], biotope_type=self.cleaned_data["biotope_type"],
@ -153,19 +158,24 @@ class NewStateModalForm(BaseModalForm):
template = self.template template = self.template
if request.method == "POST": if request.method == "POST":
if self.is_valid(): if self.is_valid():
is_before_state = bool(request.GET.get("before", False)) # Modal forms send one POST for checking on data validity. This can be used to return possible errors
self.save(is_before_state=is_before_state) # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
messages.success( # saving/commiting of the data to the database. is_ajax() performs this check. The first request is
request, # an ajax call, the second is a regular form POST.
msg_success if not is_ajax(request.META):
) is_before_state = bool(request.GET.get("before", False))
return redirect(redirect_url) self.save(is_before_state=is_before_state)
messages.success(
request,
msg_success
)
return HttpResponseRedirect(redirect_url)
else: else:
messages.info( context = {
request, "form": self,
msg_error }
) context = BaseContext(request, context).context
return redirect(redirect_url) return render(request, template, context)
elif request.method == "GET": elif request.method == "GET":
context = { context = {
"form": self, "form": self,
@ -238,6 +248,8 @@ class NewDeadlineModalForm(BaseModalForm):
action=UserAction.EDITED, action=UserAction.EDITED,
comment=_("Added deadline") comment=_("Added deadline")
) )
self.instance.modified = edited_action
self.instance.save()
self.instance.log.add(edited_action) self.instance.log.add(edited_action)
self.instance.deadlines.add(deadline) self.instance.deadlines.add(deadline)
return deadline return deadline
@ -308,6 +320,8 @@ class NewActionModalForm(BaseModalForm):
action=UserAction.EDITED, action=UserAction.EDITED,
comment=_("Added action"), comment=_("Added action"),
) )
self.instance.modified = edited_action
self.instance.save()
self.instance.log.add(edited_action) self.instance.log.add(edited_action)
self.instance.actions.add(comp_action) self.instance.actions.add(comp_action)
return comp_action return comp_action

View File

@ -91,8 +91,8 @@ class CompensationAction(BaseResource):
class AbstractCompensation(BaseObject): class AbstractCompensation(BaseObject):
""" """
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
or EcoAccount. EMA or EcoAccount.
""" """
responsible = models.OneToOneField( responsible = models.OneToOneField(
@ -112,9 +112,6 @@ class AbstractCompensation(BaseObject):
geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL) geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL)
documents = models.ManyToManyField("konova.Document", blank=True) documents = models.ManyToManyField("konova.Document", blank=True)
# Holds a successor for this data
next_version = models.ForeignKey("Compensation", null=True, blank=True, on_delete=models.DO_NOTHING)
class Meta: class Meta:
abstract = True abstract = True
@ -260,9 +257,17 @@ class EcoAccount(AbstractCompensation):
y, y,
) )
def quality_check(self) -> (bool, dict): def quality_check(self) -> list:
# ToDo """ Quality check
pass
Returns:
ret_msgs (list): Holds error messages
"""
ret_msgs = []
# ToDo: Add check methods!
return ret_msgs
class EcoAccountWithdraw(BaseResource): class EcoAccountWithdraw(BaseResource):

View File

@ -7,5 +7,6 @@ Created on: 18.12.20
""" """
COMPENSATION_IDENTIFIER_LENGTH = 10 COMPENSATION_IDENTIFIER_LENGTH = 10
COMPENSATION_IDENTIFIER_TEMPLATE = "KOM-{}" COMPENSATION_IDENTIFIER_TEMPLATE = "KOM-{}"
ECO_ACCOUNT_IDENTIFIER_LENGTH = 10 ECO_ACCOUNT_IDENTIFIER_LENGTH = 10
ECO_ACCOUNT_IDENTIFIER_TEMPLATE = "OEK-{}" ECO_ACCOUNT_IDENTIFIER_TEMPLATE = "OEK-{}"

View File

@ -5,7 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.12.20 Created on: 01.12.20
""" """
from django.db.models import Sum
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
@ -15,7 +14,6 @@ from django.utils.translation import gettext_lazy as _
from compensation.filters import CompensationTableFilter, EcoAccountTableFilter from compensation.filters import CompensationTableFilter, EcoAccountTableFilter
from compensation.models import Compensation, EcoAccount from compensation.models import Compensation, EcoAccount
from intervention.filters import InterventionTableFilter
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
from konova.utils.tables import BaseTable from konova.utils.tables import BaseTable
import django_tables2 as tables import django_tables2 as tables
@ -53,7 +51,7 @@ class CompensationTable(BaseTable):
lm = tables.Column( lm = tables.Column(
verbose_name=_("Last edit"), verbose_name=_("Last edit"),
orderable=True, orderable=True,
accessor="created__timestamp", accessor="modified__timestamp",
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):
@ -126,21 +124,21 @@ class CompensationTable(BaseTable):
""" """
html = "" html = ""
checked = value is not None recorded = value is not None
tooltip = _("Not recorded yet") tooltip = _("Not recorded yet")
if checked: if recorded:
value = value.timestamp value = value.timestamp
value = localtime(value) value = localtime(value)
on = value.strftime(DEFAULT_DATE_TIME_FORMAT) on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user) tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark( html += self.render_bookmark(
tooltip=tooltip, tooltip=tooltip,
icn_filled=checked, icn_filled=recorded,
) )
return format_html(html) return format_html(html)
def render_e(self, value, record: Compensation): def render_e(self, value, record: Compensation):
""" Renders the registered column for a compensation """ Renders the editable column for a compensation
Args: Args:
value (str): The identifier value value (str): The identifier value
@ -192,7 +190,7 @@ class EcoAccountTable(BaseTable):
lm = tables.Column( lm = tables.Column(
verbose_name=_("Last edit"), verbose_name=_("Last edit"),
orderable=True, orderable=True,
accessor="created__timestamp", accessor="modified__timestamp",
) )
class Meta(BaseTable.Meta): class Meta(BaseTable.Meta):

View File

@ -63,9 +63,9 @@
<tr> <tr>
<th scope="row">{% trans 'Last modified' %}</th> <th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle"> <td class="align-middle">
{{obj.created.timestamp|default_if_none:""|naturalday}} {{obj.modified.timestamp|default_if_none:""|naturalday}}
<br> <br>
{{obj.created.user.username}} {{obj.modified.user.username}}
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -55,9 +55,9 @@
<tr> <tr>
<th scope="row">{% trans 'Last modified' %}</th> <th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle"> <td class="align-middle">
{{obj.created.timestamp|default_if_none:""|naturalday}} {{obj.modified.timestamp|default_if_none:""|naturalday}}
<br> <br>
{{obj.created.user.username}} {{obj.modified.user.username}}
</td> </td>
</tr> </tr>
<tr> <tr>

View File

@ -118,7 +118,7 @@ def log_view(request: HttpRequest, id: str):
context = { context = {
"modal_body_template": body_template, "modal_body_template": body_template,
"log": comp.log.all().order_by("-timestamp"), "log": comp.log.all(),
"modal_title": _("Log"), "modal_title": _("Log"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
@ -141,7 +141,7 @@ def remove_view(request: HttpRequest, id: str):
return form.process_request( return form.process_request(
request=request, request=request,
msg_success=_("Compensation removed"), msg_success=_("Compensation removed"),
redirect_url="", redirect_url=reverse("compensation:index"),
) )

View File

@ -180,7 +180,7 @@ def log_view(request: HttpRequest, id: str):
context = { context = {
"modal_body_template": body_template, "modal_body_template": body_template,
"log": comp.log.all().order_by("-timestamp"), "log": comp.log.all(),
"modal_title": _("Log"), "modal_title": _("Log"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context

View File

@ -6,18 +6,15 @@ Created on: 09.08.21
""" """
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404
from compensation.forms import NewPaymentForm from compensation.forms import NewPaymentForm
from compensation.models import Payment from compensation.models import Payment
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import default_group_required from konova.decorators import default_group_required
from konova.forms import RemoveModalForm from konova.forms import RemoveModalForm
from konova.utils.message_templates import FORM_INVALID
@login_required @login_required
@ -34,29 +31,10 @@ def new_payment_view(request: HttpRequest, intervention_id: str):
""" """
intervention = get_object_or_404(Intervention, id=intervention_id) intervention = get_object_or_404(Intervention, id=intervention_id)
form = NewPaymentForm(request.POST or None, instance=intervention, user=request.user) form = NewPaymentForm(request.POST or None, instance=intervention, user=request.user)
template = form.template return form.process_request(
if request.method == "POST": request,
if form.is_valid(): msg_success=_("Payment added")
payment = form.save() )
messages.success(
request,
_("Payment added")
)
return redirect(request.META.get("HTTP_REFERER", "home"))
else:
messages.info(
request,
FORM_INVALID
)
return redirect(request.META.get("HTTP_REFERER", "home"))
elif request.method == "GET":
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, template, context)
else:
raise NotImplementedError
@login_required @login_required

0
ema/__init__.py Normal file
View File

10
ema/admin.py Normal file
View File

@ -0,0 +1,10 @@
from django.contrib import admin
from compensation.admin import CompensationAdmin
from ema.models import Ema
class EmaAdmin(CompensationAdmin):
pass
admin.site.register(Ema, EmaAdmin)

5
ema/apps.py Normal file
View File

@ -0,0 +1,5 @@
from django.apps import AppConfig
class EmaConfig(AppConfig):
name = 'ema'

53
ema/filters.py Normal file
View File

@ -0,0 +1,53 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 19.08.21
"""
from django.db.models import QuerySet
from compensation.filters import CompensationTableFilter
class EmaTableFilter(CompensationTableFilter):
"""
Since EMA and compensation are basically the same, we can reuse CompensationTableFilter and extend the MAE filter
in the future by inheriting.
"""
def _filter_show_all(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_all' setting
Args:
queryset ():
name ():
value ():
Returns:
"""
if not value:
return queryset.filter(
users__in=[self.user], # requesting user has access
)
else:
return queryset
def _filter_show_recorded(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_recorded' setting
Args:
queryset ():
name ():
value ():
Returns:
"""
if not value:
return queryset.filter(
recorded=None,
)
else:
return queryset

86
ema/models.py Normal file
View File

@ -0,0 +1,86 @@
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 user.models import UserActionLogEntry
class Ema(AbstractCompensation):
"""
EMA = Ersatzzahlungsmaßnahme
(compensation actions from payments)
Until 2015 the EMA was the data object to keep track of any compensation, which has been funded by payments
previously paid. In 2015 another organization got in charge of this, which led to the creation of the data object
MAE (which is basically the same, just renamed in their system) to differ between the 'old' payment funded ones and
the new. For historical reasons, we need to keep EMAs in our system, since there are still entries done to this day,
which have been performed somewhere before 2015 and therefore needs to be entered.
Further information:
https://snu.rlp.de/de/foerderungen/massnahmen-aus-ersatzzahlungen/uebersicht-mae/
EMA therefore holds data like a compensation: actions, before-/after-states, deadlines, ...
"""
# Users having access on this object
# Not needed in regular Compensation since their access is defined by the linked intervention's access
users = models.ManyToManyField(
User,
help_text="Users having access (shared with)"
)
# Refers to "verzeichnen"
recorded = models.OneToOneField(
UserActionLogEntry,
on_delete=models.SET_NULL,
null=True,
blank=True,
help_text="Holds data on user and timestamp of this action",
related_name="+"
)
def __str__(self):
return "{}".format(self.identifier)
def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0:
# Create new identifier
new_id = self._generate_new_identifier()
while Ema.objects.filter(identifier=new_id).exists():
new_id = self._generate_new_identifier()
self.identifier = new_id
super().save(*args, **kwargs)
def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry
Returns:
"""
try:
geom = self.geometry.geom.transform(DEFAULT_SRID_RLP, clone=True)
x = geom.centroid.x
y = geom.centroid.y
zoom_lvl = 16
except AttributeError:
# If no geometry has been added, yet.
x = 1
y = 1
zoom_lvl = 6
return LANIS_LINK_TEMPLATE.format(
zoom_lvl,
x,
y,
)
def quality_check(self) -> list:
""" Quality check
Returns:
ret_msgs (list): Holds error messages
"""
ret_msgs = []
# ToDo: Add check methods!
return ret_msgs

10
ema/settings.py Normal file
View File

@ -0,0 +1,10 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 19.08.21
"""
EMA_ACCOUNT_IDENTIFIER_LENGTH = 10
EMA_ACCOUNT_IDENTIFIER_TEMPLATE = "EMA-{}"

132
ema/tables.py Normal file
View File

@ -0,0 +1,132 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 19.08.21
"""
from django.http import HttpRequest
from django.utils.html import format_html
from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
from django.urls import reverse
import django_tables2 as tables
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
from konova.utils.tables import BaseTable
from ema.filters import EmaTableFilter
from ema.models import Ema
class EmaTable(BaseTable):
"""
Since EMA and compensation are basically the same, we can reuse CompensationTableFilter and extend the EMA filter
in the future by inheriting.
"""
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="created__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Payment funded compensations")
self.subtitle = _("EMA explanation")
self.add_new_url = reverse("ema:new")
qs = kwargs.get("queryset", None)
self.filter = EmaTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
super().__init__(request, self.filter, *args, **kwargs)
def render_id(self, value, record: Ema):
""" Renders the id column for a EMA
Args:
value (str): The identifier value
record (EMA): The EMA record
Returns:
"""
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("EMA")),
href=reverse("ema:open", args=(record.id,)),
txt=value,
new_tab=False,
)
return format_html(html)
def render_r(self, value, record: Ema):
""" Renders the registered column for a EMA
Args:
value (str): The identifier value
record (Ema): The EMA record
Returns:
"""
html = ""
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
value = value.timestamp
value = localtime(value)
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=recorded,
)
return format_html(html)
def render_e(self, value, record: Ema):
""" Renders the editable column for a EMA
Args:
value (str): The identifier value
record (Ema): The EMA record
Returns:
"""
html = ""
has_access = value.filter(
username=self.user.username
).exists()
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)

View File

@ -0,0 +1,61 @@
{% load i18n l10n fontawesome_5 humanize %}
<div id="actions" class="card">
<div class="card-header rlp-r">
<div class="row">
<div class="col-sm-6">
<h5>
<span class="badge badge-light">{{obj.actions.count}}</span>
{% trans 'Actions' context 'Compensation' %}
</h5>
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-action' obj.id %}" title="{% trans 'Add new action' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'seedling' %}
</button>
{% endif %}
</div>
</div>
</div>
</div>
<div class="card-body scroll-300">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
{% trans 'Action type' %}
</th>
<th scope="col">
{% trans 'Amount' context 'Compensation' %}
</th>
<th scope="col">
{% trans 'Comment' %}
</th>
<th scope="col">
{% trans 'Action' %}
</th>
</tr>
</thead>
<tbody>
{% for action in obj.actions.all %}
<tr>
<td class="align-middle">
{{ action.action_type }}
</td>
<td class="align-middle">{{ action.amount|floatformat:2|intcomma }} {{ action.unit_humanize }}</td>
<td class="align-middle">{{ action.comment|default_if_none:"" }}</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:action-remove' action.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove action' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,40 @@
{% load i18n l10n fontawesome_5 %}
<div class="d-flex justify-content-end">
<a href="{{LANIS_LINK}}" class="mr-2" target="_blank">
<button class="btn btn-default" title="{% trans 'Open in LANIS' %}">
LANIS
</button>
</a>
<a href="{% url 'home' %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Public report' %}">
{% fa5_icon 'file-alt' %}
</button>
</a>
{% if has_access %}
{% if is_ets_member %}
{% if obj.recorded %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}">
{% fa5_icon 'bookmark' 'far' %}
</button>
{% else %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Record' %}" data-form-url="{% url 'ema:record' obj.id %}">
{% fa5_icon 'bookmark' %}
</button>
{% endif %}
{% endif %}
{% if is_default_member %}
<a href="{% url 'home' %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %}
</button>
</a>
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'ema:log' obj.id %}" title="{% trans 'Show log' %}">
{% fa5_icon 'history' %}
</button>
<button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}
{% endif %}
</div>

View File

@ -0,0 +1,61 @@
{% load i18n l10n fontawesome_5 %}
<div id="deadlines" class="card">
<div class="card-header rlp-r">
<div class="row">
<div class="col-sm-6">
<h5>
<span class="badge badge-light">{{obj.deadlines.count}}</span>
{% trans 'Deadlines' %}
</h5>
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'calendar-check' %}
</button>
{% endif %}
</div>
</div>
</div>
</div>
<div class="card-body scroll-300">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
{% trans 'Type' %}
</th>
<th scope="col">
{% trans 'Date' %}
</th>
<th scope="col">
{% trans 'Comment' %}
</th>
<th scope="col">
{% trans 'Action' %}
</th>
</tr>
</thead>
<tbody>
{% for deadline in obj.deadlines.all %}
<tr>
<td class="align-middle">
{% trans deadline.type_humanized %}
</td>
<td class="align-middle">{{ deadline.date }}</td>
<td class="align-middle">{{ deadline.comment }}</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'deadline-remove' deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove deadline' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,59 @@
{% load i18n l10n fontawesome_5 %}
<div id="documents" class="card">
<div class="card-header rlp-r">
<div class="row">
<div class="col-sm-6">
<h5>
<span class="badge badge-light">{{obj.documents.count}}</span>
{% trans 'Documents' %}
</h5>
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'file' %}
</button>
{% endif %}
</div>
</div>
</div>
</div>
<div class="card-body scroll-300">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
{% trans 'Title' %}
</th>
<th scope="col">
{% trans 'Comment' %}
</th>
<th scope="col">
{% trans 'Action' %}
</th>
</tr>
</thead>
<tbody>
{% for doc in obj.documents.all %}
<tr>
<td class="align-middle">
<a href="{% url 'doc-open' 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' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,62 @@
{% load i18n l10n fontawesome_5 %}
<div id="states-after" class="card">
<div class="card-header rlp-r">
<div class="row">
<div class="col-sm-6">
<h5>
<span class="badge badge-light">{{obj.after_states.count}}</span>
{% trans 'States after' %}
</h5>
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %}
</button>
{% endif %}
</div>
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states > sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
{% trans 'Biotope type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
<th scope="col">
{% trans 'Action' %}
</th>
</tr>
</thead>
<tbody>
{% for state in after_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:state-remove' state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,62 @@
{% load i18n l10n fontawesome_5 %}
<div id="states-before" class="card">
<div class="card-header rlp-r">
<div class="row">
<div class="col-sm-6">
<h5>
<span class="badge badge-light">{{obj.before_states.count}}</span>
{% trans 'States before' %}
</h5>
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %}
</button>
{% endif %}
</div>
</div>
</div>
</div>
<div class="card-body scroll-300">
{% if sum_before_states < sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces according to states after: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
<table class="table table-hover">
<thead>
<tr>
<th scope="col">
{% trans 'Biotope type' %}
</th>
<th scope="col">
{% trans 'Surface' %}
</th>
<th scope="col">
{% trans 'Action' %}
</th>
</tr>
</thead>
<tbody>
{% for state in before_states %}
<tr>
<td class="align-middle">
{{ state.biotope_type }}
</td>
<td class="align-middle">{{ state.surface|floatformat:2 }} m²</td>
<td>
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:state-remove' state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>

View File

@ -0,0 +1,100 @@
{% extends 'base.html' %}
{% load i18n l10n static fontawesome_5 humanize %}
{% block head %}
{% endblock %}
{% block body %}
<div id="detail-header" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<h3>{{obj.identifier}}</h3>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/controls.html' %}
</div>
</div>
<hr>
<div id="data" class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="table-container">
<table class="table table-hover">
<tr>
<th class="w-25" scope="row">{% trans 'Title' %}</th>
<td class="align-middle">{{obj.title}}</td>
</tr>
<tr>
<th scope="row">{% trans 'Recorded' %}</th>
<td class="align-middle">
{% if obj.recorded is None %}
<span title="{% trans 'Not recorded yet' %}">
{% fa5_icon 'bookmark' 'far' %}
</span>
{% else %}
<span class="registered-bookmark" title="{% trans 'Recorded on '%} {{obj.recorded.timestamp}} {% trans 'by' %} {{obj.recorded.user}}">
{% fa5_icon 'bookmark' %}
</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
{% if obj.modified %}
{{obj.modified.timestamp|default_if_none:""|naturalday}}
<br>
{{obj.modified.user.username}}
{% else %}
{{obj.created.timestamp|default_if_none:""|naturalday}}
<br>
{{obj.created.user.username}}
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle">
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
</td>
</tr>
</table>
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'map/geom_form.html' %}
</div>
</div>
<hr>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/states-before.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/states-after.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/deadlines.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'ema/detail/includes/documents.html' %}
</div>
</div>
{% with 'btn-modal' as btn_class %}
{% include 'modal/modal_form_script.html' %}
{% endwith %}
{% endblock %}

3
ema/tests.py Normal file
View File

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

33
ema/urls.py Normal file
View File

@ -0,0 +1,33 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 19.08.21
"""
from django.urls import path
from ema.views import *
app_name = "ema"
urlpatterns = [
path("", index_view, name="index"),
path("new/", new_view, name="new"),
path("<id>", open_view, name="open"),
path('<id>/log', log_view, name='log'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/record', record_view, name='record'),
path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/action/new', action_new_view, name='new-action'),
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/', document_new_view, name='new-doc'),
# Generic state routes
path('state/<id>/remove', state_remove_view, name='state-remove'),
# Generic action routes
path('action/<id>/remove', action_remove_view, name='action-remove'),
]

285
ema/views.py Normal file
View File

@ -0,0 +1,285 @@
from django.contrib.auth.decorators import login_required
from django.db.models import Sum
from django.http import HttpRequest
from django.shortcuts import render, get_object_or_404
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
import compensation
from compensation.forms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm
from ema.tables import EmaTable
from konova.contexts import BaseContext
from konova.decorators import conservation_office_group_required
from ema.models import Ema
from konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.utils.user_checks import in_group
@login_required
def index_view(request: HttpRequest):
""" Renders the index view for EMAs
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "generic_index.html"
emas = Ema.objects.filter(
deleted=None,
).order_by(
"-modified"
)
table = EmaTable(
request,
queryset=emas
)
context = {
"table": table,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@conservation_office_group_required
def new_view(request: HttpRequest):
""" Renders the form for a new EMA
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "generic_index.html"
context = {}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def open_view(request: HttpRequest, id: str):
""" Renders the detail view of an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA id
Returns:
"""
template = "ema/detail/view.html"
ema = get_object_or_404(Ema, id=id, deleted=None)
geom_form = SimpleGeomForm(instance=ema)
_user = request.user
is_data_shared = ema.is_shared_with(_user)
# Order states according to surface
before_states = ema.before_states.all().order_by("-surface")
after_states = ema.after_states.all().order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0
sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0
diff_states = abs(sum_before_states - sum_after_states)
context = {
"obj": ema,
"geom_form": geom_form,
"has_access": is_data_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"is_default_member": in_group(_user, DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": ema.get_LANIS_link(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def log_view(request: HttpRequest, id: str):
""" Renders a log view using modal
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
template = "modal/modal_generic.html"
body_template = "log.html"
context = {
"modal_body_template": body_template,
"log": ema.log.all(),
"modal_title": _("Log"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
def edit_view(request: HttpRequest, id: str):
get_object_or_404(Ema, id=id)
@login_required
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = RemoveModalForm(request.POST or None, instance=ema, user=request.user)
return form.process_request(
request=request,
msg_success=_("EMA removed"),
redirect_url=reverse("ema:index"),
)
@login_required
def record_view(request: HttpRequest, id: str):
""" Renders a modal view for recording the EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = RecordForm(request.POST or None, instance=ema, user=request.user)
return form.process_request(
request=request,
msg_success=_("EMA recorded"),
)
@login_required
def state_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new states for an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id to which the new state will be related
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = NewStateModalForm(request.POST or None, instance=ema, user=request.user)
return form.process_request(
request,
msg_success=_("State added")
)
@login_required
def action_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new actions for an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id to which the new state will be related
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = NewActionModalForm(request.POST or None, instance=ema, user=request.user)
return form.process_request(
request,
msg_success=_("Action added")
)
@login_required
def deadline_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new states for an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id to which the new state will be related
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = NewDeadlineModalForm(request.POST or None, instance=ema, user=request.user)
return form.process_request(
request,
msg_success=_("Deadline added")
)
@login_required
def document_new_view(request: HttpRequest, id: str):
""" Renders a form for uploading new documents
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id to which the new document will be related
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = NewDocumentForm(request.POST or None, request.FILES or None, instance=ema, user=request.user)
return form.process_request(
request,
msg_success=_("Document added")
)
@login_required
def state_remove_view(request: HttpRequest, id: str):
""" Renders a form for removing an EMA state
Args:
request (HttpRequest): The incoming request
id (str): The state's id
Returns:
"""
return compensation.views.compensation_views.state_remove_view(
request,
id
)
@login_required
def action_remove_view(request: HttpRequest, id: str):
""" Renders a form for removing an EMA state
Args:
request (HttpRequest): The incoming request
id (str): The state's id
Returns:
"""
# Reuses the route logic from compensation view
return compensation.views.compensation_views.action_remove_view(
request,
id
)

View File

@ -203,6 +203,8 @@ class EditInterventionForm(NewInterventionForm):
action=UserAction.EDITED action=UserAction.EDITED
) )
self.instance.log.add(user_action) self.instance.log.add(user_action)
self.instance.modified = user_action
self.instance.save()
return self.instance return self.instance
@ -381,6 +383,8 @@ class NewRevocationForm(BaseModalForm):
document=document, document=document,
created=created_action, created=created_action,
) )
self.instance.modified = edited_action
self.instance.save()
self.instance.log.add(edited_action) self.instance.log.add(edited_action)
self.instance.legal.revocation = revocation self.instance.legal.revocation = revocation
self.instance.legal.save() self.instance.legal.save()
@ -553,6 +557,8 @@ class NewWithdrawForm(BaseModalForm):
action=UserAction.CREATED action=UserAction.CREATED
) )
self.instance.log.add(user_action_edit) self.instance.log.add(user_action_edit)
self.instance.modified = user_action_edit
self.instance.save()
# Create withdraw depending on Intervention or EcoAccount as the initial instance # Create withdraw depending on Intervention or EcoAccount as the initial instance
if self.is_intervention_initially: if self.is_intervention_initially:

View File

@ -21,9 +21,9 @@ class ResponsibilityData(UuidModel):
Holds intervention data about responsible organizations and their file numbers for this case Holds intervention data about responsible organizations and their file numbers for this case
""" """
registration_office = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, related_name="+") registration_office = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, related_name="+", blank=True)
registration_file_number = models.CharField(max_length=1000, blank=True, null=True) registration_file_number = models.CharField(max_length=1000, blank=True, null=True)
conservation_office = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, related_name="+") conservation_office = models.ForeignKey(Organisation, on_delete=models.SET_NULL, null=True, related_name="+", blank=True)
conservation_file_number = models.CharField(max_length=1000, blank=True, null=True) conservation_file_number = models.CharField(max_length=1000, blank=True, null=True)
handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher'") handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher'")
@ -115,9 +115,6 @@ class Intervention(BaseObject):
related_name="+" related_name="+"
) )
# Holds which intervention is simply a newer version of this dataset
next_version = models.ForeignKey("Intervention", null=True, blank=True, on_delete=models.DO_NOTHING)
# Users having access on this object # Users having access on this object
users = models.ManyToManyField(User, help_text="Users having access (data shared with)") users = models.ManyToManyField(User, help_text="Users having access (data shared with)")
access_token = models.CharField( access_token = models.CharField(
@ -177,7 +174,7 @@ class Intervention(BaseObject):
""" Quality check """ Quality check
Returns: Returns:
ret_msgs (list): True if quality acceptable, False otherwise ret_msgs (list): Holds error messages
""" """
ret_msgs = [] ret_msgs = []

View File

@ -50,7 +50,7 @@ class InterventionTable(BaseTable):
lm = tables.Column( lm = tables.Column(
verbose_name=_("Last edit"), verbose_name=_("Last edit"),
orderable=True, orderable=True,
accessor="created__timestamp", accessor="modified__timestamp",
) )
""" """
# ToDo: Decide to keep actions column or to dismiss them # ToDo: Decide to keep actions column or to dismiss them
@ -164,23 +164,3 @@ class InterventionTable(BaseTable):
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit", icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
) )
return format_html(html) return format_html(html)
def render_ac(self, value, record):
"""
Renders possible actions for this record, such as delete.
"""
intervention = _("Intervention")
html = ""
html += self.render_open_btn(
_("Open {}").format(intervention),
reverse("intervention:open", args=(record.id,))
)
html += self.render_edit_btn(
_("Edit {}").format(intervention),
reverse("intervention:edit", args=(record.id,)),
)
html += self.render_delete_btn(
_("Delete {}").format(intervention),
reverse("intervention:remove", args=(record.id,)),
)
return format_html(html)

View File

@ -94,7 +94,7 @@
<th scope="row">{% trans 'Binding on' %}</th> <th scope="row">{% trans 'Binding on' %}</th>
<td class="align-middle">{{intervention.legal.binding_date|default_if_none:""}}</td> <td class="align-middle">{{intervention.legal.binding_date|default_if_none:""}}</td>
</tr> </tr>
<tr {% if intervention.legal.revocation %}class="alert alert-danger" title="{% trans 'Missing' %}" {% endif %}> <tr {% if intervention.legal.revocation %}class="alert alert-danger" title="{% trans 'Exists' %}" {% endif %}>
<th scope="row">{% trans 'Revocation' %}</th> <th scope="row">{% trans 'Revocation' %}</th>
<td class="align-middle">{{intervention.legal.revocation.date|naturalday|default_if_none:""}}</td> <td class="align-middle">{{intervention.legal.revocation.date|naturalday|default_if_none:""}}</td>
</tr> </tr>

View File

@ -33,9 +33,6 @@ def index_view(request: HttpRequest):
# Filtering by user access is performed in table filter inside of InterventionTableFilter class # Filtering by user access is performed in table filter inside of InterventionTableFilter class
interventions = Intervention.objects.filter( interventions = Intervention.objects.filter(
deleted=None, # not deleted deleted=None, # not deleted
next_version=None, # only newest versions
).order_by(
"-created__timestamp"
) )
table = InterventionTable( table = InterventionTable(
request=request, request=request,
@ -273,22 +270,10 @@ def create_share_view(request: HttpRequest, id: str):
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
form = ShareInterventionForm(request.POST or None, instance=intervention, request=request) form = ShareInterventionForm(request.POST or None, instance=intervention, request=request)
if request.method == "POST": return form.process_request(
if form.is_valid(): request,
form.save() msg_success=_("Share settings updated")
messages.info( )
request,
_("Share settings updated")
)
return redirect(request.META.get("HTTP_REFERER", "home"))
elif request.method == "GET":
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, form.template, context)
else:
raise NotImplementedError
@login_required @login_required
@ -324,28 +309,10 @@ def new_revocation_view(request: HttpRequest, id: str):
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
form = NewRevocationForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user) form = NewRevocationForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user)
if request.method == "POST": return form.process_request(
if form.is_valid(): request,
form.save() msg_success=_("Revocation added")
messages.info( )
request,
_("Revocation added")
)
else:
messages.error(
request,
FORM_INVALID,
extra_tags="danger",
)
return redirect(request.META.get("HTTP_REFERER", "home"))
elif request.method == "GET":
context = {
"form": form,
}
context = BaseContext(request, context).context
return render(request, form.template, context)
else:
raise NotImplementedError
@login_required @login_required
@ -365,7 +332,7 @@ def log_view(request: HttpRequest, id: str):
context = { context = {
"modal_body_template": body_template, "modal_body_template": body_template,
"log": intervention.log.all().order_by("-timestamp"), "log": intervention.log.all(),
"modal_title": _("Log"), "modal_title": _("Log"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context

View File

@ -22,6 +22,7 @@ from django.utils import timezone
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount from compensation.models import EcoAccount
from ema.models import Ema
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.models import Document, BaseObject from konova.models import Document, BaseObject
@ -45,7 +46,6 @@ class BaseForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance", None) self.instance = kwargs.pop("instance", None)
self.user = kwargs.pop("user", None) self.user = kwargs.pop("user", None)
self.request = kwargs.pop("request", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Check for required fields # Check for required fields
@ -173,6 +173,10 @@ class BaseModalForm(BaseForm, BSModalForm):
if request.method == "POST": if request.method == "POST":
if self.is_valid(): if self.is_valid():
if not is_ajax(request.META): if not is_ajax(request.META):
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
# on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
# an ajax call, the second is a regular form POST.
self.save() self.save()
messages.success( messages.success(
request, request,
@ -337,6 +341,8 @@ class NewDocumentForm(BaseModalForm):
comment=_("Added document"), comment=_("Added document"),
) )
self.instance.log.add(edited_action) self.instance.log.add(edited_action)
self.instance.modified = edited_action
self.instance.save()
return doc return doc
@ -365,7 +371,8 @@ class RecordForm(BaseModalForm):
implemented_cls_logic = { implemented_cls_logic = {
Intervention, Intervention,
EcoAccount EcoAccount,
Ema,
} }
instance_name = self.instance.__class__ instance_name = self.instance.__class__
if instance_name not in implemented_cls_logic: if instance_name not in implemented_cls_logic:

View File

@ -16,6 +16,7 @@ from django.db import models, transaction
from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH, \ from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH, \
ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH 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 intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE
from konova.utils.generators import generate_random_string from konova.utils.generators import generate_random_string
from user.models import UserActionLogEntry, UserAction from user.models import UserActionLogEntry, UserAction
@ -39,7 +40,21 @@ class BaseResource(UuidModel):
""" """
A basic resource model, which defines attributes for every derived model A basic resource model, which defines attributes for every derived model
""" """
created = models.ForeignKey(UserActionLogEntry, on_delete=models.SET_NULL, null=True, blank=True, related_name='+') created = models.ForeignKey(
UserActionLogEntry,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='+'
)
modified = models.ForeignKey(
UserActionLogEntry,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='+',
help_text="Last modified"
)
class Meta: class Meta:
abstract = True abstract = True
@ -131,6 +146,8 @@ class BaseObject(BaseResource):
""" """
from compensation.models import Compensation, EcoAccount from compensation.models import Compensation, EcoAccount
from intervention.models import Intervention from intervention.models import Intervention
from ema.models import Ema
definitions = { definitions = {
Intervention: { Intervention: {
"length": INTERVENTION_IDENTIFIER_LENGTH, "length": INTERVENTION_IDENTIFIER_LENGTH,
@ -144,6 +161,10 @@ class BaseObject(BaseResource):
"length": ECO_ACCOUNT_IDENTIFIER_LENGTH, "length": ECO_ACCOUNT_IDENTIFIER_LENGTH,
"template": ECO_ACCOUNT_IDENTIFIER_TEMPLATE, "template": ECO_ACCOUNT_IDENTIFIER_TEMPLATE,
}, },
Ema: {
"length": EMA_ACCOUNT_IDENTIFIER_LENGTH,
"template": EMA_ACCOUNT_IDENTIFIER_TEMPLATE,
},
} }
if self.__class__ not in definitions: if self.__class__ not in definitions:

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -68,6 +68,7 @@ INSTALLED_APPS = [
'organisation', 'organisation',
'news', 'news',
'user', 'user',
'ema',
] ]
if DEBUG: if DEBUG:
INSTALLED_APPS += [ INSTALLED_APPS += [

View File

@ -31,7 +31,7 @@ urlpatterns = [
path('', home_view, name="home"), path('', home_view, name="home"),
path('intervention/', include("intervention.urls")), path('intervention/', include("intervention.urls")),
path('compensation/', include("compensation.urls")), path('compensation/', include("compensation.urls")),
path('ema/', include("intervention.urls")), #ToDo path('ema/', include("ema.urls")),
path('organisation/', include("organisation.urls")), path('organisation/', include("organisation.urls")),
path('user/', include("user.urls")), path('user/', include("user.urls")),
path('news/', include("news.urls")), path('news/', include("news.urls")),

View File

@ -24,10 +24,11 @@ class BaseTable(tables.tables.Table):
add_new_entries = True add_new_entries = True
add_new_url = None add_new_url = None
title = None title = None
subtitle = ""
class Meta: class Meta:
attrs = { attrs = {
"class": "table table-hover", "class": "table table-hover table-responsive-md table-responsive-sm",
} }
def __init__(self, request: HttpRequest = None, filter_set=None, queryset=None, *args, **kwargs): def __init__(self, request: HttpRequest = None, filter_set=None, queryset=None, *args, **kwargs):

View File

@ -64,7 +64,6 @@ def home_view(request: HttpRequest):
# First fetch all valid objects (undeleted, only newest versions) # First fetch all valid objects (undeleted, only newest versions)
interventions = Intervention.objects.filter( interventions = Intervention.objects.filter(
deleted=None, deleted=None,
next_version=None,
) )
# Then fetch only user related ones # Then fetch only user related ones
user_interventions = interventions.filter( user_interventions = interventions.filter(
@ -74,14 +73,12 @@ def home_view(request: HttpRequest):
# Repeat for other objects # Repeat for other objects
comps = Compensation.objects.filter( comps = Compensation.objects.filter(
deleted=None, deleted=None,
next_version=None,
) )
user_comps = comps.filter( user_comps = comps.filter(
intervention__users__in=[user] intervention__users__in=[user]
) )
eco_accs = EcoAccount.objects.filter( eco_accs = EcoAccount.objects.filter(
deleted=None, deleted=None,
next_version=None,
) )
user_ecco_accs = eco_accs.filter( user_ecco_accs = eco_accs.filter(
users__in=[user] users__in=[user]

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -3,7 +3,9 @@
<html lang="{{ language }}"> <html lang="{{ language }}">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<title>{{ base_title }}</title> <meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ base_frontend_title }}</title>
<link rel="icon" type="image/ico" href="{% static 'images/ksp-favicon.ico' %}">
{% bootstrap_css %} {% bootstrap_css %}
{% bootstrap_javascript jquery='full' %} {% bootstrap_javascript jquery='full' %}
{% fontawesome_5_static %} {% fontawesome_5_static %}

View File

@ -5,12 +5,21 @@
{% block body %} {% block body %}
<div class="col-md"> <div class="col-md">
{% if table.title is not None %} {% if table.title %}
<div class="row"> <div class="row">
<h3> <h3>
{{ table.title }} {{ table.title }}
</h3> </h3>
</div> </div>
{% if table.subtitle %}
<div class="row mb-2">
<div class="col-lg">
<small>
{{ table.subtitle }}
</small>
</div>
</div>
{% endif %}
{% endif %} {% endif %}
<div class="row"> <div class="row">
{% if table.add_new_entries %} {% if table.add_new_entries %}

View File

@ -34,13 +34,13 @@
{% trans 'Eco-account' %} {% trans 'Eco-account' %}
</a> </a>
</li> </li>
<li class=" menu-elem dropdown" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false"> <li class=" menu-elem dropdown">
<a class="nav-btn nav-link"> <div class="btn nav-btn" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
{% fa5_icon 'ellipsis-v' %} {% fa5_icon 'ellipsis-v' %}
{% trans 'More' %} {% trans 'More' %}
</a> </div>
<div class="dropdown-menu"> <div class="dropdown-menu">
<a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'euro-sign' %} {% trans 'EMA' %}</a> <a class="dropdown-item" href="{% url 'ema:index' %}" title="{% trans 'Payment funded compensations' %}">{% fa5_icon 'euro-sign' %} {% trans 'EMA' %}</a>
<a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-import' %} {% trans 'Import...' %}</a> <a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-import' %} {% trans 'Import...' %}</a>
<a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-export' %} {% trans 'Export...' %}</a> <a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-export' %} {% trans 'Export...' %}</a>
<a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-alt' %} {% trans 'Reports' %}</a> <a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-alt' %} {% trans 'Reports' %}</a>

View File

@ -74,6 +74,11 @@ class UserActionLogEntry(models.Model):
) )
comment = models.CharField(max_length=255, null=True, blank=True, help_text="Additional comment on this entry") comment = models.CharField(max_length=255, null=True, blank=True, help_text="Additional comment on this entry")
class Meta:
ordering = (
"-timestamp",
)
def __str__(self): def __str__(self):
return "{} | {} | {}".format(self.user.username, self.timestamp, self.action) return "{} | {} | {}".format(self.user.username, self.timestamp, self.action)

View File

@ -4,7 +4,7 @@
{% block body %} {% block body %}
<div class="row"> <div class="row">
<div class="col-md-6 col-lg-4 border"> <div class="col-md-6 col-lg-4 border">
<table class="table"> <table class="table table-responsive">
<tr> <tr>
<th scope="row">{% trans 'Username' %}</th> <th scope="row">{% trans 'Username' %}</th>
<td>{{user.username}}</td> <td>{{user.username}}</td>