Compensation detail view

* adds compensation detail view (WIP)
* adds includes dir for related objects, similar to interventions
* adds functionality for
   * adding/removing before_states
   * adding/removing after_states
   * adding/removing deadlines
   * adding/removing documents
* refactors usage of BaseModalForm
   * holds now process_request() in base class for generic usage anywhere
* adds __str__() method for some models
* compensation__action is blank=True now
* renamed tooltips
* adds new routes for state/deadline/document handling inside of compensation/urls.py
* adds precalculation of before/after_states for detail view, so users will see directly if there are missing states
* removes unnecessary link for intervention detail payment
* adds missing tooltips for check and record icon on detail views
* refactors DeadlineTypeEnum into DeadlineType in konova/models.py, just as the django 3.x documentation suggests for model enumerations
* UuidModel id field is not editable anymore in the admin interface
* adds/updates translations
This commit is contained in:
mipel
2021-08-03 13:13:01 +02:00
parent a06b532108
commit 881edaeba6
22 changed files with 1010 additions and 236 deletions

View File

@@ -9,9 +9,10 @@ from django import forms
from django.db import transaction
from django.utils.translation import gettext_lazy as _
from compensation.models import Payment
from compensation.models import Payment, CompensationState
from konova.enums import UserActionLogEntryEnum
from konova.forms import BaseForm, BaseModalForm
from konova.models import Deadline, DeadlineType
from user.models import UserActionLogEntry
@@ -69,4 +70,94 @@ class NewPaymentForm(BaseModalForm):
comment=self.cleaned_data.get("transfer_note", None),
intervention=self.intervention,
)
return pay
return pay
class NewStateModalForm(BaseModalForm):
biotope_type = forms.CharField(
label=_("Biotope Type"),
label_suffix="",
required=True,
help_text=_("Select the biotope type")
)
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Surface"),
label_suffix="",
required=True,
help_text=_("in m²")
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New state")
self.form_caption = _("Insert data for the new state")
def save(self, is_before_state: bool = False):
with transaction.atomic():
state = CompensationState.objects.create(
biotope_type=self.cleaned_data["biotope_type"],
surface=self.cleaned_data["surface"],
)
if is_before_state:
self.instance.before_states.add(state)
else:
self.instance.after_states.add(state)
return state
class NewDeadlineModalForm(BaseModalForm):
type = forms.ChoiceField(
label=_("Deadline Type"),
label_suffix="",
required=True,
help_text=_("Select the deadline type"),
choices=DeadlineType.choices
)
date = forms.DateField(
label=_("Date"),
label_suffix="",
required=True,
help_text=_("Select date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"cols": 30,
"rows": 5,
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New deadline")
self.form_caption = _("Insert data for the new deadline")
def save(self):
with transaction.atomic():
action = UserActionLogEntry.objects.create(
user=self.user,
action=UserActionLogEntryEnum.CREATED.value
)
deadline = Deadline.objects.create(
type=self.cleaned_data["type"],
date=self.cleaned_data["date"],
comment=self.cleaned_data["comment"],
created=action,
)
self.instance.deadlines.add(deadline)
return deadline

View File

@@ -53,13 +53,16 @@ class CompensationControl(BaseResource):
comment = models.TextField()
class CompensationState(models.Model):
class CompensationState(UuidModel):
"""
Compensations must define the state of an area before and after the compensation.
"""
biotope_type = models.CharField(max_length=500, null=True, blank=True)
surface = models.FloatField()
def __str__(self):
return "{} | {}".format(self.biotope_type, self.surface)
class CompensationAction(BaseResource):
"""
@@ -70,6 +73,9 @@ class CompensationAction(BaseResource):
unit = models.CharField(max_length=100, null=True, blank=True)
control = models.ForeignKey(CompensationControl, on_delete=models.SET_NULL, null=True, blank=True)
def __str__(self):
return "{} | {} {}".format(self.action_type, self.amount, self.unit)
class AbstractCompensation(BaseObject):
"""
@@ -87,7 +93,7 @@ class AbstractCompensation(BaseObject):
before_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Ausgangszustand Biotop'")
after_states = models.ManyToManyField(CompensationState, blank=True, related_name='+', help_text="Refers to 'Zielzustand Biotop'")
actions = models.ManyToManyField(CompensationAction, help_text="Refers to 'Maßnahmen'")
actions = models.ManyToManyField(CompensationAction, blank=True, help_text="Refers to 'Maßnahmen'")
deadlines = models.ManyToManyField("konova.Deadline", null=True, blank=True, related_name="+")

View File

@@ -125,12 +125,12 @@ class CompensationTable(BaseTable):
"""
html = ""
checked = value is not None
tooltip = _("Not registered yet")
tooltip = _("Not recorded yet")
if checked:
value = value.timestamp
value = localtime(value)
on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
tooltip = _("Registered on {} by {}").format(on, record.intervention.recorded.user)
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=checked,

View File

@@ -0,0 +1,66 @@
{% load i18n l10n fontawesome_5 %}
<div id="related-documents" class="card">
<div class="card-header rlp-r">
<div class="row">
<div class="col-sm-6">
<h5>
<span class="badge badge-light">{{comp.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 'compensation:new-deadline' comp.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 comp.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>
{% if sum_before_states > sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,59 @@
{% load i18n l10n fontawesome_5 %}
<div id="related-documents" class="card">
<div class="card-header rlp-r">
<div class="row">
<div class="col-sm-6">
<h5>
<span class="badge badge-light">{{comp.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 'compensation:new-doc' comp.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 comp.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="related-documents" class="card">
<div class="card-header rlp-r">
<div class="row">
<div class="col-sm-6">
<h5>
<span class="badge badge-light">{{comp.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 'compensation:new-state' comp.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">
<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 comp.after_states.all %}
<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 'compensation:state-remove' state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if sum_before_states > sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,62 @@
{% load i18n l10n fontawesome_5 %}
<div id="related-documents" class="card">
<div class="card-header rlp-r">
<div class="row">
<div class="col-sm-6">
<h5>
<span class="badge badge-light">{{comp.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 'compensation:new-state' comp.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">
<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 comp.before_states.all %}
<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 'compensation:state-remove' state.id %}" class="btn btn-default btn-modal" title="{% trans 'Remove state' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if sum_before_states < sum_after_states %}
<div class="row alert alert-danger">
{% trans 'Missing surfaces: ' %}{{ diff_states|floatformat:2 }} m²
</div>
{% endif %}
</div>
</div>

View File

@@ -0,0 +1,144 @@
{% 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>{% trans 'Compensation' %} {{comp.identifier}}</h3>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="d-flex justify-content-end">
<a href="{% url 'home' %}" class="mr-2">
<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_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" data-form-url="{% url 'compensation:remove' comp.id %}" title="{% trans 'Delete' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %}
{% endif %}
</div>
</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">{{comp.title}}</td>
</tr>
<tr>
<th scope="row">{% trans 'compensates intervention' %}</th>
<td class="align-middle">
<a href="{% url 'intervention:open' comp.intervention.id %}">
{{comp.intervention.identifier}}
</a>
</td>
</tr>
<tr>
<th scope="row">{% trans 'Checked' %}</th>
<td class="align-middle">
{% if comp.intervention.checked is None %}
<span>
{% fa5_icon 'star' 'far' %}
</span>
{% else %}
<span class="check-star" title="{% trans 'Checked on '%} {{comp.intervention.checked.timestamp}} {% trans 'by' %} {{comp.intervention.checked.user}}">
{% fa5_icon 'star' %}
</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Recorded' %}</th>
<td class="align-middle">
{% if comp.intervention.recorded is None %}
<span title="{% trans 'Not recorded yet' %}">
{% fa5_icon 'bookmark' 'far' %}
</span>
{% else %}
<span class="registered-bookmark" title="{% trans 'Recorded on '%} {{comp.intervention.recorded.timestamp}} {% trans 'by' %} {{comp.intervention.recorded.user}}">
{% fa5_icon 'bookmark' %}
</span>
{% endif %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Last modified' %}</th>
<td class="align-middle">
{{comp.created.timestamp|default_if_none:""|naturalday}}
<br>
{% with comp.created.user as user %}
{% include 'user/includes/contact_modal_button.html' %}
{% endwith %}
</td>
</tr>
<tr>
<th scope="row">{% trans 'Shared with' %}</th>
<td class="align-middle">
{% for user in comp.intervention.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">
{% if geom_form.area == 0 %}
<div class="alert alert-info">{% trans 'No geometry added, yet.' %}</div>
{% endif %}
{{geom_form.media}}
{{geom_form.geom}}
</div>
</div>
<hr>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/includes/states-before.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/includes/states-after.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/includes/actions.html' %}
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/includes/deadlines.html' %}
</div>
</div>
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-6">
{% include 'compensation/detail/includes/documents.html' %}
</div>
</div>
{% with 'btn-modal' as btn_class %}
{% include 'modal/modal_form_script.html' %}
{% endwith %}
{% endblock %}

View File

@@ -17,6 +17,11 @@ urlpatterns = [
path('<id>', open_view, name='open'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/deadline/new', deadline_new_view, name="new-deadline"),
# Documents
path('<id>/document/new/', new_document_view, name='new-doc'),
# Payment
path('pay/<intervention_id>/new', new_payment_view, name='pay-new'),
@@ -34,4 +39,6 @@ urlpatterns = [
# Eco-account withdraws
path('acc/<id>/remove/<withdraw_id>', withdraw_remove_view, name='withdraw-remove'),
# Generic state routes
path('state/<id>/remove', state_remove_view, name='state-remove'),
]

View File

@@ -1,17 +1,19 @@
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum
from django.http import HttpRequest, Http404
from django.shortcuts import render, get_object_or_404
from django.utils.translation import gettext_lazy as _
from compensation.forms import NewPaymentForm
from compensation.models import Compensation, EcoAccount, Payment
from compensation.forms import NewPaymentForm, NewStateModalForm, NewDeadlineModalForm
from compensation.models import Compensation, EcoAccount, Payment, CompensationState
from compensation.tables import CompensationTable, EcoAccountTable
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import *
from konova.forms import RemoveModalForm
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm
from konova.utils.message_templates import FORM_INVALID
from konova.utils.user_checks import in_group
@login_required
@@ -59,8 +61,40 @@ def edit_view(request: HttpRequest, id: str):
@login_required
@any_group_check
def open_view(request: HttpRequest, id: str):
# ToDo
pass
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
template = "compensation/detail/view.html"
comp = get_object_or_404(Compensation, id=id)
geom_form = SimpleGeomForm(instance=comp)
_user = request.user
is_data_shared = comp.intervention.is_shared_with(_user)
# 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 = comp.before_states.all().aggregate(Sum("surface"))["surface__sum"] or 0
sum_after_states = comp.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0
diff_states = abs(sum_before_states - sum_after_states)
context = {
"comp": comp,
"geom_form": geom_form,
"has_access": is_data_shared,
"is_default_member": in_group(_user, _(DEFAULT_GROUP)),
"is_zb_member": in_group(_user, _(ZB_GROUP)),
"is_ets_member": in_group(_user, _(ETS_GROUP)),
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@@ -79,6 +113,7 @@ def remove_view(request: HttpRequest, id: str):
return form.process_request(
request=request,
msg_success=_("Compensation removed"),
redirect_url="",
)
@@ -220,3 +255,69 @@ def withdraw_remove_view(request: HttpRequest, id: str, withdraw_id: str):
request=request,
msg_success=_("Withdraw removed")
)
@login_required
def new_document_view(request: HttpRequest, id: str):
""" Renders a form for uploading new documents
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id to which the new document will be related
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = NewDocumentForm(request.POST or None, request.FILES or None, instance=comp, user=request.user)
return form.process_request(
request,
msg_success=_("Document added")
)
@login_required
def state_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new states for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id to which the new state will be related
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = NewStateModalForm(request.POST or None, instance=comp, user=request.user)
return form.process_request(
request,
msg_success=_("State added")
)
@login_required
def deadline_new_view(request: HttpRequest, id: str):
""" Renders a form for adding new states for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id to which the new state will be related
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = NewDeadlineModalForm(request.POST or None, instance=comp, user=request.user)
return form.process_request(
request,
msg_success=_("Deadline added")
)
@login_required
def state_remove_view(request: HttpRequest, id: str):
state = get_object_or_404(CompensationState, id=id)
form = RemoveModalForm(request.POST or None, instance=state, user=request.user)
return form.process_request(
request,
msg_success=_("State removed")
)