diff options
author | Étienne Loks <etienne.loks@iggdrasil.net> | 2020-11-27 11:26:55 +0100 |
---|---|---|
committer | Étienne Loks <etienne.loks@iggdrasil.net> | 2021-02-28 12:15:21 +0100 |
commit | 8355b16b1ae2f213de8d9f98221227f587e3b29f (patch) | |
tree | 300713d38252ed0fb9d25e48e8499b29523b7985 | |
parent | eb7e96c07ad61a6e66fa8fd94f80fa317a99afb0 (diff) | |
download | Ishtar-8355b16b1ae2f213de8d9f98221227f587e3b29f.tar.bz2 Ishtar-8355b16b1ae2f213de8d9f98221227f587e3b29f.zip |
Documents: dynamic filter of support and medium by document type - collapse related fields on edition
-rw-r--r-- | ishtar_common/admin.py | 20 | ||||
-rw-r--r-- | ishtar_common/forms.py | 116 | ||||
-rw-r--r-- | ishtar_common/forms_common.py | 70 | ||||
-rw-r--r-- | ishtar_common/lookups.py | 9 | ||||
-rw-r--r-- | ishtar_common/migrations/0208_auto_20201126_1516.py (renamed from ishtar_common/migrations/0208_auto_20201126_1217.py) | 12 | ||||
-rw-r--r-- | ishtar_common/models.py | 9 | ||||
-rw-r--r-- | ishtar_common/static/js/ishtar.js | 19 | ||||
-rw-r--r-- | ishtar_common/templates/blocks/bs_form_snippet.html | 2 | ||||
-rw-r--r-- | ishtar_common/templates/ishtar/forms/document.html | 20 | ||||
-rw-r--r-- | ishtar_common/templatetags/table_form.py | 5 | ||||
-rw-r--r-- | ishtar_common/views.py | 15 |
11 files changed, 271 insertions, 26 deletions
diff --git a/ishtar_common/admin.py b/ishtar_common/admin.py index b5ff67567..26ceb01cd 100644 --- a/ishtar_common/admin.py +++ b/ishtar_common/admin.py @@ -982,8 +982,8 @@ class GeneralTypeAdmin(ImportActionAdmin, ImportJSONActionAdmin): general_models = [models.OrganizationType, models.SourceType, - models.AuthorType, models.TitleType, models.Format, - models.SupportType, models.PersonType, models.LicenseType, + models.AuthorType, models.TitleType, + models.PersonType, models.LicenseType, models.Language] for model in general_models: admin_site.register(model, GeneralTypeAdmin) @@ -1081,6 +1081,22 @@ class CreateDepartmentActionAdmin(GeneralTypeAdmin): {'form': form, 'current_action': 'create_area'} ) +@admin.register(models.SupportType, site=admin_site) +class SupportType(GeneralTypeAdmin): + model = models.SupportType + form = make_ajax_form(model, {'document_types': 'source_type'}) + + +@admin.register(models.Format, site=admin_site) +class Format(GeneralTypeAdmin): + model = models.Format + form = make_ajax_form(model, {'document_types': 'source_type'}) + + +@admin.register(models.DocumentTag, site=admin_site) +class DocumentTag(MergeActionAdmin, GeneralTypeAdmin): + pass + class AreaAdmin(CreateDepartmentActionAdmin): list_display = ('label', 'reference', 'parent', 'available') diff --git a/ishtar_common/forms.py b/ishtar_common/forms.py index 6193c72c4..f1e5b34ca 100644 --- a/ishtar_common/forms.py +++ b/ishtar_common/forms.py @@ -622,6 +622,7 @@ class FormHeader(object): class IshtarForm(forms.Form, BSForm): TYPES = [] # FieldType list CONDITIONAL_FIELDS = [] # dynamic conditions on field display + # can be dynamic with "get_conditional_fields" PROFILE_FILTER = {} # profile key associated to field list HEADERS = {} # field key associated to FormHeader instance # permission check for widget options, ex: forms_common.DocumentForm @@ -668,14 +669,19 @@ class IshtarForm(forms.Form, BSForm): self.fields[field.key].choices = field.get_choices() self.fields[field.key].help_text = field.get_help() + def get_headers(self): + return self.HEADERS + def headers(self, key): - if key not in self.HEADERS: + headers = self.get_headers() + if key not in headers: return - self.current_header = self.HEADERS[key] + self.current_header = headers[key] return self.current_header def extra_render(self): - return self.get_conditional() + return (self.get_conditional() or "") + ( + self.get_conditional_filters() or "") HIDE_JS_TEMPLATE = """ var %(id)s_item_show_list = ['%(item_list)s']; @@ -688,7 +694,6 @@ class IshtarForm(forms.Form, BSForm): var %(id)s_item_show_list = ['%(item_list)s']; var %(id)s_hide_display = function(){ var current_val = $("#id_%(name)s").val(); - console.log("#id_%(name)s"); if (%(id)s_check_list.indexOf(current_val) != -1){ for (idx in %(id)s_item_show_list){ $("#main_div-id_" + %(id)s_item_show_list[idx]).removeClass("d-none"); @@ -707,12 +712,15 @@ class IshtarForm(forms.Form, BSForm): """ def get_conditional(self): - if not self.CONDITIONAL_FIELDS or not self.TYPES: + conditional_fields = self.CONDITIONAL_FIELDS + if hasattr(self, 'get_conditional_fields'): + conditional_fields = self.get_conditional_fields() + if not conditional_fields or not self.TYPES: return type_dict = dict([(typ.key, typ.model) for typ in self.TYPES]) html = "" - for condition, target_names in self.CONDITIONAL_FIELDS: + for condition, target_names in conditional_fields: condition_field, condition_attr, condition_val = condition if condition_field not in type_dict: continue @@ -740,6 +748,102 @@ class IshtarForm(forms.Form, BSForm): html = "<script type='text/javascript'>" + html + "</script>" return html + CONDITIONAL_FILTER_JS_TEMPLATE = """ + %(filter_list)s; + var %(id)s_prefix = "%(prefix)s"; + var %(id)s_filter_display = function(){ + var current_val = $("#id_%(name)s").val(); + if (current_val in %(id)s_filter_list){ + for (var k in %(id)s_filter_list[current_val]){ + var cname = k; + if (%(id)s_prefix) cname = %(id)s_prefix + cname; + update_select_widget( + cname, + %(id)s_all_value_list[k], + %(id)s_filter_list[current_val][k]); + } + } else { + for (var k in %(id)s_exclude_list){ + var cname = k; + if (%(id)s_prefix) cname = %(id)s_prefix + cname; + update_select_widget( + cname, + %(id)s_all_value_list[k], + null, + %(id)s_exclude_list[k]); + } + } + }; + + $("#id_%(name)s").change(%(id)s_filter_display); + setTimeout(function(){ + %(id)s_filter_display(); + }, 500); + """ + + def get_conditional_filters(self): + if not hasattr(self, 'get_conditional_filter_fields'): + return + conditional_fields, excluded_fields, all_values = \ + self.get_conditional_filter_fields() + + types = [typ.key for typ in self.TYPES] + html = "" + + outputs = set() + for input_key in conditional_fields: + if input_key not in types: + continue + name = input_key + if self.prefix: + name = self.prefix + "-" + input_key + cidx = name.replace("-", "_") + filter_list = "var %s_filter_list = {\n" % cidx + for idx, input_pk in enumerate(conditional_fields[input_key]): + if idx: + filter_list += ",\n" + filter_list += ' "%s": {\n' % input_pk + for idx2, output in enumerate( + conditional_fields[input_key][input_pk]): + if idx2: + filter_list += ",\n" + if output[0] in excluded_fields: + outputs.add(output[0]) + filter_list += ' "{}": [{}]'.format(*output) + filter_list += " }" + filter_list += "};\n" + + html += self.CONDITIONAL_FILTER_JS_TEMPLATE % { + "id": cidx, + "name": name, + "filter_list": filter_list, + "prefix": self.prefix or "" + } + html += "var %s_other_widget_list = [" % cidx + for idx, k in enumerate(all_values): + if idx: + html += ", " + html += '"' + k + '"' + html += "];\n" + + html += "var %s_exclude_list = {\n" % cidx + for idx, output in enumerate(outputs): + if idx: + html += ",\n" + html += ' "%s": [%s]' % (output, excluded_fields[output]) + html += "\n};\n" + + html += "var %s_all_value_list = {\n" % cidx + for idx, k in enumerate(all_values): + if idx: + html += ",\n" + html += ' "%s": %s' % (k, all_values[k]) + html += "\n};\n" + + if html: + html = "<script type='text/javascript'>" + html + "</script>" + return html + class TableSelect(IshtarForm): def __init__(self, *args, **kwargs): diff --git a/ishtar_common/forms_common.py b/ishtar_common/forms_common.py index c76577e65..c191a2e6e 100644 --- a/ishtar_common/forms_common.py +++ b/ishtar_common/forms_common.py @@ -1398,6 +1398,7 @@ class DocumentForm(forms.ModelForm, CustomForm, ManageOldType): ] HEADERS = { + 'finds': FormHeader(_("Related items")), 'title': FormHeader(_("Identification")), 'format_type': FormHeader(_("Format")), 'image': FormHeader(_("Content")), @@ -1407,7 +1408,6 @@ class DocumentForm(forms.ModelForm, CustomForm, ManageOldType): 'source': FormHeader(_("Source"), collapse=True), 'container_id': FormHeader(_("Warehouse"), collapse=True), 'comment': FormHeader(_("Advanced"), collapse=True), - 'finds': FormHeader(_("Related items")), } OPTIONS_PERMISSIONS = [ # field name, permission, options @@ -1427,6 +1427,7 @@ class DocumentForm(forms.ModelForm, CustomForm, ManageOldType): self.user = None if kwargs.get("user", None): self.user = kwargs.pop("user") + self.is_instancied = bool(kwargs.get('instance', False)) super(DocumentForm, self).__init__(*args, **kwargs) fields = OrderedDict() for related_key in models.Document.RELATED_MODELS_ALT: @@ -1447,6 +1448,73 @@ class DocumentForm(forms.ModelForm, CustomForm, ManageOldType): fields[k] = self.fields[k] self.fields = fields + def get_headers(self): + headers = self.HEADERS.copy() + if self.is_instancied: + headers["finds"] = FormHeader(headers["finds"].label, collapse=True) + return headers + + def get_conditional_filter_fields(self): + """ + Helper to get values for filtering select widget on select by another + widget + :return: conditional_fields, excluded_fields, all_values + * conditional_fields: + {input_key: { + input_pk: [(output_key1, "pk1,pk2,..."), + (output_key2, "pk1,pk2,...")], + input_pk2: ... + }, + input_key2: {...} + } + * excluded_fields: {output_key: "pk1,pk2,...", ...} # list pk for all + output_key in condition_fields + * all_values: {output_key: [(pk1, label1), (pk2, label2), ...]} + """ + conditional_fields = {} + excluded_fields = {} + key = 'source_type' + for doc_type in models.SourceType.objects.filter( + available=True, formats__pk__isnull=False).all(): + if key not in conditional_fields: + conditional_fields[key] = {} + sub_key = doc_type.pk + if sub_key in conditional_fields[key]: + continue + lst = [str(f.pk) for f in models.Format.objects.filter( + available=True, document_types__pk=doc_type.pk)] + if 'format_type' not in excluded_fields: + excluded_fields['format_type'] = [] + for k in lst: + if k not in excluded_fields['format_type']: + excluded_fields['format_type'].append(k) + conditional_fields[key][sub_key] = [] + conditional_fields[key][sub_key].append( + ('format_type', ",".join(lst))) + for doc_type in models.SourceType.objects.filter( + available=True, supports__pk__isnull=False).all(): + if key not in conditional_fields: + conditional_fields[key] = {} + lst = [str(f.pk) for f in models.SupportType.objects.filter( + available=True, document_types__pk=doc_type.pk)] + if 'support_type' not in excluded_fields: + excluded_fields['support_type'] = [] + for k in lst: + if k not in excluded_fields['support_type']: + excluded_fields['support_type'].append(k) + sub_key = doc_type.pk + if sub_key not in conditional_fields[key]: + conditional_fields[key][sub_key] = [] + conditional_fields[key][sub_key].append( + ('support_type', ",".join(lst))) + for k in excluded_fields: + excluded_fields[k] = ",".join(excluded_fields[k]) + all_values = { + "format_type": [list(tp) for tp in models.Format.get_types()], + "support_type": [list(tp) for tp in models.SupportType.get_types()] + } + return conditional_fields, excluded_fields, all_values + def clean(self): cleaned_data = self.cleaned_data if not cleaned_data.get('title', None) and \ diff --git a/ishtar_common/lookups.py b/ishtar_common/lookups.py index 3481ba1a9..66b43f3e4 100644 --- a/ishtar_common/lookups.py +++ b/ishtar_common/lookups.py @@ -156,3 +156,12 @@ class DocumentLookup(LookupChannel): internal_reference__icontains=q ) return self.model.objects.filter(query).order_by('title')[:20] + + +@register('source_type') +class SourceTypeLookup(LookupChannel): + model = models.SourceType + + def get_query(self, q, request): + query = Q(label__icontains=q) + return self.model.objects.filter(query).order_by('label')[:20] diff --git a/ishtar_common/migrations/0208_auto_20201126_1217.py b/ishtar_common/migrations/0208_auto_20201126_1516.py index 53b883075..0bd51e4c0 100644 --- a/ishtar_common/migrations/0208_auto_20201126_1217.py +++ b/ishtar_common/migrations/0208_auto_20201126_1516.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -# Generated by Django 1.11.27 on 2020-11-26 12:17 +# Generated by Django 1.11.27 on 2020-11-26 15:16 from __future__ import unicode_literals from django.db import migrations, models @@ -12,6 +12,16 @@ class Migration(migrations.Migration): ] operations = [ + migrations.AddField( + model_name='format', + name='document_types', + field=models.ManyToManyField(blank=True, help_text='Only available for theses document types', related_name='formats', to='ishtar_common.SourceType'), + ), + migrations.AddField( + model_name='supporttype', + name='document_types', + field=models.ManyToManyField(blank=True, help_text='Only available for theses document types', related_name='supports', to='ishtar_common.SourceType'), + ), migrations.AlterField( model_name='document', name='isbn', diff --git a/ishtar_common/models.py b/ishtar_common/models.py index e251fee89..ffe302454 100644 --- a/ishtar_common/models.py +++ b/ishtar_common/models.py @@ -2636,6 +2636,11 @@ post_delete.connect(post_save_cache, sender=SourceType) class SupportType(GeneralType): + document_types = models.ManyToManyField( + "SourceType", blank=True, related_name='supports', + help_text=_("Only available for theses document types") + ) + class Meta: verbose_name = _("Support type") verbose_name_plural = _("Support types") @@ -2651,6 +2656,10 @@ class Format(GeneralType): help_text=_("Template to insert an iframe for this format. Use django " "template with a {{document}} variable matching the " "current document.")) + document_types = models.ManyToManyField( + "SourceType", blank=True, related_name='formats', + help_text=_("Only available for theses document types") + ) class Meta: verbose_name = _("Format type") diff --git a/ishtar_common/static/js/ishtar.js b/ishtar_common/static/js/ishtar.js index fcaa73053..6528d3797 100644 --- a/ishtar_common/static/js/ishtar.js +++ b/ishtar_common/static/js/ishtar.js @@ -1856,3 +1856,22 @@ var is_valid_issn = function(str) { } return (check == str[str.length-1].toUpperCase()); } + +var update_select_widget = function(input_name, values, only_values, excluded_values){ + cvalue = $("#id_" + input_name).val(); + var options = ""; + for (var idx in values){ + var option = values[idx]; + if (!(option[0] && option[0] != cvalue + && ((excluded_values && + excluded_values.indexOf(option[0]) != -1) || + (only_values && only_values.indexOf(option[0]) == -1) + ))){ + var selected = ""; + if (option[0] == cvalue) selected = " selected"; + options += "<option value='" + option[0] + "'" + selected + ">"; + options += option[1] + "</option>"; + } + } + $("#id_" + input_name).html(options); +};
\ No newline at end of file diff --git a/ishtar_common/templates/blocks/bs_form_snippet.html b/ishtar_common/templates/blocks/bs_form_snippet.html index e24220005..3d84ce3dc 100644 --- a/ishtar_common/templates/blocks/bs_form_snippet.html +++ b/ishtar_common/templates/blocks/bs_form_snippet.html @@ -1,5 +1,5 @@ {% load i18n from_dict %} -{% if form.non_field_errors %} +{% if form.non_field_errors and not no_error %} <div class="alert alert-danger" role="alert"> {{form.non_field_errors}} </div> diff --git a/ishtar_common/templates/ishtar/forms/document.html b/ishtar_common/templates/ishtar/forms/document.html index ed5a39daf..8e2683a03 100644 --- a/ishtar_common/templates/ishtar/forms/document.html +++ b/ishtar_common/templates/ishtar/forms/document.html @@ -13,18 +13,18 @@ <div class='form{% if not form.SEARCH_AND_SELECT %} container{% endif %}'> {% if form.non_field_errors or form.errors %} <div class="alert alert-danger"> - <div><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> - {% trans "Error on validation. Check all your fields. Modification not saved." %} + <div><i class="fa fa-exclamation-triangle" aria-hidden="true"></i> + {% trans "Error on validation. Check all your fields. Modification not saved." %} + </div> + {% for key, error_details in form.errors.items %} + {% with field=key|from_dict:form.fields %}<strong>{{ field.label }}</strong>{{error_details}}{% endwith %} + {% endfor %} </div> - {% for error in form.non_field_errors %} - <p>{{ error }}</p> - {% endfor %} - {% for key, error_details in form.errors.items %} - {% with field=key|from_dict:form.fields %}<strong>{{ field.label }}</strong>{{error_details}}{% endwith %} - {% endfor %} -</div> {% endif %} - {% bs_form form %} + {% if item_related_label %}<div class="alert alert-info"> + <strong>{% trans "Related items" %}{% trans ":" %}</strong> {{ item_related_label }} + </div>{% endif %} + {% bs_form form 0 True %} </div> {% endblock %} diff --git a/ishtar_common/templatetags/table_form.py b/ishtar_common/templatetags/table_form.py index 5da2171d0..d5b20da26 100644 --- a/ishtar_common/templatetags/table_form.py +++ b/ishtar_common/templatetags/table_form.py @@ -8,11 +8,12 @@ register = Library() @register.inclusion_tag('blocks/bs_form_snippet.html', takes_context=True) -def bs_form(context, form, position=0): +def bs_form(context, form, position=0, no_error=False): user = context['user'] show_field_number = user.ishtaruser and user.ishtaruser.show_field_number() return {'form': form, 'odd': position % 2, - 'show_field_number': show_field_number} + 'show_field_number': show_field_number, + 'no_error': no_error} @register.inclusion_tag('blocks/bs_compact_form_snippet.html', diff --git a/ishtar_common/views.py b/ishtar_common/views.py index 704894d52..ceb8db200 100644 --- a/ishtar_common/views.py +++ b/ishtar_common/views.py @@ -1923,7 +1923,7 @@ display_document = display_item(models.Document) document_search_wizard = wizards.DocumentSearch.as_view( [('selec-document_search', forms.DocumentFormSelection)], - label=_(u"Document: search"), + label=_("Document: search"), url_name='search-document', ) @@ -2016,12 +2016,21 @@ class DocumentEditView(DocumentFormMixin, UpdateView): for related_item in getattr(document, k).all(): key = "{}_{}_main_image".format(k, related_item.pk) kwargs["main_items_fields"][k].append( - (key, u"{} - {}".format( - _(u"Main image for"), related_item))) + (key, "{} - {}".format( + _("Main image for"), related_item))) if related_item.main_image == document: initial[key] = True kwargs['initial'] = initial kwargs["user"] = self.request.user + self.document = document + return kwargs + + def get_context_data(self, **kwargs): + kwargs = super(DocumentEditView, self).get_context_data(**kwargs) + rel = self.document.cache_related_label + if len(rel) == 1000: # truncated + rel += " (...)" + kwargs["item_related_label"] = rel return kwargs |