diff --git a/api/tests/v1/create/compensation_create_post_body.json b/api/tests/v1/create/compensation_create_post_body.json
index b0d21e83..865621a1 100644
--- a/api/tests/v1/create/compensation_create_post_body.json
+++ b/api/tests/v1/create/compensation_create_post_body.json
@@ -6,6 +6,7 @@
"title": "Test_compensation",
"is_cef": false,
"is_coherence_keeping": false,
+ "is_pik": false,
"intervention": "MUST_BE_SET_IN_TEST",
"before_states": [
],
diff --git a/api/tests/v1/create/ecoaccount_create_post_body.json b/api/tests/v1/create/ecoaccount_create_post_body.json
index 8300277d..7550d8cd 100644
--- a/api/tests/v1/create/ecoaccount_create_post_body.json
+++ b/api/tests/v1/create/ecoaccount_create_post_body.json
@@ -5,6 +5,7 @@
"properties": {
"title": "Test_ecoaccount",
"deductable_surface": 10000.0,
+ "is_pik": false,
"responsible": {
"conservation_office": null,
"conservation_file_number": null,
diff --git a/api/tests/v1/create/ema_create_post_body.json b/api/tests/v1/create/ema_create_post_body.json
index 4949b7ab..8b826661 100644
--- a/api/tests/v1/create/ema_create_post_body.json
+++ b/api/tests/v1/create/ema_create_post_body.json
@@ -4,6 +4,7 @@
],
"properties": {
"title": "Test_ema",
+ "is_pik": false,
"responsible": {
"conservation_office": null,
"conservation_file_number": null,
diff --git a/api/tests/v1/get/test_api_get.py b/api/tests/v1/get/test_api_get.py
index 953b0f69..0db58cbc 100644
--- a/api/tests/v1/get/test_api_get.py
+++ b/api/tests/v1/get/test_api_get.py
@@ -122,6 +122,7 @@ class APIV1GetTestCase(BaseAPIV1TestCase):
props = geojson["properties"]
props["is_cef"]
props["is_coherence_keeping"]
+ props["is_pik"]
props["intervention"]
props["intervention"]["id"]
props["intervention"]["identifier"]
diff --git a/api/tests/v1/update/compensation_update_put_body.json b/api/tests/v1/update/compensation_update_put_body.json
index 57ad5ed4..4ae91ac7 100644
--- a/api/tests/v1/update/compensation_update_put_body.json
+++ b/api/tests/v1/update/compensation_update_put_body.json
@@ -46,6 +46,7 @@
"title": "TEST_compensation_CHANGED",
"is_cef": true,
"is_coherence_keeping": true,
+ "is_pik": true,
"intervention": "CHANGE_BEFORE_RUN!!!",
"before_states": [],
"after_states": [],
diff --git a/api/tests/v1/update/ecoaccount_update_put_body.json b/api/tests/v1/update/ecoaccount_update_put_body.json
index ff636ff0..2b8235cb 100644
--- a/api/tests/v1/update/ecoaccount_update_put_body.json
+++ b/api/tests/v1/update/ecoaccount_update_put_body.json
@@ -45,6 +45,7 @@
"properties": {
"title": "TEST_account_CHANGED",
"deductable_surface": "100000.0",
+ "is_pik": true,
"responsible": {
"conservation_office": null,
"conservation_file_number": "123-TEST",
diff --git a/api/tests/v1/update/ema_update_put_body.json b/api/tests/v1/update/ema_update_put_body.json
index cc835850..6c7adf68 100644
--- a/api/tests/v1/update/ema_update_put_body.json
+++ b/api/tests/v1/update/ema_update_put_body.json
@@ -52,6 +52,7 @@
"detail": "TEST_HANDLER_CHANGED"
}
},
+ "is_pik": true,
"before_states": [],
"after_states": [],
"actions": [],
diff --git a/api/tests/v1/update/test_api_update.py b/api/tests/v1/update/test_api_update.py
index bfc670bc..ff867e69 100644
--- a/api/tests/v1/update/test_api_update.py
+++ b/api/tests/v1/update/test_api_update.py
@@ -97,6 +97,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
self.assertNotEqual(modified_on, self.compensation.modified)
self.assertEqual(put_props["is_cef"], self.compensation.is_cef)
self.assertEqual(put_props["is_coherence_keeping"], self.compensation.is_coherence_keeping)
+ self.assertEqual(put_props["is_pik"], self.compensation.is_pik)
self.assertEqual(len(put_props["actions"]), self.compensation.actions.count())
self.assertEqual(len(put_props["before_states"]), self.compensation.before_states.count())
self.assertEqual(len(put_props["after_states"]), self.compensation.after_states.count())
diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py
index 24e499ef..89dbe0e6 100644
--- a/api/utils/serializer/v1/compensation.py
+++ b/api/utils/serializer/v1/compensation.py
@@ -34,6 +34,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
def _extend_properties_data(self, entry):
self.properties_data["is_cef"] = entry.is_cef
self.properties_data["is_coherence_keeping"] = entry.is_coherence_keeping
+ self.properties_data["is_pik"] = entry.is_pik
self.properties_data["intervention"] = self.intervention_to_json(entry.intervention)
self.properties_data["before_states"] = self._compensation_state_to_json(entry.before_states.all())
self.properties_data["after_states"] = self._compensation_state_to_json(entry.after_states.all())
@@ -113,6 +114,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj.title = properties["title"]
obj.is_cef = properties["is_cef"]
obj.is_coherence_keeping = properties["is_coherence_keeping"]
+ obj.is_pik = properties.get("is_pik", False)
obj = self.set_intervention(obj, properties["intervention"], user)
obj.geometry.save()
@@ -149,6 +151,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj.title = properties["title"]
obj.is_cef = properties["is_cef"]
obj.is_coherence_keeping = properties["is_coherence_keeping"]
+ obj.is_pik = properties.get("is_pik", False)
obj.modified = update_action
obj.geometry.geom = self._create_geometry_from_json(json_model)
obj.geometry.modified = update_action
diff --git a/api/utils/serializer/v1/ecoaccount.py b/api/utils/serializer/v1/ecoaccount.py
index 7fe4373d..106789f4 100644
--- a/api/utils/serializer/v1/ecoaccount.py
+++ b/api/utils/serializer/v1/ecoaccount.py
@@ -25,6 +25,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
model = EcoAccount
def _extend_properties_data(self, entry):
+ self.properties_data["is_pik"] = entry.is_pik
self.properties_data["deductable_surface"] = entry.deductable_surface
self.properties_data["deductable_surface_available"] = entry.deductable_surface - entry.get_deductions_surface()
self.properties_data["responsible"] = self._responsible_to_json(entry.responsible)
@@ -122,6 +123,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
properties = json_model["properties"]
obj.identifier = obj.generate_new_identifier()
obj.title = properties["title"]
+ obj.is_pik = properties.get("is_pik", False)
try:
obj.deductable_surface = float(properties["deductable_surface"])
@@ -169,6 +171,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
# Fill in data to objects
properties = json_model["properties"]
obj.title = properties["title"]
+ obj.is_pik = properties.get("is_pik", False)
obj.deductable_surface = float(properties["deductable_surface"])
obj.modified = update_action
obj.geometry.geom = self._create_geometry_from_json(json_model)
diff --git a/api/utils/serializer/v1/ema.py b/api/utils/serializer/v1/ema.py
index dfda7249..787a64b2 100644
--- a/api/utils/serializer/v1/ema.py
+++ b/api/utils/serializer/v1/ema.py
@@ -21,6 +21,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
model = Ema
def _extend_properties_data(self, entry):
+ self.properties_data["is_pik"] = entry.is_pik
self.properties_data["responsible"] = self._responsible_to_json(entry.responsible)
self.properties_data["before_states"] = self._compensation_state_to_json(entry.before_states.all())
self.properties_data["after_states"] = self._compensation_state_to_json(entry.after_states.all())
@@ -104,6 +105,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
properties = json_model["properties"]
obj.identifier = obj.generate_new_identifier()
obj.title = properties["title"]
+ obj.is_pik = properties.get("is_pik", False)
obj = self._set_responsibility(obj, properties["responsible"])
obj.geometry.save()
@@ -141,6 +143,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
# Fill in data to objects
properties = json_model["properties"]
obj.title = properties["title"]
+ obj.is_pik = properties.get("is_pik", False)
obj.modified = update_action
obj.geometry.geom = self._create_geometry_from_json(json_model)
obj.geometry.modified = update_action
diff --git a/codelist/models.py b/codelist/models.py
index de7a2100..8e5de0c9 100644
--- a/codelist/models.py
+++ b/codelist/models.py
@@ -65,24 +65,23 @@ class KonovaCode(models.Model):
ret_val += ", " + self.parent.long_name
return ret_val
- def add_children(self):
+ def add_children(self, order_by: str = "long_name"):
""" Adds all children (resurcively until leaf) as .children to the KonovaCode
Returns:
code (KonovaCode): The manipulated KonovaCode instance
"""
if self.is_leaf:
- return None
+ return self
children = KonovaCode.objects.filter(
- code_lists__in=self.code_lists.all(),
parent=self
).order_by(
- "long_name"
+ order_by
)
self.children = children
for child in children:
- child.add_children()
+ child.add_children(order_by)
return self
diff --git a/compensation/admin.py b/compensation/admin.py
index a5cd1a88..2821a146 100644
--- a/compensation/admin.py
+++ b/compensation/admin.py
@@ -55,6 +55,7 @@ class CompensationAdmin(AbstractCompensationAdmin):
return super().get_fields(request, obj) + [
"is_cef",
"is_coherence_keeping",
+ "is_pik",
"intervention",
]
diff --git a/compensation/filters.py b/compensation/filters.py
index b6377092..c9bf9b70 100644
--- a/compensation/filters.py
+++ b/compensation/filters.py
@@ -60,7 +60,7 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter):
if not value:
return queryset.filter(
Q(intervention__users__in=[self.user]) | # requesting user has access
- Q(intervention__teams__users__in=[self.user])
+ Q(intervention__teams__in=self.user.shared_teams)
).distinct()
else:
return queryset
diff --git a/compensation/forms/forms.py b/compensation/forms/forms.py
index 4e9e329e..8f28c5ff 100644
--- a/compensation/forms/forms.py
+++ b/compensation/forms/forms.py
@@ -160,7 +160,23 @@ class CoherenceCompensationFormMixin(forms.Form):
)
-class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, CoherenceCompensationFormMixin):
+class PikCompensationFormMixin(forms.Form):
+ """ A form mixin, providing PIK compensation field
+
+ """
+ is_pik = forms.BooleanField(
+ label_suffix="",
+ label=_("Is PIK"),
+ help_text=_("Optionally: Whether this compensation is a compensation integrated in production?"),
+ required=False,
+ widget=forms.CheckboxInput()
+ )
+
+
+class NewCompensationForm(AbstractCompensationForm,
+ CEFCompensationFormMixin,
+ CoherenceCompensationFormMixin,
+ PikCompensationFormMixin):
""" Form for creating new compensations.
Can be initialized with an intervention id for preselecting the related intervention.
@@ -191,6 +207,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
"identifier",
"title",
"intervention",
+ "is_pik",
"is_cef",
"is_coherence_keeping",
"comment",
@@ -234,6 +251,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
+ is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
@@ -249,6 +267,7 @@ class NewCompensationForm(AbstractCompensationForm, CEFCompensationFormMixin, Co
created=action,
is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping,
+ is_pik=is_pik,
geometry=geometry,
comment=comment,
)
@@ -281,6 +300,7 @@ class EditCompensationForm(NewCompensationForm):
"intervention": self.instance.intervention,
"is_cef": self.instance.is_cef,
"is_coherence_keeping": self.instance.is_coherence_keeping,
+ "is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
@@ -297,6 +317,7 @@ class EditCompensationForm(NewCompensationForm):
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
+ is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
@@ -313,6 +334,7 @@ class EditCompensationForm(NewCompensationForm):
self.instance.is_cef = is_cef
self.instance.is_coherence_keeping = is_coherence_keeping
self.instance.comment = comment
+ self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
@@ -322,7 +344,7 @@ class EditCompensationForm(NewCompensationForm):
return self.instance
-class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
+class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating eco accounts
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
@@ -363,6 +385,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
"registration_date",
"surface",
"conservation_file_number",
+ "is_pik",
"handler_type",
"handler_detail",
"comment",
@@ -392,6 +415,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
+ is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
@@ -423,6 +447,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
created=action,
geometry=geometry,
comment=comment,
+ is_pik=is_pik,
legal=legal
)
acc.share_with_user(user)
@@ -458,6 +483,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
"registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
+ "is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
@@ -478,6 +504,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
+ is_pik = self.cleaned_data.get("is_pik", None)
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
@@ -503,6 +530,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
self.instance.deductable_surface = surface
self.instance.geometry = geometry
self.instance.comment = comment
+ self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
diff --git a/compensation/forms/modalForms.py b/compensation/forms/modalForms.py
index 8404c7ad..581a7b3a 100644
--- a/compensation/forms/modalForms.py
+++ b/compensation/forms/modalForms.py
@@ -17,7 +17,8 @@ from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.models import CompensationDocument, EcoAccountDocument
-from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple
+from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
+ CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.models import DeadlineType
@@ -156,22 +157,12 @@ class NewStateModalForm(BaseModalForm):
What has been on this area before changes/compensations have been applied and what will be the result ('after')?
"""
- biotope_type = forms.ModelChoiceField(
+ biotope_type = forms.ChoiceField(
label=_("Biotope Type"),
label_suffix="",
required=True,
help_text=_("Select the biotope type"),
- queryset=KonovaCode.objects.filter(
- is_archived=False,
- is_leaf=True,
- code_lists__in=[CODELIST_BIOTOPES_ID],
- ),
- widget=autocomplete.ModelSelect2(
- url="codes-biotope-autocomplete",
- attrs={
- "data-placeholder": _("Biotope Type"),
- }
- ),
+ widget=CompensationStateTreeRadioSelect(),
)
biotope_extra = forms.ModelMultipleChoiceField(
label=_("Biotope additional type"),
@@ -209,6 +200,16 @@ class NewStateModalForm(BaseModalForm):
super().__init__(*args, **kwargs)
self.form_title = _("New state")
self.form_caption = _("Insert data for the new state")
+ choices = KonovaCode.objects.filter(
+ code_lists__in=[CODELIST_BIOTOPES_ID],
+ is_archived=False,
+ is_leaf=True,
+ ).values_list("id", flat=True)
+ choices = [
+ (choice, choice)
+ for choice in choices
+ ]
+ self.fields["biotope_type"].choices = choices
def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, is_before_state)
@@ -271,8 +272,9 @@ class EditCompensationStateModalForm(NewStateModalForm):
self.state = kwargs.pop("state", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit state")
+ biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
form_data = {
- "biotope_type": self.state.biotope_type,
+ "biotope_type": biotope_type_id,
"biotope_extra": self.state.biotope_type_details.all(),
"surface": self.state.surface,
}
@@ -280,7 +282,8 @@ class EditCompensationStateModalForm(NewStateModalForm):
def save(self, is_before_state: bool = False):
state = self.state
- state.biotope_type = self.cleaned_data.get("biotope_type", None)
+ biotope_type_id = self.cleaned_data.get("biotope_type", None)
+ state.biotope_type = KonovaCode.objects.get(id=biotope_type_id)
state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", []))
state.surface = self.cleaned_data.get("surface", None)
state.save()
diff --git a/compensation/migrations/0007_auto_20220531_1245.py b/compensation/migrations/0007_auto_20220531_1245.py
new file mode 100644
index 00000000..688ffb18
--- /dev/null
+++ b/compensation/migrations/0007_auto_20220531_1245.py
@@ -0,0 +1,23 @@
+# Generated by Django 3.1.3 on 2022-05-31 10:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('compensation', '0006_ecoaccount_teams'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='compensation',
+ name='is_pik',
+ field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
+ ),
+ migrations.AddField(
+ model_name='ecoaccount',
+ name='is_pik',
+ field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
+ ),
+ ]
diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py
index 3deff50a..2e42ff7a 100644
--- a/compensation/models/compensation.py
+++ b/compensation/models/compensation.py
@@ -8,6 +8,8 @@ Created on: 16.11.21
import shutil
from django.contrib import messages
+
+from codelist.models import KonovaCode
from user.models import User, Team
from django.db import models, transaction
from django.db.models import QuerySet, Sum
@@ -142,8 +144,10 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
"""
form_data = form.cleaned_data
with transaction.atomic():
+ biotope_type_id = form_data["biotope_type"]
+ code = KonovaCode.objects.get(id=biotope_type_id)
state = CompensationState.objects.create(
- biotope_type=form_data["biotope_type"],
+ biotope_type=code,
surface=form_data["surface"],
)
state_additional_types = form_data["biotope_extra"]
@@ -253,7 +257,22 @@ class CoherenceMixin(models.Model):
abstract = True
-class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
+class PikMixin(models.Model):
+ """ Provides PIK flag as Mixin
+
+ """
+ is_pik = models.BooleanField(
+ blank=True,
+ null=True,
+ default=False,
+ help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'"
+ )
+
+ class Meta:
+ abstract = True
+
+
+class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
"""
Regular compensation, linked to an intervention
"""
diff --git a/compensation/models/eco_account.py b/compensation/models/eco_account.py
index 6d95b399..3d48b691 100644
--- a/compensation/models/eco_account.py
+++ b/compensation/models/eco_account.py
@@ -17,14 +17,13 @@ from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _
from compensation.managers import EcoAccountManager, EcoAccountDeductionManager
-from compensation.models.compensation import AbstractCompensation
+from compensation.models.compensation import AbstractCompensation, PikMixin
from compensation.utils.quality import EcoAccountQualityChecker
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
generate_document_file_upload_path
-from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
-class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
+class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
"""
An eco account is a kind of 'prepaid' compensation. It can be compared to an account that already has been filled
with some kind of currency. From this account one is able to deduct currency for current projects.
diff --git a/compensation/tables.py b/compensation/tables.py
index 401a7416..1373f2cd 100644
--- a/compensation/tables.py
+++ b/compensation/tables.py
@@ -5,17 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.12.20
"""
-from user.models import User
+from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
-from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
from compensation.filters import CompensationTableFilter, EcoAccountTableFilter
from compensation.models import Compensation, EcoAccount
-from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT
from konova.utils.tables import BaseTable, TableRenderMixin
import django_tables2 as tables
@@ -111,16 +109,21 @@ class CompensationTable(BaseTable, TableRenderMixin):
"""
html = ""
checked = value is not None
- tooltip = _("Not checked yet")
+ tooltip = DATA_IS_UNCHECKED
+ previously_checked = record.intervention.get_last_checked_action()
if checked:
- value = value.timestamp
- value = localtime(value)
- checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
- tooltip = _("Checked on {} by {}").format(checked_on, record.intervention.checked.user)
+ checked_on = value.get_timestamp_str_formatted()
+ tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.intervention.checked.user)
html += self.render_checked_star(
tooltip=tooltip,
icn_filled=checked,
)
+ if previously_checked and not checked:
+ checked_on = previously_checked.get_timestamp_str_formatted()
+ tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(checked_on, previously_checked.user)
+ html += self.render_previously_checked_star(
+ tooltip=tooltip,
+ )
return format_html(html)
def render_d(self, value, record: Compensation):
@@ -159,9 +162,7 @@ class CompensationTable(BaseTable, TableRenderMixin):
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
- value = value.timestamp
- value = localtime(value)
- on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
+ on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
@@ -179,8 +180,6 @@ class CompensationTable(BaseTable, TableRenderMixin):
Returns:
"""
- if value is None:
- value = User.objects.none()
has_access = record.is_shared_with(self.user)
html = self.render_icn(
@@ -318,9 +317,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin):
checked = value is not None
tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
if checked:
- value = value.timestamp
- value = localtime(value)
- on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
+ on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
diff --git a/compensation/templates/compensation/detail/compensation/view.html b/compensation/templates/compensation/detail/compensation/view.html
index 8e1b1267..5a125ccd 100644
--- a/compensation/templates/compensation/detail/compensation/view.html
+++ b/compensation/templates/compensation/detail/compensation/view.html
@@ -39,6 +39,16 @@
+
+ {% trans 'Is PIK' %} |
+
+ {% if obj.is_pik %}
+ {% trans 'Yes' %}
+ {% else %}
+ {% trans 'No' %}
+ {% endif %}
+ |
+
{% trans 'Is CEF compensation' %} |
@@ -66,6 +76,11 @@
{% fa5_icon 'star' 'far' %}
+ {% if last_checked %}
+
+ {% fa5_icon 'star' 'fas' %}
+
+ {% endif %}
{% else %}
{% fa5_icon 'star' %}
@@ -104,7 +119,7 @@
{% trans 'Shared with' %} |
- {% for team in obj.intervention.teams.all %}
+ {% for team in obj.intervention.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
@@ -119,7 +134,9 @@
- {% include 'map/geom_form.html' %}
+
+ {% include 'map/geom_form.html' %}
+
{% include 'konova/includes/parcels/parcels.html' %}
diff --git a/compensation/templates/compensation/detail/eco_account/view.html b/compensation/templates/compensation/detail/eco_account/view.html
index 0eb354fe..432315a8 100644
--- a/compensation/templates/compensation/detail/eco_account/view.html
+++ b/compensation/templates/compensation/detail/eco_account/view.html
@@ -70,6 +70,16 @@
{% trans 'Action handler' %} |
{{obj.responsible.handler|default_if_none:""}} |
|
+
+ {% trans 'Is PIK' %} |
+
+ {% if obj.is_pik %}
+ {% trans 'Yes' %}
+ {% else %}
+ {% trans 'No' %}
+ {% endif %}
+ |
+
{% trans 'Last modified' %} |
@@ -87,7 +97,7 @@
|
{% trans 'Shared with' %} |
- {% for team in obj.teams.all %}
+ {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
@@ -101,7 +111,9 @@
- {% include 'map/geom_form.html' %}
+
+ {% include 'map/geom_form.html' %}
+
{% include 'konova/includes/parcels/parcels.html' %}
diff --git a/compensation/templates/compensation/report/compensation/report.html b/compensation/templates/compensation/report/compensation/report.html
index 7088ff07..2be278aa 100644
--- a/compensation/templates/compensation/report/compensation/report.html
+++ b/compensation/templates/compensation/report/compensation/report.html
@@ -20,6 +20,36 @@
|
+
+ {% trans 'Is PIK' %} |
+
+ {% if obj.is_pik %}
+ {% trans 'Yes' %}
+ {% else %}
+ {% trans 'No' %}
+ {% endif %}
+ |
+
+
+ {% trans 'Is CEF' %} |
+
+ {% if obj.is_cef %}
+ {% trans 'Yes' %}
+ {% else %}
+ {% trans 'No' %}
+ {% endif %}
+ |
+
+
+ {% trans 'Is coherence keeping' %} |
+
+ {% if obj.is_coherence_keeping %}
+ {% trans 'Yes' %}
+ {% else %}
+ {% trans 'No' %}
+ {% endif %}
+ |
+
{% trans 'Last modified' %} |
@@ -35,7 +65,9 @@
- {% include 'map/geom_form.html' %}
+
+ {% include 'map/geom_form.html' %}
+
{% include 'konova/includes/parcels/parcels.html' %}
diff --git a/compensation/templates/compensation/report/eco_account/report.html b/compensation/templates/compensation/report/eco_account/report.html
index a3632ee2..e2147f69 100644
--- a/compensation/templates/compensation/report/eco_account/report.html
+++ b/compensation/templates/compensation/report/eco_account/report.html
@@ -20,6 +20,16 @@
{% trans 'Conservation office file number' %} |
{{obj.responsible.conservation_file_number|default_if_none:""}} |
|
+
+ {% trans 'Is PIK' %} |
+
+ {% if obj.is_pik %}
+ {% trans 'Yes' %}
+ {% else %}
+ {% trans 'No' %}
+ {% endif %}
+ |
+
{% trans 'Deductions for' %} |
@@ -48,7 +58,9 @@
- {% include 'map/geom_form.html' %}
+
+ {% include 'map/geom_form.html' %}
+
{% include 'konova/includes/parcels/parcels.html' %}
diff --git a/compensation/tests/compensation/test_workflow.py b/compensation/tests/compensation/test_workflow.py
index 570045f2..bba12334 100644
--- a/compensation/tests/compensation/test_workflow.py
+++ b/compensation/tests/compensation/test_workflow.py
@@ -50,10 +50,11 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
+ geom_json = self.create_geojson(test_geom)
post_data = {
"identifier": test_id,
"title": test_title,
- "geom": test_geom.geojson,
+ "geom": geom_json,
"intervention": self.intervention.id,
}
pre_creation_intervention_log_count = self.intervention.log.count()
@@ -88,10 +89,11 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
+ geom_json = self.create_geojson(test_geom)
post_data = {
"identifier": test_id,
"title": test_title,
- "geom": test_geom.geojson,
+ "geom": geom_json,
}
pre_creation_intervention_log_count = self.intervention.log.count()
@@ -126,6 +128,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
+ geojson = self.create_geojson(new_geometry)
check_on_elements = {
self.compensation.title: new_title,
@@ -140,7 +143,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
"title": new_title,
"intervention": self.intervention.id, # just keep the intervention as it is
"comment": new_comment,
- "geom": new_geometry.geojson,
+ "geom": geojson,
}
self.client_user.post(url, post_data)
self.compensation.refresh_from_db()
diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py
index bd6894ae..d1a3cf09 100644
--- a/compensation/tests/ecoaccount/test_workflow.py
+++ b/compensation/tests/ecoaccount/test_workflow.py
@@ -40,12 +40,13 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
+ geom_json = self.create_geojson(test_geom)
test_deductable_surface = 1000
test_conservation_office = self.get_conservation_office_code()
post_data = {
"identifier": test_id,
"title": test_title,
- "geom": test_geom.geojson,
+ "geom": geom_json,
"deductable_surface": test_deductable_surface,
"conservation_office": test_conservation_office.id
}
diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py
index c3d3e2b5..31087ed7 100644
--- a/compensation/views/compensation.py
+++ b/compensation/views/compensation.py
@@ -23,7 +23,7 @@ from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DA
CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \
COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, \
DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, \
- DEADLINE_EDITED, RECORDED_BLOCKS_EDIT, PARAMS_INVALID
+ DEADLINE_EDITED, RECORDED_BLOCKS_EDIT, PARAMS_INVALID, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from konova.utils.user_checks import in_group
@@ -217,8 +217,15 @@ def detail_view(request: HttpRequest, id: str):
request = comp.set_status_messages(request)
+ last_checked = comp.intervention.get_last_checked_action()
+ last_checked_tooltip = ""
+ if last_checked:
+ last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
+
context = {
"obj": comp,
+ "last_checked": last_checked,
+ "last_checked_tooltip": last_checked_tooltip,
"geom_form": geom_form,
"parcels": parcels,
"has_access": is_data_shared,
diff --git a/ema/forms.py b/ema/forms.py
index 91b8de19..a7e82c4f 100644
--- a/ema/forms.py
+++ b/ema/forms.py
@@ -12,14 +12,15 @@ from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
-from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin
+from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin, \
+ PikCompensationFormMixin
from ema.models import Ema, EmaDocument
from intervention.models import Responsibility, Handler
from konova.forms import SimpleGeomForm, NewDocumentModalForm
from user.models import UserActionLogEntry
-class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
+class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating new EMA objects.
Inherits basic form fields from AbstractCompensationForm and additional from CompensationResponsibleFormMixin.
@@ -31,6 +32,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
"title",
"conservation_office",
"conservation_file_number",
+ "is_pik",
"handler_type",
"handler_detail",
"comment",
@@ -58,6 +60,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
handler_detail = self.cleaned_data.get("handler_detail", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
+ is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
@@ -83,6 +86,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin):
created=action,
geometry=geometry,
comment=comment,
+ is_pik=is_pik,
)
# Add the creating user to the list of shared users
@@ -116,6 +120,7 @@ class EditEmaForm(NewEmaForm):
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"comment": self.instance.comment,
+ "is_pik": self.instance.is_pik,
}
disabled_fields = []
self.load_initial_data(
@@ -133,6 +138,7 @@ class EditEmaForm(NewEmaForm):
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
+ is_pik = self.cleaned_data.get("is_pik", None)
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
@@ -152,6 +158,7 @@ class EditEmaForm(NewEmaForm):
self.instance.title = title
self.instance.geometry = geometry
self.instance.comment = comment
+ self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
diff --git a/ema/migrations/0004_ema_is_pik.py b/ema/migrations/0004_ema_is_pik.py
new file mode 100644
index 00000000..534155a7
--- /dev/null
+++ b/ema/migrations/0004_ema_is_pik.py
@@ -0,0 +1,18 @@
+# Generated by Django 3.1.3 on 2022-05-31 10:45
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('ema', '0003_ema_teams'),
+ ]
+
+ operations = [
+ migrations.AddField(
+ model_name='ema',
+ name='is_pik',
+ field=models.BooleanField(blank=True, default=False, help_text="Flag if compensation is a 'Produktonsintegrierte Kompensation'", null=True),
+ ),
+ ]
diff --git a/ema/models/ema.py b/ema/models/ema.py
index 983bdbd7..abec7c43 100644
--- a/ema/models/ema.py
+++ b/ema/models/ema.py
@@ -13,15 +13,14 @@ from django.db.models import QuerySet
from django.http import HttpRequest
from django.urls import reverse
-from compensation.models import AbstractCompensation
+from compensation.models import AbstractCompensation, PikMixin
from ema.managers import EmaManager
from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
-from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE
-class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin):
+class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
"""
EMA = Ersatzzahlungsmaßnahme
(compensation actions from payments)
diff --git a/ema/tables.py b/ema/tables.py
index 38d8a8c0..d26a31dc 100644
--- a/ema/tables.py
+++ b/ema/tables.py
@@ -115,7 +115,6 @@ class EmaTable(BaseTable, TableRenderMixin):
)
return html
-
def render_r(self, value, record: Ema):
""" Renders the registered column for a EMA
@@ -130,9 +129,7 @@ class EmaTable(BaseTable, TableRenderMixin):
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
- value = value.timestamp
- value = localtime(value)
- on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
+ on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
diff --git a/ema/templates/ema/detail/view.html b/ema/templates/ema/detail/view.html
index 31d26e0b..16f31378 100644
--- a/ema/templates/ema/detail/view.html
+++ b/ema/templates/ema/detail/view.html
@@ -56,6 +56,16 @@
{% trans 'Action handler' %} |
{{obj.responsible.handler|default_if_none:""}} |
|
+
+ {% trans 'Is PIK' %} |
+
+ {% if obj.is_pik %}
+ {% trans 'Yes' %}
+ {% else %}
+ {% trans 'No' %}
+ {% endif %}
+ |
+
{% trans 'Last modified' %} |
@@ -73,11 +83,11 @@
|
{% trans 'Shared with' %} |
- {% for team in obj.teams.all %}
+ {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
- {% for user in obj.users.all %}
+ {% for user in obj.user.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}
|
@@ -87,7 +97,9 @@
- {% include 'map/geom_form.html' %}
+
+ {% include 'map/geom_form.html' %}
+
{% include 'konova/includes/parcels/parcels.html' %}
diff --git a/ema/templates/ema/report/report.html b/ema/templates/ema/report/report.html
index 43b64865..fd166596 100644
--- a/ema/templates/ema/report/report.html
+++ b/ema/templates/ema/report/report.html
@@ -20,6 +20,16 @@
{% trans 'Conservation office file number' %} |
{{obj.responsible.conservation_file_number|default_if_none:""}} |
+
+ {% trans 'Is PIK' %} |
+
+ {% if obj.is_pik %}
+ {% trans 'Yes' %}
+ {% else %}
+ {% trans 'No' %}
+ {% endif %}
+ |
+
{% trans 'Last modified' %} |
@@ -35,7 +45,9 @@
- {% include 'map/geom_form.html' %}
+
+ {% include 'map/geom_form.html' %}
+
{% include 'konova/includes/parcels/parcels.html' %}
diff --git a/ema/tests/test_workflow.py b/ema/tests/test_workflow.py
index ecc3f195..4391fdc6 100644
--- a/ema/tests/test_workflow.py
+++ b/ema/tests/test_workflow.py
@@ -41,11 +41,12 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
+ geom_json = self.create_geojson(test_geom)
test_conservation_office = self.get_conservation_office_code()
post_data = {
"identifier": test_id,
"title": test_title,
- "geom": test_geom.geojson,
+ "geom": geom_json,
"conservation_office": test_conservation_office.id
}
self.client_user.post(new_url, post_data)
diff --git a/intervention/forms/forms.py b/intervention/forms/forms.py
index b83c4fba..b85ba101 100644
--- a/intervention/forms/forms.py
+++ b/intervention/forms/forms.py
@@ -216,6 +216,10 @@ class NewInterventionForm(BaseForm):
identifier = tmp_intervention.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
+ def is_valid(self):
+ super_valid_result = super().is_valid()
+ return super_valid_result
+
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
diff --git a/intervention/inputs.py b/intervention/inputs.py
index 34fe0434..a08c0b74 100644
--- a/intervention/inputs.py
+++ b/intervention/inputs.py
@@ -1,6 +1,6 @@
from django import forms
from codelist.models import KonovaCode
-from codelist.settings import CODELIST_COMPENSATION_ACTION_ID
+from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID
class DummyFilterInput(forms.HiddenInput):
@@ -38,7 +38,17 @@ class TreeCheckboxSelectMultiple(forms.CheckboxSelectMultiple):
""" Provides multiple selection of parent-child data
"""
- template_name = "konova/widgets/checkbox-tree-select.html"
+ template_name = "konova/widgets/tree/checkbox/checkbox-tree-select.html"
+
+ class meta:
+ abstract = True
+
+
+class TreeRadioSelect(forms.RadioSelect):
+ """ Provides single selection of parent-child data
+
+ """
+ template_name = "konova/widgets/tree/radio/radio-tree-select.html"
class meta:
abstract = True
@@ -68,6 +78,30 @@ class KonovaCodeTreeCheckboxSelectMultiple(TreeCheckboxSelectMultiple):
return context
+class KonovaCodeTreeRadioSelect(TreeRadioSelect):
+ """ Provides single selection of KonovaCode
+
+ """
+ filter = None
+
+ def __init__(self, *args, **kwargs):
+ self.code_list = kwargs.pop("code_list", None)
+ self.filter = kwargs.pop("filter", {})
+ super().__init__(*args, **kwargs)
+
+ def get_context(self, name, value, attrs):
+ context = super().get_context(name, value, attrs)
+ codes = KonovaCode.objects.filter(
+ **self.filter,
+ )
+ codes = [
+ parent_code.add_children()
+ for parent_code in codes
+ ]
+ context["codes"] = codes
+ return context
+
+
class CompensationActionTreeCheckboxSelectMultiple(KonovaCodeTreeCheckboxSelectMultiple):
""" Provides multiple selection of CompensationActions
@@ -79,4 +113,31 @@ class CompensationActionTreeCheckboxSelectMultiple(KonovaCodeTreeCheckboxSelectM
self.filter = {
"code_lists__in": [CODELIST_COMPENSATION_ACTION_ID],
"parent": None,
- }
\ No newline at end of file
+ }
+
+
+class CompensationStateTreeRadioSelect(KonovaCodeTreeRadioSelect):
+ """ Provides single selection of CompensationState
+
+ """
+ filter = None
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.filter = {
+ "code_lists__in": [CODELIST_BIOTOPES_ID],
+ "parent": None,
+ "is_archived": False,
+ }
+
+ def get_context(self, name, value, attrs):
+ context = super().get_context(name, value, attrs)
+ codes = KonovaCode.objects.filter(
+ **self.filter,
+ )
+ codes = [
+ parent_code.add_children("short_name")
+ for parent_code in codes
+ ]
+ context["codes"] = codes
+ return context
\ No newline at end of file
diff --git a/intervention/tables.py b/intervention/tables.py
index 8f312099..cff9391c 100644
--- a/intervention/tables.py
+++ b/intervention/tables.py
@@ -9,12 +9,11 @@ from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
-from django.utils.timezone import localtime
from django.utils.translation import gettext_lazy as _
from intervention.filters import InterventionTableFilter
from intervention.models import Intervention
-from konova.sub_settings.django_settings import DEFAULT_DATE_TIME_FORMAT, DEFAULT_DATE_FORMAT
+from konova.utils.message_templates import DATA_CHECKED_ON_TEMPLATE, DATA_IS_UNCHECKED, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from konova.utils.tables import BaseTable, TableRenderMixin
import django_tables2 as tables
@@ -108,16 +107,21 @@ class InterventionTable(BaseTable, TableRenderMixin):
"""
html = ""
checked = value is not None
- tooltip = _("Not checked yet")
+ previously_checked = record.get_last_checked_action()
+ tooltip = DATA_IS_UNCHECKED
if checked:
- value = value.timestamp
- value = localtime(value)
- checked_on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
- tooltip = _("Checked on {} by {}").format(checked_on, record.checked.user)
+ checked_on = value.get_timestamp_str_formatted()
+ tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.checked.user)
html += self.render_checked_star(
tooltip=tooltip,
icn_filled=checked,
)
+ if previously_checked and not checked:
+ checked_on = previously_checked.get_timestamp_str_formatted()
+ tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(checked_on, previously_checked.user)
+ html += self.render_previously_checked_star(
+ tooltip=tooltip,
+ )
return format_html(html)
def render_d(self, value, record: Intervention):
@@ -156,9 +160,7 @@ class InterventionTable(BaseTable, TableRenderMixin):
checked = value is not None
tooltip = _("Not recorded yet")
if checked:
- value = value.timestamp
- value = localtime(value)
- on = value.strftime(DEFAULT_DATE_TIME_FORMAT)
+ on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
diff --git a/intervention/templates/intervention/detail/view.html b/intervention/templates/intervention/detail/view.html
index 2d9628cd..1a596bb0 100644
--- a/intervention/templates/intervention/detail/view.html
+++ b/intervention/templates/intervention/detail/view.html
@@ -70,6 +70,11 @@
{% fa5_icon 'star' 'far' %}
+ {% if last_checked %}
+
+ {% fa5_icon 'star' 'fas' %}
+
+ {% endif %}
{% else %}
{% fa5_icon 'star' %}
@@ -120,7 +125,7 @@
{% trans 'Shared with' %} |
- {% for team in obj.teams.all %}
+ {% for team in obj.shared_teams %}
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
@@ -134,7 +139,9 @@
- {% include 'map/geom_form.html' %}
+
+ {% include 'map/geom_form.html' %}
+
{% include 'konova/includes/parcels/parcels.html' %}
diff --git a/intervention/templates/intervention/report/report.html b/intervention/templates/intervention/report/report.html
index 6e238fa2..1c1016d0 100644
--- a/intervention/templates/intervention/report/report.html
+++ b/intervention/templates/intervention/report/report.html
@@ -94,7 +94,9 @@
- {% include 'map/geom_form.html' %}
+
+ {% include 'map/geom_form.html' %}
+
{% include 'konova/includes/parcels/parcels.html' %}
diff --git a/intervention/tests/test_workflow.py b/intervention/tests/test_workflow.py
index 66770977..9124c3e8 100644
--- a/intervention/tests/test_workflow.py
+++ b/intervention/tests/test_workflow.py
@@ -46,6 +46,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
test_id = self.create_dummy_string()
test_title = self.create_dummy_string()
test_geom = self.create_dummy_geometry()
+ geom_json = self.create_geojson(test_geom)
new_url = reverse("intervention:new", args=())
@@ -59,7 +60,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
- "geom": test_geom.geojson,
+ "geom": geom_json,
}
response = self.client_user.post(
new_url,
diff --git a/intervention/views.py b/intervention/views.py
index f882f214..36577202 100644
--- a/intervention/views.py
+++ b/intervention/views.py
@@ -19,7 +19,7 @@ from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \
CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \
COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED, \
- RECORDED_BLOCKS_EDIT
+ RECORDED_BLOCKS_EDIT, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from konova.utils.user_checks import in_group
@@ -265,15 +265,18 @@ def detail_view(request: HttpRequest, id: str):
geom_form = SimpleGeomForm(
instance=intervention,
)
-
- parcels = intervention.get_underlying_parcels()
+ last_checked = intervention.get_last_checked_action()
+ last_checked_tooltip = ""
+ if last_checked:
+ last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
context = {
"obj": intervention,
+ "last_checked": last_checked,
+ "last_checked_tooltip": last_checked_tooltip,
"compensations": compensations,
"has_access": is_data_shared,
"geom_form": geom_form,
- "parcels": parcels,
"is_default_member": in_group(_user, DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP),
diff --git a/konova/admin.py b/konova/admin.py
index 213120ea..b30f4b14 100644
--- a/konova/admin.py
+++ b/konova/admin.py
@@ -98,6 +98,18 @@ class DeadlineAdmin(admin.ModelAdmin):
]
+class DeletableObjectMixinAdmin(admin.ModelAdmin):
+ class Meta:
+ abstract = True
+
+ def restore_deleted_data(self, request, queryset):
+ queryset = queryset.filter(
+ deleted__isnull=False
+ )
+ for entry in queryset:
+ entry.deleted.delete()
+
+
class BaseResourceAdmin(admin.ModelAdmin):
fields = [
"created",
@@ -109,7 +121,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
]
-class BaseObjectAdmin(BaseResourceAdmin):
+class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
search_fields = [
"identifier",
"title",
@@ -126,13 +138,6 @@ class BaseObjectAdmin(BaseResourceAdmin):
"deleted",
]
- def restore_deleted_data(self, request, queryset):
- queryset = queryset.filter(
- deleted__isnull=False
- )
- for entry in queryset:
- entry.deleted.delete()
-
# Outcommented for a cleaner admin backend on production
diff --git a/konova/autocompletes.py b/konova/autocompletes.py
index e6036f02..fbd92f75 100644
--- a/konova/autocompletes.py
+++ b/konova/autocompletes.py
@@ -96,7 +96,9 @@ class ShareTeamAutocomplete(Select2QuerySetView):
def get_queryset(self):
if self.request.user.is_anonymous:
return Team.objects.none()
- qs = Team.objects.all()
+ qs = Team.objects.filter(
+ deleted__isnull=True
+ )
if self.q:
# Due to privacy concerns only a full username match will return the proper user entry
qs = qs.filter(
@@ -108,6 +110,29 @@ class ShareTeamAutocomplete(Select2QuerySetView):
return qs
+class TeamAdminAutocomplete(Select2QuerySetView):
+ """ Autocomplete for share with teams
+
+ """
+ def get_queryset(self):
+ if self.request.user.is_anonymous:
+ return User.objects.none()
+ qs = User.objects.filter(
+ id__in=self.forwarded.get("members", [])
+ ).exclude(
+ id__in=self.forwarded.get("admins", [])
+ )
+ if self.q:
+ # Due to privacy concerns only a full username match will return the proper user entry
+ qs = qs.filter(
+ name__icontains=self.q
+ )
+ qs = qs.order_by(
+ "username"
+ )
+ return qs
+
+
class KonovaCodeAutocomplete(Select2GroupQuerySetView):
"""
Provides simple autocomplete functionality for codes
diff --git a/konova/filters/mixins.py b/konova/filters/mixins.py
index beb44dac..cfd9cc4c 100644
--- a/konova/filters/mixins.py
+++ b/konova/filters/mixins.py
@@ -305,7 +305,7 @@ class ShareableTableFilterMixin(django_filters.FilterSet):
if not value:
return queryset.filter(
Q(users__in=[self.user]) | # requesting user has access
- Q(teams__users__in=[self.user])
+ Q(teams__in=self.user.shared_teams)
).distinct()
else:
return queryset
diff --git a/konova/forms.py b/konova/forms.py
index a1e14478..0a341e0c 100644
--- a/konova/forms.py
+++ b/konova/forms.py
@@ -5,18 +5,20 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20
"""
-
+import json
from abc import abstractmethod
from bootstrap_modal_forms.forms import BSModalForm
from bootstrap_modal_forms.utils import is_ajax
from django import forms
from django.contrib import messages
+from django.contrib.gis import gdal
from django.db.models.fields.files import FieldFile
+from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import User
-from django.contrib.gis.forms import OSMWidget, MultiPolygonField
-from django.contrib.gis.geos import MultiPolygon
+from django.contrib.gis.forms import MultiPolygonField
+from django.contrib.gis.geos import MultiPolygon, Polygon
from django.db import transaction
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render
@@ -272,41 +274,85 @@ class SimpleGeomForm(BaseForm):
""" A geometry form for rendering geometry read-only using a widget
"""
+ read_only = True
geom = MultiPolygonField(
- srid=DEFAULT_SRID,
+ srid=DEFAULT_SRID_RLP,
label=_("Geometry"),
help_text=_(""),
label_suffix="",
required=False,
disabled=False,
- widget=OSMWidget(
- attrs={
- "map_width": 600,
- "map_height": 400,
- # default_zoom defines the nearest possible zoom level from which the JS automatically
- # zooms out if geometry requires a larger view port. So define a larger range for smaller geometries
- "default_zoom": 25,
- }
- )
)
def __init__(self, *args, **kwargs):
- read_only = kwargs.pop("read_only", True)
+ self.read_only = kwargs.pop("read_only", True)
super().__init__(*args, **kwargs)
# Initialize geometry
try:
geom = self.instance.geometry.geom
self.empty = geom.empty
+
+ if self.empty:
+ raise AttributeError
+
+ geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
+ geom = json.dumps(geojson)
except AttributeError:
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
- geom = None
+ geom = ""
self.empty = True
- self.fields["geom"].widget.attrs["default_zoom"] = 1
self.initialize_form_field("geom", geom)
- if read_only:
- self.fields["geom"].disabled = True
+
+ def is_valid(self):
+ super().is_valid()
+ is_valid = True
+
+ # Get geojson from form
+ geom = self.data["geom"]
+ if geom is None or len(geom) == 0:
+ # empty geometry is a valid geometry
+ return is_valid
+ geom = json.loads(geom)
+
+ # Write submitted data back into form field to make sure invalid geometry
+ # will be rendered again on failed submit
+ self.initialize_form_field("geom", self.data["geom"])
+
+ # Read geojson into gdal geometry
+ # HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
+ # this case)
+ features = []
+ features_json = geom.get("features", [])
+ for feature in features_json:
+ g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
+ if g.geom_type not in ["Polygon", "MultiPolygon"]:
+ self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
+ is_valid = False
+ return is_valid
+
+ polygon = Polygon.from_ewkt(g.ewkt)
+ is_valid = polygon.valid
+ if not is_valid:
+ self.add_error("geom", polygon.valid_reason)
+ return is_valid
+
+ features.append(polygon)
+ form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
+ for feature in features:
+ form_geom = form_geom.union(feature)
+
+ # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
+ if form_geom.geom_type != "MultiPolygon":
+ form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
+
+ # Write unioned Multipolygon into cleaned data
+ if self.cleaned_data is None:
+ self.cleaned_data = {}
+ self.cleaned_data["geom"] = form_geom.ewkt
+
+ return is_valid
def save(self, action: UserActionLogEntry):
""" Saves the form's geometry
diff --git a/konova/migrations/0010_auto_20220420_1034.py b/konova/migrations/0010_auto_20220420_1034.py
new file mode 100644
index 00000000..c916ff54
--- /dev/null
+++ b/konova/migrations/0010_auto_20220420_1034.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.1.3 on 2022-04-20 08:34
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('konova', '0009_auto_20220411_1004'),
+ ]
+
+ operations = [
+ migrations.AddConstraint(
+ model_name='parcel',
+ constraint=models.UniqueConstraint(fields=('district', 'municipal', 'parcel_group', 'flr', 'flrstck_nnr', 'flrstck_zhlr'), name='Unique parcel constraint'),
+ ),
+ ]
diff --git a/konova/migrations/0011_auto_20220420_1101.py b/konova/migrations/0011_auto_20220420_1101.py
new file mode 100644
index 00000000..b402818e
--- /dev/null
+++ b/konova/migrations/0011_auto_20220420_1101.py
@@ -0,0 +1,25 @@
+# Generated by Django 3.1.3 on 2022-04-20 09:01
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+ dependencies = [
+ ('konova', '0010_auto_20220420_1034'),
+ ]
+
+ operations = [
+ migrations.AddConstraint(
+ model_name='district',
+ constraint=models.UniqueConstraint(fields=('key', 'name'), name='Unique district constraint'),
+ ),
+ migrations.AddConstraint(
+ model_name='municipal',
+ constraint=models.UniqueConstraint(fields=('key', 'name', 'district'), name='Unique municipal constraint'),
+ ),
+ migrations.AddConstraint(
+ model_name='parcelgroup',
+ constraint=models.UniqueConstraint(fields=('key', 'name', 'municipal'), name='Unique parcel group constraint'),
+ ),
+ ]
diff --git a/konova/models/geometry.py b/konova/models/geometry.py
index 283f8508..b996feb7 100644
--- a/konova/models/geometry.py
+++ b/konova/models/geometry.py
@@ -5,11 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.11.21
"""
+import json
+
from django.contrib.gis.db.models import MultiPolygonField
-from django.db import models
+from django.contrib.gis.geos import Polygon
+from django.db import models, transaction
from django.utils import timezone
from konova.models import BaseResource, UuidModel
+from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.wfs.spatial import ParcelWFSFetcher
@@ -96,6 +100,7 @@ class Geometry(BaseResource):
objs += set_objs
return objs
+ @transaction.atomic
def update_parcels(self):
""" Updates underlying parcel information
@@ -152,6 +157,7 @@ class Geometry(BaseResource):
underlying_parcels.append(parcel_obj)
# Update the linked parcels
+ self.parcels.clear()
self.parcels.set(underlying_parcels)
# Set the calculated_on intermediate field, so this related data will be found on lookups
@@ -172,7 +178,6 @@ class Geometry(BaseResource):
Returns:
parcels (QuerySet): The related parcels as queryset
"""
-
parcels = self.parcels.filter(
parcelintersection__calculated_on__isnull=False,
).prefetch_related(
@@ -184,6 +189,44 @@ class Geometry(BaseResource):
return parcels
+ def count_underlying_parcels(self):
+ """ Getter for number of underlying parcels
+
+ Returns:
+
+ """
+ num_parcels = self.parcels.filter(
+ parcelintersection__calculated_on__isnull=False,
+ ).count()
+ return num_parcels
+
+ def as_feature_collection(self, srid=DEFAULT_SRID_RLP):
+ """ Returns a FeatureCollection structure holding all polygons of the MultiPolygon as single features
+
+ This method is used to convert a single MultiPolygon into multiple Polygons, which can be used as separated
+ features in the NETGIS map client.
+
+ Args:
+ srid (int): The spatial reference system identifier to be transformed to
+
+ Returns:
+ geojson (dict): The FeatureCollection json (as dict)
+ """
+ geom = self.geom
+ geom.transform(ct=srid)
+
+ polygons = []
+ for coords in geom.coords:
+ p = Polygon(coords[0], srid=geom.srid)
+ polygons.append(p)
+ geojson = {
+ "type": "FeatureCollection",
+ "features": [
+ json.loads(x.geojson) for x in polygons
+ ]
+ }
+ return geojson
+
class GeometryConflict(UuidModel):
"""
diff --git a/konova/models/object.py b/konova/models/object.py
index a1cff71b..b468932a 100644
--- a/konova/models/object.py
+++ b/konova/models/object.py
@@ -87,25 +87,15 @@ class BaseResource(UuidModel):
super().delete()
-class BaseObject(BaseResource):
- """
- A basic object model, which specifies BaseResource.
+class DeletableObjectMixin(models.Model):
+ """ Wraps deleted field and related functionality
- Mainly used for intervention, compensation, ecoaccount
"""
- identifier = models.CharField(max_length=1000, null=True, blank=True)
- title = models.CharField(max_length=1000, null=True, blank=True)
deleted = models.ForeignKey("user.UserActionLogEntry", on_delete=models.SET_NULL, null=True, blank=True, related_name='+')
- comment = models.TextField(null=True, blank=True)
- log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
class Meta:
abstract = True
- @abstractmethod
- def set_status_messages(self, request: HttpRequest):
- raise NotImplementedError
-
def mark_as_deleted(self, user, send_mail: bool = True):
""" Mark an entry as deleted
@@ -140,6 +130,25 @@ class BaseObject(BaseResource):
self.save()
+
+class BaseObject(BaseResource, DeletableObjectMixin):
+ """
+ A basic object model, which specifies BaseResource.
+
+ Mainly used for intervention, compensation, ecoaccount
+ """
+ identifier = models.CharField(max_length=1000, null=True, blank=True)
+ title = models.CharField(max_length=1000, null=True, blank=True)
+ comment = models.TextField(null=True, blank=True)
+ log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
+
+ class Meta:
+ abstract = True
+
+ @abstractmethod
+ def set_status_messages(self, request: HttpRequest):
+ raise NotImplementedError
+
def mark_as_edited(self, performing_user, request: HttpRequest = None, edit_comment: str = None):
""" In case the object or a related object changed the log history needs to be updated
@@ -408,6 +417,20 @@ class CheckableObjectMixin(models.Model):
self.log.add(action)
return action
+ def get_last_checked_action(self):
+ """ Getter for the most recent checked action on the log
+
+ Returns:
+ previously_checked (UserActionLogEntry): The most recent checked action
+ """
+ from user.models import UserAction
+ previously_checked = self.log.filter(
+ action=UserAction.CHECKED
+ ).order_by(
+ "-timestamp"
+ ).first()
+ return previously_checked
+
class ShareableObjectMixin(models.Model):
# Users having access on this object
@@ -470,8 +493,8 @@ class ShareableObjectMixin(models.Model):
Returns:
"""
- directly_shared = self.users.filter(id=user.id).exists()
- team_shared = self.teams.filter(
+ directly_shared = self.shared_users.filter(id=user.id).exists()
+ team_shared = self.shared_teams.filter(
users__in=[user]
).exists()
is_shared = directly_shared or team_shared
@@ -608,7 +631,9 @@ class ShareableObjectMixin(models.Model):
Returns:
teams (QuerySet)
"""
- return self.teams.all()
+ return self.teams.filter(
+ deleted__isnull=True
+ )
@abstractmethod
def get_share_url(self):
@@ -652,10 +677,21 @@ class GeoReferencedMixin(models.Model):
Returns:
parcels (Iterable): An empty list or a Queryset
"""
+ result = []
if self.geometry is not None:
- return self.geometry.get_underlying_parcels()
- else:
- return []
+ result = self.geometry.get_underlying_parcels()
+ return result
+
+ def count_underlying_parcels(self):
+ """ Getter for number of underlying parcels
+
+ Returns:
+
+ """
+ result = 0
+ if self.geometry is not None:
+ result = self.geometry.count_underlying_parcels()
+ return result
def set_geometry_conflict_message(self, request: HttpRequest):
if self.geometry is None:
diff --git a/konova/models/parcel.py b/konova/models/parcel.py
index f74b7af9..cc91e006 100644
--- a/konova/models/parcel.py
+++ b/konova/models/parcel.py
@@ -39,7 +39,17 @@ class District(UuidModel, AdministrativeSpatialReference):
""" The model District refers to "Kreis"
"""
- pass
+
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=[
+ "key",
+ "name",
+ ],
+ name="Unique district constraint"
+ )
+ ]
class Municipal(UuidModel, AdministrativeSpatialReference):
@@ -53,6 +63,18 @@ class Municipal(UuidModel, AdministrativeSpatialReference):
blank=True,
)
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=[
+ "key",
+ "name",
+ "district",
+ ],
+ name="Unique municipal constraint"
+ )
+ ]
+
class ParcelGroup(UuidModel, AdministrativeSpatialReference):
""" The model ParcelGroup refers to "Gemarkung", which is defined as a loose group of parcels
@@ -65,6 +87,18 @@ class ParcelGroup(UuidModel, AdministrativeSpatialReference):
blank=True,
)
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=[
+ "key",
+ "name",
+ "municipal",
+ ],
+ name="Unique parcel group constraint"
+ )
+ ]
+
class Parcel(UuidModel):
""" The Parcel model holds administrative data on covered properties.
@@ -106,6 +140,21 @@ class Parcel(UuidModel):
)
updated_on = models.DateTimeField(auto_now_add=True)
+ class Meta:
+ constraints = [
+ models.UniqueConstraint(
+ fields=[
+ "district",
+ "municipal",
+ "parcel_group",
+ "flr",
+ "flrstck_nnr",
+ "flrstck_zhlr",
+ ],
+ name="Unique parcel constraint"
+ )
+ ]
+
def __str__(self):
return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
diff --git a/konova/static/css/konova.css b/konova/static/css/konova.css
index 7e7f3fd1..ed797046 100644
--- a/konova/static/css/konova.css
+++ b/konova/static/css/konova.css
@@ -262,4 +262,13 @@ Similar to bootstraps 'shadow-lg'
padding-left: 2em;
}
- */
\ No newline at end of file
+ */
+.collapse-icn > i{
+ transition: all 0.3s ease;
+}
+.collapsed .collapse-icn > i{
+ transform: rotate(-90deg);
+}
+.tree-label.badge{
+ font-size: 90%;
+}
\ No newline at end of file
diff --git a/konova/sub_settings/django_settings.py b/konova/sub_settings/django_settings.py
index cbbf7fc5..54c35bee 100644
--- a/konova/sub_settings/django_settings.py
+++ b/konova/sub_settings/django_settings.py
@@ -46,8 +46,8 @@ ALLOWED_HOSTS = [
LOGIN_URL = "/login/"
# Session settings
-#SESSION_COOKIE_AGE = 30 * 60 # 30 minutes
-#SESSION_SAVE_EVERY_REQUEST = True
+SESSION_COOKIE_AGE = 60 * 60 # 60 minutes
+SESSION_SAVE_EVERY_REQUEST = True
# Application definition
@@ -192,6 +192,8 @@ STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'konova/static'),
+ os.path.join(BASE_DIR, 'templates/map/client'), # NETGIS map client files
+ os.path.join(BASE_DIR, 'templates/map/client/libs'), # NETGIS map client files
]
# DJANGO DEBUG TOOLBAR
diff --git a/konova/templates/konova/includes/comment_card.html b/konova/templates/konova/includes/comment_card.html
index a84f378e..d9ea59bc 100644
--- a/konova/templates/konova/includes/comment_card.html
+++ b/konova/templates/konova/includes/comment_card.html
@@ -6,7 +6,7 @@
{% if obj.comment %}
- | | |