diff options
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 |
commit | 435797a54e4d322a46711f303c2fc1fd5286330e (patch) | |
tree | 7c209135a17fe9b12fa2e2e844f345debed45d97 | |
parent | 69df0a88eeaafeee81d76a3307e79fe5cad8ecf8 (diff) | |
download | Ishtar-435797a54e4d322a46711f303c2fc1fd5286330e.tar.bz2 Ishtar-435797a54e4d322a46711f303c2fc1fd5286330e.zip |
✨ site and operation relations forms refactoring
-rw-r--r-- | archaeological_context_records/views.py | 2 | ||||
-rw-r--r-- | archaeological_operations/forms.py | 70 | ||||
-rw-r--r-- | archaeological_operations/models.py | 30 | ||||
-rw-r--r-- | archaeological_operations/urls.py | 34 | ||||
-rw-r--r-- | archaeological_operations/views.py | 144 | ||||
-rw-r--r-- | ishtar_common/static/js/ishtar.js | 21 | ||||
-rw-r--r-- | ishtar_common/templates/blocks/bs_alert_message.html | 4 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/blocks/window_nav.html | 4 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/forms/modify_relations.html | 9 | ||||
-rw-r--r-- | ishtar_common/templatetags/ishtar_helpers.py | 2 | ||||
-rw-r--r-- | ishtar_common/templatetags/window_header.py | 7 | ||||
-rw-r--r-- | ishtar_common/urls_converters.py | 29 |
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> {{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) + |