summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorÉtienne Loks <etienne.loks@iggdrasil.net>2024-10-10 13:42:18 +0200
committerÉtienne Loks <etienne.loks@iggdrasil.net>2024-10-14 16:48:37 +0200
commit435797a54e4d322a46711f303c2fc1fd5286330e (patch)
tree7c209135a17fe9b12fa2e2e844f345debed45d97
parent69df0a88eeaafeee81d76a3307e79fe5cad8ecf8 (diff)
downloadIshtar-435797a54e4d322a46711f303c2fc1fd5286330e.tar.bz2
Ishtar-435797a54e4d322a46711f303c2fc1fd5286330e.zip
✨ site and operation relations forms refactoring
-rw-r--r--archaeological_context_records/views.py2
-rw-r--r--archaeological_operations/forms.py70
-rw-r--r--archaeological_operations/models.py30
-rw-r--r--archaeological_operations/urls.py34
-rw-r--r--archaeological_operations/views.py144
-rw-r--r--ishtar_common/static/js/ishtar.js21
-rw-r--r--ishtar_common/templates/blocks/bs_alert_message.html4
-rw-r--r--ishtar_common/templates/ishtar/blocks/window_nav.html4
-rw-r--r--ishtar_common/templates/ishtar/forms/modify_relations.html9
-rw-r--r--ishtar_common/templatetags/ishtar_helpers.py2
-rw-r--r--ishtar_common/templatetags/window_header.py7
-rw-r--r--ishtar_common/urls_converters.py29
12 files changed, 287 insertions, 69 deletions
diff --git a/archaeological_context_records/views.py b/archaeological_context_records/views.py
index 98236dcd6..330a4ab3e 100644
--- a/archaeological_context_records/views.py
+++ b/archaeological_context_records/views.py
@@ -195,7 +195,7 @@ def reset_wizards(request):
context_record_modify_relations = get_relation_modify(
models.ContextRecord, models.RecordRelations,
- "context-record-relation-modify", forms.RecordRelationsFormSet,
+ forms.RecordRelationsFormSet, "context-record-relation-modify",
filter_operations=True
)
diff --git a/archaeological_operations/forms.py b/archaeological_operations/forms.py
index 433f3fd54..1a41503d4 100644
--- a/archaeological_operations/forms.py
+++ b/archaeological_operations/forms.py
@@ -425,30 +425,59 @@ class RecordRelationsForm(ManageOldType):
class RecordRelationsFormSetBase(FormSet):
delete_widget = forms.CheckboxInput
- # passing left_record should be nicely done with form_kwargs with Django 1.9
- # with no need of all these complications
-
- def __init__(self, *args, **kwargs):
- self.left_record = None
- if 'left_record' in kwargs:
- self.left_record = kwargs.pop('left_record')
- super().__init__(*args, **kwargs)
-
- def _construct_forms(self):
- # instantiate all the forms and put them in self.forms
- self.forms = []
- for i in range(self.total_form_count()):
- self.forms.append(self._construct_form(
- i, left_record=self.left_record))
RecordRelationsFormSet = formset_factory(
- RecordRelationsForm, can_delete=True, formset=RecordRelationsFormSetBase)
+ RecordRelationsForm, can_delete=True, formset=RecordRelationsFormSetBase,
+ extra=3
+)
RecordRelationsFormSet.form_label = _("Operations - Relations")
RecordRelationsFormSet.form_admin_name = _("Operation - Relations")
RecordRelationsFormSet.form_slug = "operation-relations"
+class OpeSiteRelationsForm(ManageOldType):
+ associated_models = {'right_record': models.ArchaeologicalSite}
+
+ right_record = forms.IntegerField(
+ label="Site",
+ widget=widgets.JQueryAutoComplete(
+ reverse_lazy('autocomplete-archaeologicalsite'),
+ associated_model=models.ArchaeologicalSite),
+ validators=[valid_id(models.ArchaeologicalSite)], required=False)
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ self.fields["right_record"].label = get_current_profile().get_site_label()
+
+OpeSiteRelationsFormSet = formset_factory(
+ OpeSiteRelationsForm, can_delete=True, formset=RecordRelationsFormSetBase,
+ extra=3
+)
+OpeSiteRelationsFormSet.form_label = _("Operations - Sites relations")
+OpeSiteRelationsFormSet.form_admin_name = _("Operation - Sites - relations")
+OpeSiteRelationsFormSet.form_slug = "operation-site-relations"
+
+
+class SiteOpeRelationsForm(ManageOldType):
+ associated_models = {'right_record': models.Operation}
+
+ right_record = forms.IntegerField(
+ label=_("Operation"),
+ widget=widgets.JQueryAutoComplete(
+ reverse_lazy('autocomplete-operation'),
+ associated_model=models.Operation),
+ validators=[valid_id(models.Operation)], required=False)
+
+SiteOpeRelationsFormSet = formset_factory(
+ SiteOpeRelationsForm, can_delete=True, formset=RecordRelationsFormSetBase,
+ extra=3
+)
+SiteOpeRelationsFormSet.form_label = _("Sites - Operations relations")
+SiteOpeRelationsFormSet.form_admin_name = _("Sites - Operation relations")
+SiteOpeRelationsFormSet.form_slug = "siteoperation-relations"
+
+
class OperationSelect(GeoItemSelect):
_model = models.Operation
form_admin_name = _("Operation - 001 - Search")
@@ -671,7 +700,6 @@ class OperationFormGeneral(CustomForm, ManageOldType):
'collaborator': Person,
'remain': models.RemainType,
'period': models.Period,
- 'archaeological_site': models.ArchaeologicalSite,
'town': Town,
}
HEADERS['code_patriarche'] = FormHeader(_("General"))
@@ -691,9 +719,6 @@ class OperationFormGeneral(CustomForm, ManageOldType):
label=_("Towns"),
model=Town,
required=False, remote=True)
- archaeological_site = widgets.Select2MultipleField(
- model=models.ArchaeologicalSite,
- required=False, remote=True)
year = forms.IntegerField(label=_("Year"),
initial=lambda: datetime.datetime.now().year,
validators=[validators.MinValueValidator(1000),
@@ -845,15 +870,10 @@ class OperationFormGeneral(CustomForm, ManageOldType):
if not profile.underwater:
self._remove_fields(("drassm_code",))
data = kwargs.get("data", {}) or kwargs.get("initial", {})
- has_site = any(1 for k, v in data.items() if v and k.split("-")[-1] == "archaeological_site")
- if not has_site and not profile.archaeological_site:
- self._remove_fields(("archaeological_site",))
if 'collaborator' in self.fields:
self.fields['collaborator'].widget.attrs['full-width'] = True
if towns and towns != -1:
self.fields['town'].choices = [('', '--')] + towns
- if 'archaeological_site' in self.fields:
- self.fields['archaeological_site'].label = get_current_profile().get_site_label()
def clean(self):
cleaned_data = self.cleaned_data
diff --git a/archaeological_operations/models.py b/archaeological_operations/models.py
index f8c8f20a0..63904bebe 100644
--- a/archaeological_operations/models.py
+++ b/archaeological_operations/models.py
@@ -759,18 +759,31 @@ class ArchaeologicalSite(
base_finds__context_record__archaeological_site__pk=self.pk
)
- def get_extra_actions(self, request):
+ def get_extra_actions(self, request, window_id=None):
"""
For sheet template
"""
# url, base_text, icon, extra_text, extra css class, is a quick action
actions = super(ArchaeologicalSite, self).get_extra_actions(request)
- # is_locked = self.is_locked(request.user)
+ is_locked = self.is_locked(request.user)
profile = get_current_profile()
+ can_edit_site = self.can_do(request, "change_archaeologicalsite")
can_add_geo = profile.mapping and self.can_do(request, "add_geovectordata")
if can_add_geo:
actions.append(self.get_add_geo_action())
+ if can_edit_site and not is_locked:
+ actions += [
+ (
+ reverse("site-operation-relations-modify", args=[self.pk, window_id]),
+ _("Modify site-operation relations"),
+ "fa fa-retweet",
+ _("operations"),
+ "",
+ True,
+ ),
+ ]
+
can_create_operation = self.can_do(request, "change_operation")
if can_create_operation and not self.operations.count():
actions.append(
@@ -796,7 +809,6 @@ class ArchaeologicalSite(
)
)
- can_edit_site = self.can_do(request, "change_archaeologicalsite")
if can_edit_site:
actions += [
(
@@ -2096,7 +2108,7 @@ class Operation(
).distinct("location", "index")
return q
- def get_extra_actions(self, request):
+ def get_extra_actions(self, request, window_id=None):
"""
For sheet template
"""
@@ -2160,6 +2172,16 @@ class Operation(
True,
),
]
+ actions += [
+ (
+ reverse("operation-site-relations-modify", args=[self.pk, window_id]),
+ _("Modify operation-site relations"),
+ "fa fa-retweet",
+ profile.get_site_label("plural").lower(),
+ "",
+ True,
+ ),
+ ]
if can_edit_operation:
actions += [
diff --git a/archaeological_operations/urls.py b/archaeological_operations/urls.py
index b87d6c5fd..7f5080c39 100644
--- a/archaeological_operations/urls.py
+++ b/archaeological_operations/urls.py
@@ -18,13 +18,17 @@
# See the file COPYING for details.
from django.conf.urls import url
-from django.urls import path
+from django.urls import path, register_converter
+from ishtar_common import urls_converters
from ishtar_common.utils import check_rights
from archaeological_operations import views
from archaeological_operations import views_api
from archaeological_operations import models
+
+register_converter(urls_converters.UnderscoreSlug, "uslug")
+
# be carreful: each check_rights must be relevant with ishtar_menu
# forms
@@ -311,6 +315,34 @@ urlpatterns = [
),
name="operation-relation-modify",
),
+ path(
+ "operation-site-relations-modify/<int:pk>/",
+ check_rights(["change_operation", "change_own_operation"])(
+ views.operation_site_modify_relations
+ ),
+ name="operation-site-relations-modify",
+ ),
+ path(
+ "operation-site-relations-modify/<int:pk>/<uslug:window_id>/",
+ check_rights(["change_operation", "change_own_operation"])(
+ views.operation_site_modify_relations
+ ),
+ name="operation-site-relations-modify",
+ ),
+ path(
+ "site-operation-relations-modify/<int:pk>/",
+ check_rights(["change_operation", "change_own_operation"])(
+ views.site_operation_modify_relations
+ ),
+ name="site-operation-relations-modify",
+ ),
+ path(
+ "site-operation-relations-modify/<int:pk>/<uslug:window_id>/",
+ check_rights(["change_operation", "change_own_operation"])(
+ views.site_operation_modify_relations
+ ),
+ name="site-operation-relations-modify",
+ ),
url(
r"^operation-qa-bulk-update/(?P<pks>[0-9-]+)?/$",
check_rights(["change_operation", "change_own_operation"])(
diff --git a/archaeological_operations/views.py b/archaeological_operations/views.py
index 70732d04e..bf33e45b6 100644
--- a/archaeological_operations/views.py
+++ b/archaeological_operations/views.py
@@ -502,7 +502,37 @@ operation_modify_parcels = get_parcel_modify(
RELATION_FORMSET_EXTRA_FORM = 3
-def get_relation_modify(model, model_relation, url_name, formset_class, filter_operations=False):
+def _formset_get_deleted(request, data, pk_key):
+ new_data = dict(request.POST)
+ new_data = {k: new_data[k][0] for k in new_data} # convert POST to classic dict
+ no_values = list(range(data["form-TOTAL_FORMS"]))
+ deleted = {}
+ for k, value in list(new_data.items()):
+ if not value or not k.startswith("form-"):
+ continue
+ try:
+ form_number = int(k.split("-")[1])
+ except (ValueError, IndexError) as __:
+ continue
+ if k.endswith("-DELETE"):
+ if new_data.get(f"form-{form_number}-{pk_key}", None):
+ deleted[form_number] = new_data[f"form-{form_number}-{pk_key}"]
+ if form_number not in no_values: # put it back in no values
+ no_values.append(form_number)
+ else:
+ new_data.pop(k)
+ elif form_number in no_values and form_number not in deleted:
+ no_values.pop(no_values.index(form_number))
+ for no_value in no_values:
+ for k in list(new_data.keys()):
+ if k.startswith(f"form-{no_value}-"):
+ new_data.pop(k)
+ data["form-TOTAL_FORMS"] = data["form-TOTAL_FORMS"] - len(no_values)
+ new_data.update(data)
+ return new_data, deleted
+
+
+def get_relation_modify(model, model_relation, formset_class, url_name, filter_operations=False):
def _modify_relation(request, pk, current_right=None):
try:
item = model.objects.get(pk=pk)
@@ -512,6 +542,7 @@ def get_relation_modify(model, model_relation, url_name, formset_class, filter_o
if not item.is_own(request.user):
raise PermissionDenied()
relations = model_relation.objects.filter(left_record_id=pk).all()
+ form_kwargs = {"left_record": item}
items, current_items = [], []
if filter_operations:
@@ -539,37 +570,9 @@ def get_relation_modify(model, model_relation, url_name, formset_class, filter_o
if filter_operations:
data["CURRENT_ITEMS"] = items
if request.method == 'POST':
- new_data = dict(request.POST)
- new_data = {k: new_data[k][0] for k in new_data} # convert POST to classic dict
-
# remove empty lines and get deleted
- no_values = list(range(data["form-TOTAL_FORMS"]))
- deleted = {}
- for k, value in list(new_data.items()):
- if not value or not k.startswith("form-"):
- continue
- try:
- form_number = int(k.split("-")[1])
- except (ValueError, IndexError) as __:
- continue
- if k.endswith("-DELETE"):
- if new_data.get(f"form-{form_number}-pk", None):
- deleted[form_number] = new_data[f"form-{form_number}-pk"]
- if form_number not in no_values: # put it back in no values
- no_values.append(form_number)
- else:
- new_data.pop(k)
- elif form_number in no_values and form_number not in deleted:
- no_values.pop(no_values.index(form_number))
- for no_value in no_values:
- for k in list(new_data.keys()):
- if k.startswith(f"form-{no_value}-"):
- new_data.pop(k)
- data["form-TOTAL_FORMS"] = data["form-TOTAL_FORMS"] - len(no_values)
-
- new_data.update(data)
- formset = formset_class(data=new_data)
-
+ new_data, deleted = _formset_get_deleted(request, data, "pk")
+ formset = formset_class(data=new_data, form_kwargs=form_kwargs)
if formset.is_valid():
is_valid = True
# delete
@@ -609,7 +612,7 @@ def get_relation_modify(model, model_relation, url_name, formset_class, filter_o
if is_valid:
return redirect(reverse(url_name, args=[pk]))
else:
- formset = formset_class(initial=initial, data=data)
+ formset = formset_class(initial=initial, data=data, form_kwargs=form_kwargs)
return render(request, 'ishtar/forms/modify_relations.html', {
'formset': formset,
@@ -620,10 +623,85 @@ def get_relation_modify(model, model_relation, url_name, formset_class, filter_o
operation_modify_relations = get_relation_modify(
models.Operation, models.RecordRelations,
- "operation-relation-modify", forms.RecordRelationsFormSet
+ forms.RecordRelationsFormSet, "operation-relation-modify"
)
+RELATION_LIMIT = 50
+
+
+def operation_site_modify(model, related_model, related_key, formset_class, url_name):
+
+ def _get_initial(q_relations):
+ initial = []
+ current_relation_ids = q_relations.values_list("id", flat=True)
+ excess = current_relation_ids.count() > RELATION_LIMIT and RELATION_LIMIT
+ for relation_id in current_relation_ids[:RELATION_LIMIT]:
+ initial.append({
+ "right_record": relation_id,
+ })
+ return initial, current_relation_ids, excess
+ get_initial = _get_initial
+
+ def view(request, pk, window_id=None, current_right=None):
+ try:
+ item = model.objects.get(pk=pk)
+ except model.DoesNotExist:
+ raise Http404()
+ if "_own_" in current_right:
+ if not item.is_own(request.user):
+ raise PermissionDenied()
+ q_relations = getattr(item, related_key)
+ initial, current_relation_ids, excess = get_initial(q_relations)
+ if request.method == 'POST':
+ formset = formset_class(data=request.POST)
+ if formset.is_valid():
+ deleted_ids = []
+ for form in formset.deleted_forms:
+ deleted_id = form.cleaned_data.get("right_record", None)
+ try:
+ deleted_id = int(deleted_id)
+ except (ValueError, TypeError):
+ continue
+ deleted_ids.append(deleted_id)
+ if deleted_id in current_relation_ids:
+ try:
+ del_item = related_model.objects.get(pk=deleted_id)
+ except related_model.DoesNotExist:
+ continue
+ q_relations.remove(del_item)
+
+ for idx_form, data in enumerate(formset.cleaned_data):
+ if not data.get('right_record'):
+ continue
+ try:
+ new_item_id = int(data.get("right_record"))
+ if new_item_id in deleted_ids or new_item_id in current_relation_ids:
+ continue
+ new_item = related_model.objects.get(pk=new_item_id)
+ except (ValueError, TypeError, related_model.DoesNotExist):
+ continue
+ q_relations.add(new_item)
+ initial, __, __ = get_initial(q_relations)
+ formset = formset_class(initial=initial)
+ else:
+ formset = formset_class(initial=initial)
+
+ return render(request, 'ishtar/forms/modify_relations.html', {
+ 'formset': formset,
+ "url": reverse(url_name, args=[pk]),
+ "window_id": window_id,
+ "excess": str(
+ _("Too many relations. Only the first {limit} items are displayed.")
+ ).format(limit=excess) if excess else None
+ })
+ return view
+
+
+operation_site_modify_relations = operation_site_modify(models.Operation, models.ArchaeologicalSite, "archaeological_sites", forms.OpeSiteRelationsFormSet, "operation-site-relations-modify")
+site_operation_modify_relations = operation_site_modify(models.ArchaeologicalSite, models.Operation, "operations", forms.SiteOpeRelationsFormSet, "site-operation-relations-modify")
+
+
# archaeological sites
diff --git a/ishtar_common/static/js/ishtar.js b/ishtar_common/static/js/ishtar.js
index 2bcfe5a7e..1ef2e4c1d 100644
--- a/ishtar_common/static/js/ishtar.js
+++ b/ishtar_common/static/js/ishtar.js
@@ -454,8 +454,29 @@ $(document).ready(function(){
$("[aria-controls='" + $(this).attr("id") + "']").click();
}
});
+ register_modal_on_close();
});
+
+var modal_on_close = new Array();
+
+var register_modal_on_close = function(){
+ $(".modal").each(
+ function(){
+ if (this.id) {
+ modal_on_close[this.id] = undefined;
+ $("#" + this.id).on('hide.bs.modal', function() {
+ if (modal_on_close[this.id] !== undefined){
+ modal_on_close[this.id]();
+ // reinitialize after call
+ modal_on_close[this.id] = undefined;
+ }
+ });
+ }
+ }
+ );
+}
+
var register_wait_button = function(){
$(".wait-button").click(function(){short_wait()});
};
diff --git a/ishtar_common/templates/blocks/bs_alert_message.html b/ishtar_common/templates/blocks/bs_alert_message.html
new file mode 100644
index 000000000..b0b5241a7
--- /dev/null
+++ b/ishtar_common/templates/blocks/bs_alert_message.html
@@ -0,0 +1,4 @@
+<div class="alert alert-info alert-dismissible fade show" role="alert">
+ <i class="fa fa-info" aria-hidden="true"></i>&nbsp;&nbsp;{{alert_message}}
+ <button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">×</span></button>
+</div>
diff --git a/ishtar_common/templates/ishtar/blocks/window_nav.html b/ishtar_common/templates/ishtar/blocks/window_nav.html
index e3ed13f97..63aafb93a 100644
--- a/ishtar_common/templates/ishtar/blocks/window_nav.html
+++ b/ishtar_common/templates/ishtar/blocks/window_nav.html
@@ -2,6 +2,8 @@
<script type="text/javascript">{% localize off %}
var current_url_{{window_id_underscore}};
var reload_window_{{window_id_underscore}} = function() {
+ /* reload put the sheet at the end of list - force other to be close in order to not loose the current sheet */
+ $(".sheet > .collapse").removeClass("show");
load_window(current_url_{{window_id_underscore}},
'', function(){hide_window("{{window_id}}");},
true);
@@ -126,4 +128,4 @@ $(document).ready(function(){
</script>
{% else %}
<h3 class="external-source"><i class="fa fa-globe" aria-hidden="true"></i> {{item.current_source}}</h3>
-{% endif %} \ No newline at end of file
+{% endif %}
diff --git a/ishtar_common/templates/ishtar/forms/modify_relations.html b/ishtar_common/templates/ishtar/forms/modify_relations.html
index 1dcf02b0c..419d94c9a 100644
--- a/ishtar_common/templates/ishtar/forms/modify_relations.html
+++ b/ishtar_common/templates/ishtar/forms/modify_relations.html
@@ -12,6 +12,10 @@
{{ formset.management_form }}
{% csrf_token %}
<div class="modal-body body-scroll">
+ {% if excess %}
+ {% with alert_message=excess %}
+ {% include "blocks/bs_alert_message.html" %}
+ {% endwith %}{% endif %}
<div class='form'>
{% block main_form %}
<table class='w-100 inline-table text-center'>
@@ -52,7 +56,10 @@
qa_action_register("{{url}}");
$(document).on("click", '.check-all-relations', function(){
$('input[id$="-DELETE"]:checkbox').prop('checked', $(this).is(':checked'));
- });
+ });{% if window_id %}
+ console.log("{{window_id}}");
+ modal_on_close["modal-dynamic-form"] = reload_window_{{window_id}};
+ {% endif %}
{% block js_ready %}
{% endblock %}
});
diff --git a/ishtar_common/templatetags/ishtar_helpers.py b/ishtar_common/templatetags/ishtar_helpers.py
index 8c8227312..329f82dde 100644
--- a/ishtar_common/templatetags/ishtar_helpers.py
+++ b/ishtar_common/templatetags/ishtar_helpers.py
@@ -107,4 +107,4 @@ def can_edit_item(item, context):
@register.filter
def format_date(value):
- return python_format_date(value) \ No newline at end of file
+ return python_format_date(value)
diff --git a/ishtar_common/templatetags/window_header.py b/ishtar_common/templatetags/window_header.py
index c36cbc465..bfcd95883 100644
--- a/ishtar_common/templatetags/window_header.py
+++ b/ishtar_common/templatetags/window_header.py
@@ -18,8 +18,11 @@ def window_nav(context, item, window_id, show_url, modify_url='', histo_url='',
revert_url='', previous=None, nxt=None, pin_action=False):
extra_actions = []
if hasattr(item, 'get_extra_actions'):
- extra_actions = sorted_actions(
- item.get_extra_actions(context['request']))
+ try:
+ extra_actions = item.get_extra_actions(context['request'], context["window_id_underscore"])
+ except (KeyError, TypeError):
+ extra_actions = item.get_extra_actions(context['request'])
+ extra_actions = sorted_actions(extra_actions)
extra_templates = []
if hasattr(item, 'get_extra_templates'):
extra_templates = item.get_extra_templates(context['request'])
diff --git a/ishtar_common/urls_converters.py b/ishtar_common/urls_converters.py
new file mode 100644
index 000000000..9395af648
--- /dev/null
+++ b/ishtar_common/urls_converters.py
@@ -0,0 +1,29 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+# Copyright (C) 2024 Étienne Loks <etienne.loks_AT_iggdrasilDOTnet>
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU Affero General Public License for more details.
+
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# See the file COPYING for details.
+
+
+class UnderscoreSlug:
+ regex = '[_0-9a-z]+'
+
+ def to_python(self, value):
+ return str(value)
+
+ def to_url(self, value):
+ return str(value)
+