summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2026-03-04 16:32:30 +0100
committerÉtienne Loks <etienne.loks@iggdrasil.net>2026-03-10 11:11:46 +0100
commit27fff0a390ea490047f0cfced48bad92f70401d1 (patch)
tree478158dc660f14f7c82e1f7dca660ef7575fe743
parent5311aea5f9b9bd0fbfb12e368560ddd5d150162b (diff)
downloadIshtar-27fff0a390ea490047f0cfced48bad92f70401d1.tar.bz2
Ishtar-27fff0a390ea490047f0cfced48bad92f70401d1.zip
🚧 Statement condition: views, forms, logic
-rw-r--r--archaeological_finds/forms_treatments.py259
-rw-r--r--archaeological_finds/models_finds.py13
-rw-r--r--archaeological_finds/templates/ishtar/forms/qa_statement_condition_form.html14
-rw-r--r--archaeological_finds/urls.py24
-rw-r--r--archaeological_finds/views.py61
5 files changed, 370 insertions, 1 deletions
diff --git a/archaeological_finds/forms_treatments.py b/archaeological_finds/forms_treatments.py
index 2c6e49c46..1713bff1a 100644
--- a/archaeological_finds/forms_treatments.py
+++ b/archaeological_finds/forms_treatments.py
@@ -17,6 +17,7 @@
# See the file COPYING for details.
+from copy import copy
import datetime
import logging
from collections import OrderedDict
@@ -26,7 +27,7 @@ from django import forms
from django.core import validators
from django.utils import timezone
from ishtar_common.utils import gettext_lazy as _
-from ishtar_common.forms import FormHeader
+from ishtar_common.forms import FloatField, FormHeader
from archaeological_finds import models
from archaeological_operations.forms import AdministrativeActForm, \
@@ -1174,3 +1175,259 @@ class QANewExhibitionLoanForm(IshtarForm):
obj = models.TreatmentFile.objects.create(**values)
self.exhibition.treatment_files.add(obj)
return obj
+
+
+class QAStatementCondition(ManageOldType, forms.Form):
+ form_label = _("Statement condition")
+ base_models = [
+ "qa_alterations",
+ "qa_alteration_causes",
+ "qa_recommended_treatments",
+ "qa_integrities",
+ "qa_conservatory_states",
+ "qa_museum_marking_type",
+ "qa_museum_inventory_marking_presence"
+ ]
+ associated_models = {
+ "qa_alterations": models.AlterationType,
+ "qa_alteration_causes": models.AlterationCauseType,
+ "qa_treatment_emergency_id": models.TreatmentEmergencyType,
+ "qa_conservatory_states": models.ConservatoryState,
+ "qa_recommended_treatments": models.RecommendedTreatmentType,
+ "qa_integrities": models.IntegrityType,
+ "follow_up_action": models.FollowUpActionType,
+ "statement_condition_type_id": models.StatementConditionType,
+ "treatment_person": Person,
+ "qa_museum_marking_type": models.MarkingType,
+ "qa_museum_inventory_marking_presence": models.InventoryMarkingPresence,
+ }
+ TREATMENT_FIELDS = ("treatment_type", "treatment_person", "treatment_organization")
+ HEADERS = {}
+ pk = forms.IntegerField(required=False, widget=forms.HiddenInput)
+ HEADERS["date"] = FormHeader(_("Statement of condition"))
+ date = DateField(label=_("Date"), initial=datetime.date.today)
+ applied = forms.ChoiceField(label=_("Input status"),
+ choices=models.StatementCondition.APPLIED_CHOICES)
+ statement_condition_type_id = forms.ChoiceField(label=_("Type"), choices=[],
+ required=True)
+ verification_officer_id = forms.IntegerField(
+ label=_("Verification officer"),
+ widget=widgets.JQueryAutoComplete(
+ reverse_lazy('autocomplete-person'), associated_model=Person,
+ new=True),
+ validators=[valid_id(Person)], required=False)
+ campaign_number = forms.CharField(label=_("Campaign/observation number"),
+ required=False)
+ report_number = forms.CharField(label=_("Report number"), required=False)
+ follow_up_action = forms.MultipleChoiceField(
+ label=_("Follow-up actions"),
+ choices=[],
+ widget=widgets.Select2Multiple,
+ required=False,
+ )
+ observations = forms.CharField(label=_("Observations"), widget=forms.Textarea,
+ required=False)
+
+ HEADERS["treatment_type"] = FormHeader(_("Treatment"), collapse=True)
+ treatment_type = forms.ChoiceField(label=_("Treatment type"), choices=[],
+ required=False)
+ treatment_person = forms.IntegerField(
+ label=_("Responsible"),
+ widget=widgets.JQueryAutoComplete(
+ reverse_lazy('autocomplete-person'), associated_model=Person,
+ new=True),
+ validators=[valid_id(Person)], required=False)
+ treatment_organization = forms.IntegerField(
+ label=_("Organization"),
+ widget=widgets.JQueryAutoComplete(
+ reverse_lazy('autocomplete-organization'),
+ associated_model=Organization, new=True),
+ validators=[valid_id(Organization)], required=False)
+
+ HEADERS["qa_description"] = FormHeader(_("Find"))
+ qa_description = forms.CharField(
+ label=_("Description"), widget=forms.Textarea, required=False
+ )
+
+ HEADERS["qa_museum_inventory_marking_presence"] = FormHeader(_("Museum"))
+ qa_museum_inventory_marking_presence = forms.MultipleChoiceField(
+ label=_("Presence of inventory marking"),
+ choices=[],
+ widget=widgets.Select2Multiple,
+ required=False,
+ )
+ qa_museum_marking_type = forms.MultipleChoiceField(
+ label=_("Type of marking"),
+ choices=[],
+ widget=widgets.Select2Multiple,
+ required=False,
+ )
+
+ HEADERS["find_number"] = FormHeader(_("Dimensions / Quantities"))
+ qa_find_number = forms.IntegerField(label=_("Number of remains"), required=False)
+ qa_museum_observed_quantity = forms.IntegerField(
+ label=_("Observed quantity"), required=False)
+ qa_length = FloatField(
+ label=_("Length (cm)"), widget=widgets.CentimeterMeterWidget, required=False
+ )
+ qa_width = FloatField(
+ label=_("Width (cm)"), required=False, widget=widgets.CentimeterMeterWidget
+ )
+ qa_height = FloatField(
+ label=_("Height (cm)"), widget=widgets.CentimeterMeterWidget, required=False
+ )
+ qa_thickness = FloatField(
+ label=_("Thickness (cm)"), widget=widgets.CentimeterMeterWidget, required=False
+ )
+ qa_diameter = FloatField(
+ label=_("Diameter (cm)"), widget=widgets.CentimeterMeterWidget, required=False
+ )
+ qa_circumference = FloatField(
+ label=_("Circumference (cm)"),
+ widget=widgets.CentimeterMeterWidget,
+ required=False,
+ )
+ qa_volume = FloatField(label=_("Volume (l)"), required=False)
+ qa_weight = FloatField(
+ label=_("Weight (g)"), widget=widgets.GramKilogramWidget, required=False
+ )
+ qa_clutter_long_side = FloatField(
+ label=_("Clutter long side (cm)"),
+ widget=widgets.CentimeterMeterWidget,
+ required=False,
+ )
+ qa_clutter_short_side = FloatField(
+ label=_("Clutter short side (cm)"),
+ widget=widgets.CentimeterMeterWidget,
+ required=False,
+ )
+ qa_clutter_height = FloatField(
+ label=_("Clutter height (cm)"),
+ widget=widgets.CentimeterMeterWidget,
+ required=False,
+ )
+ qa_dimensions_comment = forms.CharField(
+ label=_("Dimensions comment"), required=False, widget=forms.Textarea
+ )
+
+ HEADERS["qa_integrities"] = FormHeader(_("Preservation"))
+ qa_integrities = forms.MultipleChoiceField(
+ label=_("Integrity"),
+ choices=[],
+ widget=widgets.Select2Multiple,
+ required=False,
+ )
+ qa_conservatory_states = forms.MultipleChoiceField(
+ label=_("Conservatory states"),
+ choices=[],
+ widget=widgets.Select2Multiple,
+ required=False,
+ )
+ qa_alterations = forms.MultipleChoiceField(
+ label=_("Alteration"),
+ choices=[],
+ widget=widgets.Select2Multiple,
+ required=False,
+ )
+ qa_alteration_causes = forms.MultipleChoiceField(
+ label=_("Alteration cause"),
+ choices=[],
+ widget=widgets.Select2Multiple,
+ required=False,
+ )
+ qa_recommended_treatments = forms.MultipleChoiceField(
+ label=_("Recommended treatments"),
+ choices=[],
+ widget=widgets.Select2Multiple,
+ required=False,
+ )
+ qa_treatment_emergency_id = forms.ChoiceField(
+ label=_("Treatment emergency"), choices=[], required=False
+ )
+ qa_conservatory_comment = forms.CharField(
+ label=_("Conservatory comment"), required=False, widget=forms.Textarea
+ )
+
+ TYPES = [
+ FieldType("qa_alterations", models.AlterationType, True),
+ FieldType("qa_alteration_causes", models.AlterationCauseType, True),
+ FieldType("qa_conservatory_states", models.ConservatoryState, is_multiple=True),
+ FieldType("follow_up_action", models.FollowUpActionType, is_multiple=True),
+ FieldType("qa_integrities", models.IntegrityType, is_multiple=True),
+ FieldType("qa_museum_marking_type", models.MarkingType, is_multiple=True),
+ FieldType("qa_museum_inventory_marking_presence", models.InventoryMarkingPresence,
+ is_multiple=True),
+ FieldType("qa_recommended_treatments", models.RecommendedTreatmentType, True),
+ FieldType("statement_condition_type_id", models.StatementConditionType,
+ empty_first=False),
+ FieldType("qa_treatment_emergency_id", models.TreatmentEmergencyType),
+ FieldType("treatment_type", models.TreatmentType, empty_first=False,
+ extra_args={'dct': {"is_statement_condition": True}}),
+ ]
+ PROFILE_FILTER = {
+ "museum": ["qa_museum_observed_quantity", "qa_museum_marking_type",
+ "qa_museum_inventory_marking_presence"]
+ }
+
+ def __init__(self, *args, **kwargs):
+ self.current_item = kwargs.pop("current_item")
+ self.user = None
+ if 'user' in kwargs:
+ self.user = kwargs.pop('user')
+ super().__init__(*args, **kwargs)
+ if not self.user:
+ return
+ q = Person.objects.filter(ishtaruser__pk=self.user.pk)
+ if q.count():
+ person = q.all()[0]
+ self.fields['treatment_person'].initial = person.pk
+ self.fields['verification_officer_id'].initial = person.pk
+
+ def save(self):
+ data = copy(self.cleaned_data)
+ data["find_id"] = self.current_item.pk
+ applied = data.pop("applied")
+ follow_up_actions = data.pop("follow_up_action")
+ # remove m2m fields
+ m2m = {}
+ for m2m_attr in models.StatementCondition.OVERLOADED_M2M_FIELDS:
+ key = f"qa_{m2m_attr}"
+ if key in data:
+ m2m[m2m_attr] = data.pop(key)
+ # remove treatment fields
+ treatment = {}
+ for k in self.TREATMENT_FIELDS:
+ if k in data.keys():
+ treatment[k] = data.pop(k)
+ # remove "qa_" prefix for remaining fields
+ for k in list(data.keys()):
+ if k.startswith("qa_"):
+ data[k[3:]] = data.pop(k)
+ created = None
+ if data.get("pk", None):
+ # update condition
+ created = True
+ pk = data.pop("pk")
+ models.StatementCondition.objects.filter(pk=pk).update(**data)
+ st = models.StatementCondition.objects.get(pk=pk)
+ else:
+ # new statement condition
+ st = models.StatementCondition.objects.create(**data)
+ st.follow_up_actions.clear()
+ for action in follow_up_actions:
+ st.follow_up_actions.add(action)
+ for m2m_attr, value in m2m.items():
+ # update m2m
+ value = list(sorted([int(v) for v in value]))
+ if not created:
+ current_value = getattr(st, m2m_attr).values_list(
+ "pk", flat=True).order_by("pk")
+ if current_value == value:
+ continue
+ getattr(st, m2m_attr).clear()
+ getattr(st, m2m_attr).add(*value)
+ if applied == "D":
+ # draft exit
+ return
+ st.applied = applied
+ st.apply_validation(treatment)
diff --git a/archaeological_finds/models_finds.py b/archaeological_finds/models_finds.py
index 9e9886635..f6b0c42b8 100644
--- a/archaeological_finds/models_finds.py
+++ b/archaeological_finds/models_finds.py
@@ -226,6 +226,8 @@ class TreatmentType(HierarchicalType):
else:
type_list = [idx for idx, __ in types]
dct["available"] = True
+ if empty_first and type_list:
+ type_list = type_list[1:]
q = cls.objects.filter(**dct).exclude(pk__in=type_list)
for t in q.all():
if instances:
@@ -2911,6 +2913,17 @@ class Find(
True,
),
]
+ if can_edit_find and not is_locked:
+ actions += [
+ (
+ reverse("find-statement-condition-add", args=[self.pk]),
+ _("Add statement condition"),
+ "fa fa-plus",
+ _("statement condition"),
+ "",
+ True,
+ ),
+ ]
can_add_geo = profile.mapping and self.can_do(request,
"ishtar_common.add_geovectordata")
if can_add_geo:
diff --git a/archaeological_finds/templates/ishtar/forms/qa_statement_condition_form.html b/archaeological_finds/templates/ishtar/forms/qa_statement_condition_form.html
new file mode 100644
index 000000000..5307d1db8
--- /dev/null
+++ b/archaeological_finds/templates/ishtar/forms/qa_statement_condition_form.html
@@ -0,0 +1,14 @@
+{% extends "ishtar/forms/qa_form.html" %}
+{% load i18n inline_formset table_form %}
+
+{% block js_ready %}
+$("#collapse-parent-traitement").hide();
+$("#id_applied").on("change", function(){
+ if ($(this).val() == 'T'){
+ $("#collapse-parent-traitement").show();
+ $("#collapse-traitement").collapse('show');
+ } else {
+ $("#collapse-parent-traitement").hide();
+ }
+});
+{% endblock %}
diff --git a/archaeological_finds/urls.py b/archaeological_finds/urls.py
index 380d16cec..9a573198c 100644
--- a/archaeological_finds/urls.py
+++ b/archaeological_finds/urls.py
@@ -834,6 +834,30 @@ urlpatterns = [
)(views.autocomplete_basefind),
name="autocomplete-basefind",
),
+ path(
+ "find-statement-condition/<int:find_pk>/",
+ check_permissions(["archaeological_finds.change_find",
+ "archaeological_finds.change_own_find"])(
+ views.statement_condition_form
+ ),
+ name="find-statement-condition-add",
+ ),
+ path(
+ "find-statement-condition/<int:find_pk>/<int:statement_condition_pk>/",
+ check_permissions(["archaeological_finds.change_find",
+ "archaeological_finds.change_own_find"])(
+ views.statement_condition_form
+ ),
+ name="find-statement-condition-modify",
+ ),
+ path(
+ "find-statement-condition-delete/<int:statement_condition_pk>/",
+ check_permissions(["archaeological_finds.change_find",
+ "archaeological_finds.change_own_find"])(
+ views.statement_condition_delete_form
+ ),
+ name="find-statement-condition-delete",
+ ),
]
urlpatterns += get_urls_for_model(models.Find, views, own=True, autocomplete=True)
diff --git a/archaeological_finds/views.py b/archaeological_finds/views.py
index b289ac075..96b60587a 100644
--- a/archaeological_finds/views.py
+++ b/archaeological_finds/views.py
@@ -1520,3 +1520,64 @@ def qa_gam_export_views(request, pk, *args, **kwargs):
]
dct["unavailable"] = True
return render(request, "ishtar/forms/qa_form.html", dct)
+
+
+def statement_condition_form(request, find_pk, statement_condition_pk=None,
+ current_right=None):
+ try:
+ item = models.Find.objects.get(pk=find_pk)
+ except models.Find.DoesNotExist:
+ raise Http404()
+ # permission not provided
+ if not current_right:
+ raise PermissionDenied()
+ # specificaly check permission for own item, otherwise already checked
+ if "_own_" in current_right:
+ if not request.user.has_perm(current_right, item):
+ raise PermissionDenied()
+ initial = {}
+ if statement_condition_pk:
+ try:
+ statement_condition = models.StatementCondition.objects.get(
+ pk=statement_condition_pk)
+ initial = statement_condition.get_initial()
+ except models.StatementCondition.DoesNotExist:
+ raise Http404()
+ else:
+ # redirect to edit if there is already a draft
+ q = models.StatementCondition.objects.filter(find_id=find_pk, applied="D")
+ if q.exists():
+ sc_pk = q.values_list("pk", flat=True)[0]
+ return HttpResponseRedirect(
+ reverse("find-statement-condition-modify", args=[find_pk, sc_pk]))
+ initial = models.StatementCondition.get_initial_from_find(item)
+ if request.method == 'POST':
+ form = forms_treatments.QAStatementCondition(
+ request.POST, current_item=item, user=request.user.ishtaruser)
+ if form.is_valid():
+ form.save()
+ return HttpResponseRedirect(reverse("success"))
+ else:
+ form = forms_treatments.QAStatementCondition(
+ current_item=item, user=request.user.ishtaruser, initial=initial)
+ button_name = _("Add") if not statement_condition_pk else _("Modify")
+ icon = "fa fa-plus" if not statement_condition_pk else "fa fa-pencil"
+ url = "find-statement-condition-"
+ url_args = [find_pk]
+ if statement_condition_pk:
+ url_args.append(statement_condition_pk)
+ url += "modify"
+ else:
+ url += "add"
+ return render(
+ request,
+ "ishtar/forms/qa_statement_condition_form.html", {
+ "page_name": _("Statement condition"),
+ "icon": icon,
+ "action_name": button_name,
+ "form": form,
+ "modal_size": "large",
+ "url": reverse(url, args=url_args)
+ })
+
+statement_condition_delete_form = statement_condition_form