Compare commits
5 Commits
c82ee8afbc
...
615b7bf5ea
Author | SHA1 | Date | |
---|---|---|---|
|
615b7bf5ea | ||
|
d1f43f8c64 | ||
|
03fe293cd8 | ||
|
198462b051 | ||
|
7dbf4d5b91 |
@ -5,11 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 04.12.20
|
||||
|
||||
"""
|
||||
from bootstrap_modal_forms.utils import is_ajax
|
||||
from django import forms
|
||||
from django.contrib import messages
|
||||
from django.db import transaction
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import redirect, render
|
||||
from django.http import HttpRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.models import Payment, CompensationState, CompensationAction, UnitChoices
|
||||
@ -88,6 +89,8 @@ class NewPaymentForm(BaseModalForm):
|
||||
intervention=self.intervention,
|
||||
)
|
||||
self.intervention.log.add(edited_action)
|
||||
self.intervention.modified = edited_action
|
||||
self.intervention.save()
|
||||
return pay
|
||||
|
||||
|
||||
@ -121,6 +124,8 @@ class NewStateModalForm(BaseModalForm):
|
||||
comment=_("Added state")
|
||||
)
|
||||
self.instance.log.add(user_action)
|
||||
self.instance.modified = user_action
|
||||
self.instance.save()
|
||||
|
||||
state = CompensationState.objects.create(
|
||||
biotope_type=self.cleaned_data["biotope_type"],
|
||||
@ -153,19 +158,24 @@ class NewStateModalForm(BaseModalForm):
|
||||
template = self.template
|
||||
if request.method == "POST":
|
||||
if self.is_valid():
|
||||
is_before_state = bool(request.GET.get("before", False))
|
||||
self.save(is_before_state=is_before_state)
|
||||
messages.success(
|
||||
request,
|
||||
msg_success
|
||||
)
|
||||
return redirect(redirect_url)
|
||||
# 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.
|
||||
if not is_ajax(request.META):
|
||||
is_before_state = bool(request.GET.get("before", False))
|
||||
self.save(is_before_state=is_before_state)
|
||||
messages.success(
|
||||
request,
|
||||
msg_success
|
||||
)
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
else:
|
||||
messages.info(
|
||||
request,
|
||||
msg_error
|
||||
)
|
||||
return redirect(redirect_url)
|
||||
context = {
|
||||
"form": self,
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
elif request.method == "GET":
|
||||
context = {
|
||||
"form": self,
|
||||
@ -238,6 +248,8 @@ class NewDeadlineModalForm(BaseModalForm):
|
||||
action=UserAction.EDITED,
|
||||
comment=_("Added deadline")
|
||||
)
|
||||
self.instance.modified = edited_action
|
||||
self.instance.save()
|
||||
self.instance.log.add(edited_action)
|
||||
self.instance.deadlines.add(deadline)
|
||||
return deadline
|
||||
@ -308,6 +320,8 @@ class NewActionModalForm(BaseModalForm):
|
||||
action=UserAction.EDITED,
|
||||
comment=_("Added action"),
|
||||
)
|
||||
self.instance.modified = edited_action
|
||||
self.instance.save()
|
||||
self.instance.log.add(edited_action)
|
||||
self.instance.actions.add(comp_action)
|
||||
return comp_action
|
||||
|
@ -91,8 +91,8 @@ class CompensationAction(BaseResource):
|
||||
|
||||
class AbstractCompensation(BaseObject):
|
||||
"""
|
||||
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation
|
||||
or EcoAccount.
|
||||
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
|
||||
EMA or EcoAccount.
|
||||
|
||||
"""
|
||||
responsible = models.OneToOneField(
|
||||
@ -112,9 +112,6 @@ class AbstractCompensation(BaseObject):
|
||||
geometry = models.ForeignKey(Geometry, null=True, blank=True, on_delete=models.SET_NULL)
|
||||
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:
|
||||
abstract = True
|
||||
|
||||
@ -260,9 +257,17 @@ class EcoAccount(AbstractCompensation):
|
||||
y,
|
||||
)
|
||||
|
||||
def quality_check(self) -> (bool, dict):
|
||||
# ToDo
|
||||
pass
|
||||
def quality_check(self) -> list:
|
||||
""" Quality check
|
||||
|
||||
Returns:
|
||||
ret_msgs (list): Holds error messages
|
||||
"""
|
||||
ret_msgs = []
|
||||
|
||||
# ToDo: Add check methods!
|
||||
|
||||
return ret_msgs
|
||||
|
||||
|
||||
class EcoAccountWithdraw(BaseResource):
|
||||
|
@ -7,5 +7,6 @@ Created on: 18.12.20
|
||||
"""
|
||||
COMPENSATION_IDENTIFIER_LENGTH = 10
|
||||
COMPENSATION_IDENTIFIER_TEMPLATE = "KOM-{}"
|
||||
|
||||
ECO_ACCOUNT_IDENTIFIER_LENGTH = 10
|
||||
ECO_ACCOUNT_IDENTIFIER_TEMPLATE = "OEK-{}"
|
@ -5,7 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 01.12.20
|
||||
|
||||
"""
|
||||
from django.db.models import Sum
|
||||
from django.http import HttpRequest
|
||||
from django.template.loader import render_to_string
|
||||
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.models import Compensation, EcoAccount
|
||||
from intervention.filters import InterventionTableFilter
|
||||
from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
|
||||
from konova.utils.tables import BaseTable
|
||||
import django_tables2 as tables
|
||||
@ -53,7 +51,7 @@ class CompensationTable(BaseTable):
|
||||
lm = tables.Column(
|
||||
verbose_name=_("Last edit"),
|
||||
orderable=True,
|
||||
accessor="created__timestamp",
|
||||
accessor="modified__timestamp",
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
@ -126,21 +124,21 @@ class CompensationTable(BaseTable):
|
||||
|
||||
"""
|
||||
html = ""
|
||||
checked = value is not None
|
||||
recorded = value is not None
|
||||
tooltip = _("Not recorded yet")
|
||||
if checked:
|
||||
if recorded:
|
||||
value = value.timestamp
|
||||
value = localtime(value)
|
||||
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
|
||||
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
|
||||
html += self.render_bookmark(
|
||||
tooltip=tooltip,
|
||||
icn_filled=checked,
|
||||
icn_filled=recorded,
|
||||
)
|
||||
return format_html(html)
|
||||
|
||||
def render_e(self, value, record: Compensation):
|
||||
""" Renders the registered column for a compensation
|
||||
""" Renders the editable column for a compensation
|
||||
|
||||
Args:
|
||||
value (str): The identifier value
|
||||
@ -192,7 +190,7 @@ class EcoAccountTable(BaseTable):
|
||||
lm = tables.Column(
|
||||
verbose_name=_("Last edit"),
|
||||
orderable=True,
|
||||
accessor="created__timestamp",
|
||||
accessor="modified__timestamp",
|
||||
)
|
||||
|
||||
class Meta(BaseTable.Meta):
|
||||
|
@ -63,9 +63,9 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
{{obj.created.timestamp|default_if_none:""|naturalday}}
|
||||
{{obj.modified.timestamp|default_if_none:""|naturalday}}
|
||||
<br>
|
||||
{{obj.created.user.username}}
|
||||
{{obj.modified.user.username}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -55,9 +55,9 @@
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Last modified' %}</th>
|
||||
<td class="align-middle">
|
||||
{{obj.created.timestamp|default_if_none:""|naturalday}}
|
||||
{{obj.modified.timestamp|default_if_none:""|naturalday}}
|
||||
<br>
|
||||
{{obj.created.user.username}}
|
||||
{{obj.modified.user.username}}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -118,7 +118,7 @@ def log_view(request: HttpRequest, id: str):
|
||||
|
||||
context = {
|
||||
"modal_body_template": body_template,
|
||||
"log": comp.log.all().order_by("-timestamp"),
|
||||
"log": comp.log.all(),
|
||||
"modal_title": _("Log"),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
@ -141,7 +141,7 @@ def remove_view(request: HttpRequest, id: str):
|
||||
return form.process_request(
|
||||
request=request,
|
||||
msg_success=_("Compensation removed"),
|
||||
redirect_url="",
|
||||
redirect_url=reverse("compensation:index"),
|
||||
)
|
||||
|
||||
|
||||
|
@ -180,7 +180,7 @@ def log_view(request: HttpRequest, id: str):
|
||||
|
||||
context = {
|
||||
"modal_body_template": body_template,
|
||||
"log": comp.log.all().order_by("-timestamp"),
|
||||
"log": comp.log.all(),
|
||||
"modal_title": _("Log"),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
|
@ -6,18 +6,15 @@ Created on: 09.08.21
|
||||
|
||||
"""
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
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.models import Payment
|
||||
from intervention.models import Intervention
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import default_group_required
|
||||
from konova.forms import RemoveModalForm
|
||||
from konova.utils.message_templates import FORM_INVALID
|
||||
|
||||
|
||||
@login_required
|
||||
@ -34,29 +31,10 @@ def new_payment_view(request: HttpRequest, intervention_id: str):
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=intervention_id)
|
||||
form = NewPaymentForm(request.POST or None, instance=intervention, user=request.user)
|
||||
template = form.template
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
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
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=_("Payment added")
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
|
0
ema/__init__.py
Normal file
0
ema/__init__.py
Normal file
10
ema/admin.py
Normal file
10
ema/admin.py
Normal 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
5
ema/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class EmaConfig(AppConfig):
|
||||
name = 'ema'
|
53
ema/filters.py
Normal file
53
ema/filters.py
Normal 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
86
ema/models.py
Normal 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
10
ema/settings.py
Normal 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
132
ema/tables.py
Normal 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)
|
61
ema/templates/ema/detail/includes/actions.html
Normal file
61
ema/templates/ema/detail/includes/actions.html
Normal 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>
|
40
ema/templates/ema/detail/includes/controls.html
Normal file
40
ema/templates/ema/detail/includes/controls.html
Normal 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>
|
61
ema/templates/ema/detail/includes/deadlines.html
Normal file
61
ema/templates/ema/detail/includes/deadlines.html
Normal 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>
|
59
ema/templates/ema/detail/includes/documents.html
Normal file
59
ema/templates/ema/detail/includes/documents.html
Normal 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>
|
62
ema/templates/ema/detail/includes/states-after.html
Normal file
62
ema/templates/ema/detail/includes/states-after.html
Normal 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>
|
62
ema/templates/ema/detail/includes/states-before.html
Normal file
62
ema/templates/ema/detail/includes/states-before.html
Normal 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>
|
100
ema/templates/ema/detail/view.html
Normal file
100
ema/templates/ema/detail/view.html
Normal 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
3
ema/tests.py
Normal file
@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
33
ema/urls.py
Normal file
33
ema/urls.py
Normal 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
285
ema/views.py
Normal 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
|
||||
)
|
||||
|
@ -203,6 +203,8 @@ class EditInterventionForm(NewInterventionForm):
|
||||
action=UserAction.EDITED
|
||||
)
|
||||
self.instance.log.add(user_action)
|
||||
self.instance.modified = user_action
|
||||
self.instance.save()
|
||||
|
||||
return self.instance
|
||||
|
||||
@ -381,6 +383,8 @@ class NewRevocationForm(BaseModalForm):
|
||||
document=document,
|
||||
created=created_action,
|
||||
)
|
||||
self.instance.modified = edited_action
|
||||
self.instance.save()
|
||||
self.instance.log.add(edited_action)
|
||||
self.instance.legal.revocation = revocation
|
||||
self.instance.legal.save()
|
||||
@ -553,6 +557,8 @@ class NewWithdrawForm(BaseModalForm):
|
||||
action=UserAction.CREATED
|
||||
)
|
||||
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
|
||||
if self.is_intervention_initially:
|
||||
|
@ -21,9 +21,9 @@ class ResponsibilityData(UuidModel):
|
||||
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)
|
||||
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)
|
||||
handler = models.CharField(max_length=500, null=True, blank=True, help_text="Refers to 'Eingriffsverursacher'")
|
||||
|
||||
@ -115,9 +115,6 @@ class Intervention(BaseObject):
|
||||
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 = models.ManyToManyField(User, help_text="Users having access (data shared with)")
|
||||
access_token = models.CharField(
|
||||
@ -177,7 +174,7 @@ class Intervention(BaseObject):
|
||||
""" Quality check
|
||||
|
||||
Returns:
|
||||
ret_msgs (list): True if quality acceptable, False otherwise
|
||||
ret_msgs (list): Holds error messages
|
||||
"""
|
||||
ret_msgs = []
|
||||
|
||||
|
@ -50,7 +50,7 @@ class InterventionTable(BaseTable):
|
||||
lm = tables.Column(
|
||||
verbose_name=_("Last edit"),
|
||||
orderable=True,
|
||||
accessor="created__timestamp",
|
||||
accessor="modified__timestamp",
|
||||
)
|
||||
"""
|
||||
# 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",
|
||||
)
|
||||
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)
|
||||
|
@ -94,7 +94,7 @@
|
||||
<th scope="row">{% trans 'Binding on' %}</th>
|
||||
<td class="align-middle">{{intervention.legal.binding_date|default_if_none:""}}</td>
|
||||
</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>
|
||||
<td class="align-middle">{{intervention.legal.revocation.date|naturalday|default_if_none:""}}</td>
|
||||
</tr>
|
||||
|
@ -33,9 +33,6 @@ def index_view(request: HttpRequest):
|
||||
# Filtering by user access is performed in table filter inside of InterventionTableFilter class
|
||||
interventions = Intervention.objects.filter(
|
||||
deleted=None, # not deleted
|
||||
next_version=None, # only newest versions
|
||||
).order_by(
|
||||
"-created__timestamp"
|
||||
)
|
||||
table = InterventionTable(
|
||||
request=request,
|
||||
@ -273,22 +270,10 @@ def create_share_view(request: HttpRequest, id: str):
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
form = ShareInterventionForm(request.POST or None, instance=intervention, request=request)
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
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
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=_("Share settings updated")
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@ -324,28 +309,10 @@ def new_revocation_view(request: HttpRequest, id: str):
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
form = NewRevocationForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user)
|
||||
if request.method == "POST":
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
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
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=_("Revocation added")
|
||||
)
|
||||
|
||||
|
||||
@login_required
|
||||
@ -365,7 +332,7 @@ def log_view(request: HttpRequest, id: str):
|
||||
|
||||
context = {
|
||||
"modal_body_template": body_template,
|
||||
"log": intervention.log.all().order_by("-timestamp"),
|
||||
"log": intervention.log.all(),
|
||||
"modal_title": _("Log"),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
|
@ -22,6 +22,7 @@ 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 konova.contexts import BaseContext
|
||||
from konova.models import Document, BaseObject
|
||||
@ -45,7 +46,6 @@ class BaseForm(forms.Form):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.pop("instance", None)
|
||||
self.user = kwargs.pop("user", None)
|
||||
self.request = kwargs.pop("request", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Check for required fields
|
||||
@ -173,6 +173,10 @@ class BaseModalForm(BaseForm, BSModalForm):
|
||||
if request.method == "POST":
|
||||
if self.is_valid():
|
||||
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()
|
||||
messages.success(
|
||||
request,
|
||||
@ -337,6 +341,8 @@ class NewDocumentForm(BaseModalForm):
|
||||
comment=_("Added document"),
|
||||
)
|
||||
self.instance.log.add(edited_action)
|
||||
self.instance.modified = edited_action
|
||||
self.instance.save()
|
||||
|
||||
return doc
|
||||
|
||||
@ -365,7 +371,8 @@ class RecordForm(BaseModalForm):
|
||||
|
||||
implemented_cls_logic = {
|
||||
Intervention,
|
||||
EcoAccount
|
||||
EcoAccount,
|
||||
Ema,
|
||||
}
|
||||
instance_name = self.instance.__class__
|
||||
if instance_name not in implemented_cls_logic:
|
||||
|
@ -16,6 +16,7 @@ from django.db import models, transaction
|
||||
|
||||
from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_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 konova.utils.generators import generate_random_string
|
||||
from user.models import UserActionLogEntry, UserAction
|
||||
@ -39,7 +40,21 @@ class BaseResource(UuidModel):
|
||||
"""
|
||||
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:
|
||||
abstract = True
|
||||
@ -131,6 +146,8 @@ class BaseObject(BaseResource):
|
||||
"""
|
||||
from compensation.models import Compensation, EcoAccount
|
||||
from intervention.models import Intervention
|
||||
from ema.models import Ema
|
||||
|
||||
definitions = {
|
||||
Intervention: {
|
||||
"length": INTERVENTION_IDENTIFIER_LENGTH,
|
||||
@ -144,6 +161,10 @@ class BaseObject(BaseResource):
|
||||
"length": ECO_ACCOUNT_IDENTIFIER_LENGTH,
|
||||
"template": ECO_ACCOUNT_IDENTIFIER_TEMPLATE,
|
||||
},
|
||||
Ema: {
|
||||
"length": EMA_ACCOUNT_IDENTIFIER_LENGTH,
|
||||
"template": EMA_ACCOUNT_IDENTIFIER_TEMPLATE,
|
||||
},
|
||||
}
|
||||
|
||||
if self.__class__ not in definitions:
|
||||
|
BIN
konova/static/images/ksp-favicon.ico
Normal file
BIN
konova/static/images/ksp-favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.4 KiB |
@ -68,6 +68,7 @@ INSTALLED_APPS = [
|
||||
'organisation',
|
||||
'news',
|
||||
'user',
|
||||
'ema',
|
||||
]
|
||||
if DEBUG:
|
||||
INSTALLED_APPS += [
|
||||
|
@ -31,7 +31,7 @@ urlpatterns = [
|
||||
path('', home_view, name="home"),
|
||||
path('intervention/', include("intervention.urls")),
|
||||
path('compensation/', include("compensation.urls")),
|
||||
path('ema/', include("intervention.urls")), #ToDo
|
||||
path('ema/', include("ema.urls")),
|
||||
path('organisation/', include("organisation.urls")),
|
||||
path('user/', include("user.urls")),
|
||||
path('news/', include("news.urls")),
|
||||
|
@ -24,10 +24,11 @@ class BaseTable(tables.tables.Table):
|
||||
add_new_entries = True
|
||||
add_new_url = None
|
||||
title = None
|
||||
subtitle = ""
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
|
@ -64,7 +64,6 @@ def home_view(request: HttpRequest):
|
||||
# First fetch all valid objects (undeleted, only newest versions)
|
||||
interventions = Intervention.objects.filter(
|
||||
deleted=None,
|
||||
next_version=None,
|
||||
)
|
||||
# Then fetch only user related ones
|
||||
user_interventions = interventions.filter(
|
||||
@ -74,14 +73,12 @@ def home_view(request: HttpRequest):
|
||||
# Repeat for other objects
|
||||
comps = Compensation.objects.filter(
|
||||
deleted=None,
|
||||
next_version=None,
|
||||
)
|
||||
user_comps = comps.filter(
|
||||
intervention__users__in=[user]
|
||||
)
|
||||
eco_accs = EcoAccount.objects.filter(
|
||||
deleted=None,
|
||||
next_version=None,
|
||||
)
|
||||
user_ecco_accs = eco_accs.filter(
|
||||
users__in=[user]
|
||||
|
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@ -3,7 +3,9 @@
|
||||
<html lang="{{ language }}">
|
||||
<head>
|
||||
<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_javascript jquery='full' %}
|
||||
{% fontawesome_5_static %}
|
||||
|
@ -5,12 +5,21 @@
|
||||
|
||||
{% block body %}
|
||||
<div class="col-md">
|
||||
{% if table.title is not None %}
|
||||
<div class="row">
|
||||
<h3>
|
||||
{{ table.title }}
|
||||
</h3>
|
||||
</div>
|
||||
{% if table.title %}
|
||||
<div class="row">
|
||||
<h3>
|
||||
{{ table.title }}
|
||||
</h3>
|
||||
</div>
|
||||
{% if table.subtitle %}
|
||||
<div class="row mb-2">
|
||||
<div class="col-lg">
|
||||
<small>
|
||||
{{ table.subtitle }}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<div class="row">
|
||||
{% if table.add_new_entries %}
|
||||
|
@ -34,13 +34,13 @@
|
||||
{% trans 'Eco-account' %}
|
||||
</a>
|
||||
</li>
|
||||
<li class=" menu-elem dropdown" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
<a class="nav-btn nav-link">
|
||||
<li class=" menu-elem dropdown">
|
||||
<div class="btn nav-btn" data-toggle="dropdown" role="button" aria-haspopup="true" aria-expanded="false">
|
||||
{% fa5_icon 'ellipsis-v' %}
|
||||
{% trans 'More' %}
|
||||
</a>
|
||||
</div>
|
||||
<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-export' %} {% trans 'Export...' %}</a>
|
||||
<a class="dropdown-item" href="{% url 'home' %}">{% fa5_icon 'file-alt' %} {% trans 'Reports' %}</a>
|
||||
|
@ -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")
|
||||
|
||||
class Meta:
|
||||
ordering = (
|
||||
"-timestamp",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "{} | {} | {}".format(self.user.username, self.timestamp, self.action)
|
||||
|
||||
|
@ -4,7 +4,7 @@
|
||||
{% block body %}
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-lg-4 border">
|
||||
<table class="table">
|
||||
<table class="table table-responsive">
|
||||
<tr>
|
||||
<th scope="row">{% trans 'Username' %}</th>
|
||||
<td>{{user.username}}</td>
|
||||
|
Loading…
Reference in New Issue
Block a user