#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2010-2017 Étienne Loks # 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 . # See the file COPYING for details. """ Forms definition """ from collections import OrderedDict import datetime import re import types from django import forms from django.apps import apps from django.conf import settings from django.contrib.auth.models import User from django.core.exceptions import ValidationError from django.core.urlresolvers import reverse from django.core import validators from django.forms.formsets import BaseFormSet, DELETION_FIELD_NAME from django.utils import formats, translation from django.utils.functional import lazy from django.utils.safestring import mark_safe from django.utils.text import slugify from django.utils.translation import ugettext_lazy as _ from bootstrap_datepicker.widgets import DatePicker, DATE_FORMAT, DateField from ishtar_common import models from ishtar_common import widgets from ishtar_common.utils import MultiValueDict # from formwizard.forms import NamedUrlSessionFormWizard class NamedUrlSessionFormWizard(forms.Form): def __init__(self, form_list, condition_list=None, url_name=""): if not condition_list: condition_list = {} self.form_list = dict(form_list) self.condition_list = condition_list self.url_name = url_name super(NamedUrlSessionFormWizard, self).__init__(self) def rindex(self, idx): return self.url_name.rindex(idx) def my_reverse(*args, **kwargs): """ Custom reverse method in order to evaluate lazy args """ if "args" in kwargs: my_args = [] for arg in kwargs["args"]: if callable(arg): my_args.append(str(arg())) else: my_args.append(str(arg)) kwargs["args"] = my_args return reverse(*args, **kwargs) reverse_lazy = lazy(my_reverse, str) regexp_name = re.compile(r"^[\.,:/\w\-'\"() \&\[\]@]+$", re.UNICODE) name_validator = validators.RegexValidator( regexp_name, _("Enter a valid name consisting of letters, spaces and hyphens."), "invalid", ) def file_size_validator(value): limit = (settings.MAX_UPLOAD_SIZE * 1024 * 1024) - 100 if value.size > limit: raise ValidationError( str(_("File too large. Size should not exceed {} Mo.")).format( settings.MAX_UPLOAD_SIZE ) ) class FloatField(forms.FloatField): """ Allow the use of comma for separating float fields """ def clean(self, value): if value and isinstance(value, str): value = value.replace(",", ".").replace("%", "") return super(FloatField, self).clean(value) class FinalForm(forms.Form): final = True form_label = _("Confirm") class FinalDeleteForm(FinalForm): confirm_msg = " " confirm_end_msg = _("Are you sure you want to delete?") def get_readonly_clean(key): def func(self): instance = getattr(self, "instance", None) if instance and getattr(instance, key): return getattr(instance, key) else: return self.cleaned_data[key] return func JSON_VALUE_TYPES_FIELDS = { "T": (forms.CharField, None), "LT": (forms.CharField, forms.Textarea), "I": (forms.IntegerField, None), "F": (FloatField, None), "D": (DateField, None), "B": (forms.NullBooleanField, None), "C": (widgets.Select2DynamicField, None), } class BSForm(object): def _post_init(self): for k in self.fields: widget = self.fields[k].widget # manage bs decoration if not hasattr(widget, "NO_FORM_CONTROL"): cls = "form-control" if "class" in widget.attrs: if "form-control" in widget.attrs["class"]: cls = widget.attrs["class"] else: cls = widget.attrs["class"] + " " + cls widget.attrs["class"] = cls # 32 bits max value if isinstance(self.fields[k], forms.IntegerField): has_max = any( isinstance(validator, validators.MaxValueValidator) for validator in self.fields[k].validators ) if not has_max: self.fields[k].validators.append( validators.MaxValueValidator(2147483647) ) # manage datepicker if not isinstance(widget, DatePicker): continue lang = translation.get_language() widget.options["language"] = lang if lang in DATE_FORMAT: widget.options["format"] = DATE_FORMAT[lang] if "autoclose" not in widget.options: widget.options["autoclose"] = "true" widget.options["todayHighlight"] = "true" class CustomForm(BSForm): form_admin_name = "" form_slug = "" need_user_for_initialization = True _explicit_ordering = False # explicit call to field ordering def __init__(self, *args, **kwargs): self.current_user = None if "user" in kwargs: try: self.current_user = kwargs.pop("user").ishtaruser except AttributeError: pass super(CustomForm, self).__init__(*args, **kwargs) if not self._explicit_ordering: self.custom_form_ordering() def custom_form_ordering(self): available, excluded, json_fields = self.check_custom_form(self.current_user) for exc in excluded: if hasattr(self, "fields"): self.remove_field(exc) else: # formset for form in self.forms: if exc in form.fields: form.fields.pop(exc) new_fields = {} for order, key, field in json_fields: while order in new_fields: # json fields with the same number order += 1 new_fields[order] = (key, field) if not hasattr(self, "fields"): # formset return field_items, field_hidden_items = [], [] for key, field in self.fields.items(): if getattr(field.widget, "is_hidden", None): field_hidden_items.append((key, field)) else: field_items.append((key, field)) if not new_fields: # add index number for admin debug for idx, field in enumerate(field_items): idx = (idx + 1) * 10 key, c_field = field c_field.order_number = idx return # re-order for json fields fields = OrderedDict() for idx, field in enumerate(field_hidden_items): key, c_field = field fields[key] = c_field old_idx = 0 for idx, field in enumerate(field_items): if not idx and isinstance(self, TableSelect): # search_vector field always first on search idx = -1 else: idx = (idx + 1) * 10 key, c_field = field # insert custom field in position for k in sorted(new_fields.keys()): if old_idx <= k < idx: alt_key, alt_field = new_fields.pop(k) alt_field.order_number = k fields[alt_key] = alt_field old_idx = idx c_field.order_number = idx fields[key] = c_field # custom field after classic fields for k in sorted(new_fields.keys()): alt_key, alt_field = new_fields.pop(k) alt_field.order_number = k fields[alt_key] = alt_field self.fields = fields self._post_init() def are_available(self, keys): return all(k in self.fields for k in keys) def remove_field(self, key): if key in self.fields: self.fields.pop(key) @classmethod def _get_dynamic_choices(cls, key): """ Get choice from existing values :param key: data key :return: tuple of choices (id, value) """ app_name = cls.__module__.split(".")[0] if app_name == "archaeological_files_pdl": app_name = "archaeological_files" model_name = cls.form_slug.split("-")[0].replace("_", "") ct_class = apps.get_model(app_name, model_name) return ct_class._get_dynamic_choices(key) @classmethod def _get_json_fields(cls, custom_form): """ Return json field list from database configuration :param custom_form: form concerned :return: ((order1, key1, field1), ...) """ fields = [] is_search = "search_vector" in cls.base_fields q = custom_form.json_fields.values( "label", "help_text", "order", "json_field__key", "json_field__value_type", "json_field__name", ).order_by("order") for field in q.all(): key = "data__" + field["json_field__key"] field_cls, widget = forms.CharField, None if field["json_field__value_type"] in JSON_VALUE_TYPES_FIELDS: field_cls, widget = JSON_VALUE_TYPES_FIELDS[ field["json_field__value_type"] ] if is_search and field["json_field__value_type"] == "LT": widget = None attrs = { "label": field["label"] or field["json_field__name"], "required": False, } if field["help_text"]: attrs["help_text"] = field["help_text"] if widget: attrs["widget"] = widget() if field_cls == widgets.Select2DynamicField: attrs["choices"] = cls._get_dynamic_choices(key) f = field_cls(**attrs) kls = "form-control" if "class" in f.widget.attrs: kls = f.widget.attrs["class"] + " " + kls f.widget.attrs["class"] = kls f.alt_name = slugify(attrs["label"]) fields.append((field["order"] or 1, key, f)) return fields @classmethod def check_custom_form(cls, current_user): """ Check form customization :param current_user: :return: available, excluded_fields, json_fields """ if not current_user: return True, [], [] base_q = {"form": cls.form_slug, "available": True} # order is important : try for user, profile type, user type then all query_dicts = [] if current_user: dct = base_q.copy() dct.update({"users__pk": current_user.pk}) query_dicts = [dct] if current_user.current_profile: dct = base_q.copy() pt = current_user.current_profile.profile_type.pk dct.update({"profile_types__pk": pt}) query_dicts.append(dct) for user_type in current_user.person.person_types.all(): dct = base_q.copy() dct.update({"user_types__pk": user_type.pk}), query_dicts.append(dct) dct = base_q.copy() dct.update({"apply_to_all": True}) query_dicts.append(dct) form = None for query_dict in query_dicts: q = models.CustomForm.objects.filter(**query_dict) if not q.count(): continue # todo: prevent multiple result in database form = q.all()[0] break if not form: return True, [], [] if not form.enabled: return False, [], [] excluded_lst = [] for excluded in form.excluded_fields.all(): # could have be filtered previously excluded_lst.append(excluded.field) json_fields = cls._get_json_fields(form) return True, excluded_lst, json_fields @classmethod def get_custom_fields(cls): """ Get fields than can be customized: excluded, re-ordered (WIP) or re-labeled (WIP) """ if hasattr(cls, "base_fields"): fields = cls.base_fields else: # formset fields = cls.form.base_fields customs = [] keys = fields.keys() for key in keys: field = fields[key] # cannot customize display of required (except in search form) and # hidden field, search_vector and field with no label if ( ("search_vector" not in keys and field.required) or key == "search_vector" or field.widget.is_hidden or not field.label ): continue customs.append((key, field.label)) return sorted(customs, key=lambda x: x[1]) class CustomFormSearch(forms.Form): need_user_for_initialization = True def __init__(self, *args, **kwargs): user = None if "user" in kwargs: user = kwargs.pop("user") super(CustomFormSearch, self).__init__(*args, **kwargs) self.request_user = user if user and "pk" in self.fields: self.fields["pk"].widget.user = user class LockForm(object): need_user_for_initialization = True associated_models = {} def clean(self): cleaned_data = self.cleaned_data pk_key = None if hasattr(self, "pk_key"): pk_key = self.pk_key elif len(self.associated_models.keys()) == 1: pk_key = list(self.associated_models.keys())[0] if not pk_key: raise NotImplementedError("pk_key must be set") if pk_key not in cleaned_data or not cleaned_data[pk_key]: raise forms.ValidationError(_("You should select an item.")) model = self.associated_models[pk_key] pks = self.cleaned_data[pk_key] pks = [pks] if isinstance(pks, int) else pks.split(",") for pk in pks: try: item = model.objects.get(pk=pk) except model.DoesNotExist: raise forms.ValidationError(_("Invalid selection.")) if item.is_locked(self.request_user): raise forms.ValidationError(_("This item is locked " "for edition.")) return self.cleaned_data class MultiSearchForm(CustomFormSearch): SEARCH_AND_SELECT = True pk_key = "pks" associated_models = {} def __init__(self, *args, **kwargs): super(MultiSearchForm, self).__init__(*args, **kwargs) if "pk" not in self.fields: raise NotImplementedError('A "pk" field must be defined') if self.pk_key not in self.associated_models: raise NotImplementedError( '"{}" must be defined in ' "associated_models".format(self.pk_key) ) self.fields["pk"].required = True self.fields[self.pk_key] = self.fields.pop("pk") @classmethod def get_current_model(cls): return cls.associated_models[cls.pk_key] @classmethod def get_formated_datas(cls, cleaned_datas): if hasattr(cls, "current_model"): current_model = cls.current_model else: current_model = cls.get_current_model() if not current_model or not cls.pk_key: return [] items = [] for data in cleaned_datas: if not data or cls.pk_key not in data or not data[cls.pk_key]: continue pks = data[cls.pk_key] for pk in str(pks).split(","): if not pk: continue try: items.append(str(current_model.objects.get(pk=int(pk)))) except (current_model.DoesNotExist, ValueError): continue return [ ( "", mark_safe( "" ), ) ] class FormSet(CustomForm, BaseFormSet): delete_widget = widgets.DeleteWidget def __init__(self, *args, **kwargs): self.readonly = False if "readonly" in kwargs: self.readonly = kwargs.pop("readonly") self.can_delete = False # no extra fields if "data" in kwargs: prefix = "" if "prefix" in kwargs: prefix = kwargs["prefix"] if prefix + "-INITIAL_FORMS" in kwargs["data"]: kwargs["data"][prefix + "-TOTAL_FORMS"] = kwargs["data"][ prefix + "-INITIAL_FORMS" ] super(FormSet, self).__init__(*args, **kwargs) def check_duplicate(self, key_names, error_msg="", check_null=False): """Check for duplicate items in the formset""" if any(self.errors): return if not error_msg: error_msg = _("There are identical items.") items = [] for i in range(self.total_form_count()): form = self.forms[i] if not form.is_valid(): continue item = [ key_name in form.cleaned_data and form.cleaned_data[key_name] for key_name in key_names ] if not check_null and not [v for v in item if v]: continue if item in items: raise forms.ValidationError(error_msg) items.append(item) def add_fields(self, form, index): super(FormSet, self).add_fields(form, index) if self.readonly: for k in form.fields: # django 1.9: use disabled form.fields[k].widget.attrs["readonly"] = True clean = get_readonly_clean(k) clean.__name__ = "clean_" + k clean.__doc__ = "autogenerated: clean_" + k setattr(form, clean.__name__, types.MethodType(clean, form)) if self.can_delete: form.fields[DELETION_FIELD_NAME].label = "" form.fields[DELETION_FIELD_NAME].widget = self.delete_widget() def _should_delete_form(self, form): """ Returns whether or not the form was marked for deletion. If no data, set deletion to True """ if form.cleaned_data.get(DELETION_FIELD_NAME, False): return True if not form.cleaned_data or not [ key for key in form.cleaned_data if key != DELETION_FIELD_NAME and form.cleaned_data[key] is not None and form.cleaned_data[key] != "" ]: form.cleaned_data[DELETION_FIELD_NAME] = True return True return False class FormSetWithDeleteSwitches(FormSet): delete_widget = widgets.DeleteSwitchWidget class FieldType(object): def __init__(self, key, model, is_multiple=False, extra_args=None): self.key = key self.model = model self.is_multiple = is_multiple self.extra_args = extra_args def get_choices(self, initial=None): args = {"empty_first": not self.is_multiple, "initial": initial} if self.extra_args: args.update(self.extra_args) return self.model.get_types(**args) def get_help(self): args = {} if self.extra_args: args.update(self.extra_args) return self.model.get_help(**args) class FormHeader(object): def __init__(self, label, level=4, collapse=False, help_message=""): self.label = label self.collapse = collapse self.level = level self.help_message = help_message def render(self): help_message = "" if self.help_message: help_message = """ """.format( self.help_message ) if not self.collapse: return mark_safe( "{label}{help_message}".format( label=self.label, level=self.level, help_message=help_message ) ) html = """
{help_message} """.format( label=self.label, slug=slugify(self.label), level=self.level, help_message=help_message, ) return mark_safe(html) def render_end(self): if not self.collapse: return "" return mark_safe( """
""" ) 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 OPTIONS_PERMISSIONS = {} SITE_KEYS = {} # archaeological sites fields and associated translation key # to manage translation def __init__(self, *args, **kwargs): super(IshtarForm, self).__init__(*args, **kwargs) profile = None if self.PROFILE_FILTER or self.SITE_KEYS: profile = models.get_current_profile() if self.PROFILE_FILTER: for profile_key in self.PROFILE_FILTER: if not getattr(profile, profile_key): for field_key in self.PROFILE_FILTER[profile_key]: if field_key in self.fields.keys(): self.fields.pop(field_key) if getattr(self, "confirm", False): return for field in self.TYPES: self._init_type(field) if self.SITE_KEYS: field_keys = list(self.fields.keys()) for site_key in list(self.SITE_KEYS.keys()): if site_key in field_keys: self.fields[site_key].label = profile.get_site_label( self.SITE_KEYS[site_key] ) user = getattr(self, "user", None) ishtar_user = None if user: try: ishtar_user = models.IshtarUser.objects.get(pk=user.pk) except models.IshtarUser.DoesNotExist: pass if ishtar_user: for field_name, permissions, options in self.OPTIONS_PERMISSIONS: if field_name not in self.fields: continue if not any( True for permission in permissions if ishtar_user.has_perm(permission) ): continue for option, value in options.items(): setattr(self.fields[field_name].widget, option, value) self._post_init() def _init_type(self, field): if field.key not in self.fields: return 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): headers = self.get_headers() if key not in headers: return self.current_header = headers[key] return self.current_header def extra_render(self): return (self.get_conditional() or "") + (self.get_conditional_filters() or "") HIDE_JS_TEMPLATE = """ var %(id)s_item_show_list = ['%(item_list)s']; for (idx in %(id)s_item_show_list){ $("#main_div-id_" + %(id)s_item_show_list[idx]).addClass("d-none"); } """ CONDITIONAL_JS_TEMPLATE = """ var %(id)s_check_list = ['%(check_id_list)s']; var %(id)s_item_show_list = ['%(item_list)s']; var %(id)s_hide_display = function(){ var current_val = $("#id_%(name)s").val(); 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"); } } else { for (idx in %(id)s_item_show_list){ $("#main_div-id_" + %(id)s_item_show_list[idx]).addClass("d-none"); } } }; $("#id_%(name)s").change(%(id)s_hide_display); setTimeout(function(){ %(id)s_hide_display(); }, 500); """ def get_conditional(self): 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 conditional_fields: condition_field, condition_attr, condition_val = condition if condition_field not in type_dict: continue model = type_dict[condition_field] condition_ids = [ str(item.pk) for item in model.objects.filter( **{condition_attr: condition_val} ).all() ] name = self.prefix + "-" + condition_field target_names = [self.prefix + "-" + name for name in target_names] if not condition_ids: html += self.HIDE_JS_TEMPLATE % { "item_list": "','".join(target_names), "id": name.replace("-", "_"), } continue html += self.CONDITIONAL_JS_TEMPLATE % { "id": name.replace("-", "_"), "name": name, "item_list": "','".join(target_names), "check_id_list": "','".join(condition_ids), } if html: html = "" 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 = "" return html class TableSelect(IshtarForm): def __init__(self, *args, **kwargs): super(TableSelect, self).__init__(*args, **kwargs) alt_names = {} if hasattr(self, "_model"): if hasattr(self._model, "get_alt_names"): alt_names = self._model.get_alt_names() for k_dyn in self._model.DYNAMIC_REQUESTS: dyn = self._model.DYNAMIC_REQUESTS[k_dyn] fields = dyn.get_form_fields() for k in fields: self.fields[k] = fields[k] for k in self.fields: self.fields[k].required = False # no field is required for search cls = "form-control" if k == "search_vector": cls += " search-vector" self.fields[k].widget.attrs["class"] = cls self.fields[k].alt_name = alt_names[k].search_key if k in alt_names else k key = list(self.fields.keys())[0] self.fields[key].widget.attrs["autofocus"] = "autofocus" def get_input_ids(self): return list(self.fields.keys()) class HistorySelect(CustomForm, TableSelect): history_creator = forms.IntegerField( label=_("Created by"), widget=widgets.JQueryAutoComplete( reverse_lazy("autocomplete-user"), associated_model=User ), required=False, ) history_modifier = forms.IntegerField( label=_("Last modified by"), widget=widgets.JQueryAutoComplete( reverse_lazy("autocomplete-user"), associated_model=User ), required=False, ) modified_before = forms.DateField( label=_("Modified before"), widget=DatePicker, required=False ) modified_after = forms.DateField( label=_("Modified after"), widget=DatePicker, required=False ) _explicit_ordering = True CURRENT_FIELDS = [ "history_creator", "history_modifier", "modified_before", "modified_after", ] def __init__(self, *args, **kwargs): super(HistorySelect, self).__init__(*args, **kwargs) field_order = self.fields.keys() fields = OrderedDict() for k in field_order: if k in self.CURRENT_FIELDS: continue fields[k] = self.fields[k] for k in self.CURRENT_FIELDS: fields[k] = self.fields[k] self.fields = fields self.custom_form_ordering() class DocumentItemSelect(HistorySelect): documents__image__isnull = forms.NullBooleanField(label=_("Has an image?")) documents__associated_file__isnull = forms.NullBooleanField( label=_("Has an attached file?") ) documents__associated_url__isnull = forms.NullBooleanField( label=_("Has a web address?") ) CURRENT_FIELDS = [ "documents__image__isnull", "documents__associated_file__isnull", "documents__associated_url__isnull", "history_creator", "history_modifier", "modified_before", "modified_after", ] _explicit_ordering = True def get_now(): format = formats.get_format("DATE_INPUT_FORMATS")[0] return datetime.datetime.now().strftime(format) class ClosingDateFormSelection(IshtarForm): form_label = _("Closing date") end_date = DateField(label=_("Closing date")) def __init__(self, *args, **kwargs): if "initial" not in kwargs: kwargs["initial"] = {} if not kwargs["initial"].get("end_date", None): kwargs["initial"]["end_date"] = datetime.date.today() super(ClosingDateFormSelection, self).__init__(*args, **kwargs) def has_map(): return models.get_current_profile().mapping def get_form_selection( class_name, label, key, model, base_form, get_url, not_selected_error=_("You should select an item."), new=False, new_message=_("Add a new item"), get_full_url=None, gallery=False, map=False, multi=False, base_form_select=None, alt_pk_field=None, ): """ Generate a class selection form class_name -- name of the class label -- label of the form key -- model, base_form -- base form to select get_url -- url to get the item not_selected_error -- message displayed when no item is selected new -- can add new items new_message -- message of link to add new items gallery -- display a gallery map -- display a map """ attrs = { "_main_key": key, "_not_selected_error": not_selected_error, "form_label": label, "associated_models": {key: model}, "currents": {key: model}, } widget_kwargs = {"new": new, "new_message": new_message} if get_full_url: widget_kwargs["source_full"] = reverse_lazy(get_full_url) if gallery: widget_kwargs["gallery"] = True if map: widget_kwargs["map"] = models.profile_mapping() if multi: widget_kwargs["multiple_select"] = True field = forms.CharField valid = models.valid_ids else: field = forms.IntegerField valid = models.valid_id if alt_pk_field: key = alt_pk_field attrs[key] = field( label="", required=False, validators=[valid(model)], widget=widgets.DataTable( reverse_lazy(get_url), base_form, model, **widget_kwargs ), ) attrs["SEARCH_AND_SELECT"] = True if not base_form_select: base_form_select = forms.Form if not isinstance(base_form_select, (tuple, list)): base_form_select = (base_form_select,) return type(class_name, base_form_select, attrs) def get_data_from_formset(data): """ convert ['formname-wizardname-1-public_domain': ['on'], ...] to [{'public_domain': 'off'}, {'public_domain': 'on'}] """ values = [] for k in data: if not data[k]: continue keys = k.split("-") if len(keys) < 3: continue try: idx = int(keys[-2]) except ValueError: continue while len(values) < (idx + 1): values.append({}) field_name = keys[-1] values[idx][field_name] = data[k] return values class ManageOldType(IshtarForm): def __init__(self, *args, **kwargs): """ init_data is used to manage deactivated items in list when editing old data """ prefix = kwargs.get("prefix") or "" self.init_data = {} if "data" in kwargs and kwargs["data"]: for k in kwargs["data"]: if prefix not in k: continue new_k = k[len(prefix) + 1 :] if hasattr(kwargs["data"], "getlist"): items = kwargs["data"].getlist(k) else: items = [kwargs["data"][k]] for val in items: if not val: continue if new_k not in self.init_data: self.init_data[new_k] = [] self.init_data[new_k].append(val) if "initial" in kwargs and kwargs["initial"]: for k in kwargs["initial"]: if k not in self.init_data or not self.init_data[k]: if hasattr(kwargs["initial"], "getlist"): items = kwargs["initial"].getlist(k) else: items = [kwargs["initial"][k]] for val in items: if not val: continue if k not in self.init_data: self.init_data[k] = [] self.init_data[k].append(val) self.init_data = MultiValueDict(self.init_data) super(ManageOldType, self).__init__(*args, **kwargs) for field in self.TYPES: self._init_type(field) def _init_type(self, field): if field.key not in self.fields: return self.fields[field.key].choices = field.get_choices( initial=self.init_data.get(field.key) ) self.fields[field.key].help_text = field.get_help() class QAForm(CustomForm, ManageOldType): MULTI = False SINGLE_FIELDS = [] REPLACE_FIELDS = [] PREFIX = "qa_" NULL_BOOL_CHOICES = ( ("", "--"), ("1", _("Unknown")), ("2", _("Yes")), ("3", _("No")), ) def __init__(self, *args, **kwargs): self.items = kwargs.pop("items") self.confirm = kwargs.pop("confirm") super(QAForm, self).__init__(*args, **kwargs) for k in list(self.fields.keys()): if self.MULTI and k in self.SINGLE_FIELDS: self.fields.pop(k) continue if self.confirm: if "data" not in kwargs or not kwargs["data"].get(k, None): self.fields.pop(k) continue if getattr(self.fields[k].widget, "allow_multiple_selected", None): self.fields[k].widget = forms.MultipleHiddenInput() else: self.fields[k].widget = forms.HiddenInput() if k in kwargs["data"] and kwargs["data"][k]: if hasattr(self, "_get_" + k): value = getattr(self, "_get_" + k)(kwargs["data"][k]) if value is None: self.fields.pop(k) continue self.fields[k].rendered_value = value elif hasattr(self.fields[k], "choices"): values = [] for v in kwargs["data"].getlist(k): dct_choices = {} for key, value in self.fields[k].choices: if isinstance(value, (list, tuple)): dct_choices.update(value) else: dct_choices[key] = value if v in list(dct_choices.keys()): values.append(str(dct_choices[v])) elif int(v) in list(dct_choices.keys()): values.append(str(dct_choices[int(v)])) self.fields[k].rendered_value = mark_safe(" ; ".join(values)) if k not in self.REPLACE_FIELDS: self.fields[k].label = str(self.fields[k].label) + str( _(" - append to existing") ) else: self.fields[k].label = str(self.fields[k].label) + str(_(" - replace")) def _set_value(self, item, base_key): value = self.cleaned_data[base_key] if not value: return key = base_key[len(self.PREFIX) :] field = item._meta.get_field(key) if getattr(field, "related_model", None): is_list = isinstance(value, (list, tuple)) if not is_list: value = [value] new_value = [] for v in value: if not isinstance(v, field.related_model): v = field.related_model.objects.get(pk=v) new_value.append(v) value = new_value if is_list else new_value[0] if getattr(field, "many_to_many", None): if type(value) not in (list, tuple): value = [value] for v in value: getattr(item, key).add(v) else: if base_key not in self.REPLACE_FIELDS and getattr(item, key): value = getattr(item, key) + "\n" + value setattr(item, key, value) def _get_null_boolean_field(self, value): if value == "1": return _("Set to null") elif value == "2": return _("Yes") elif value == "3": return _("No") return def _set_null_boolean_field(self, item, key): value = self.cleaned_data.get(key, None) if value == "1": value = None elif value == "2": value = True elif value == "3": value = False else: return setattr(item, key[3:], value) def save(self, items, user): for item in items: for base_key in self.cleaned_data: if hasattr(self, "_set_" + base_key): getattr(self, "_set_" + base_key)(item, user) else: self._set_value(item, base_key) item.history_modifier = user item._cached_label_checked = False item.save() class DocumentGenerationForm(forms.Form): """ Form to generate document by choosing the template """ _associated_model = None # ex: AdministrativeAct # ex: 'archaeological_operations.models.AdministrativeAct' _associated_object_name = "" document_template = forms.ChoiceField(label=_("Template"), choices=[]) def __init__(self, *args, **kwargs): super(DocumentGenerationForm, self).__init__(*args, **kwargs) self.fields["document_template"].choices = models.DocumentTemplate.get_tuples( dct={"associated_model__klass": self._associated_object_name} ) def save(self, object_pk): try: c_object = self._associated_model.objects.get(pk=object_pk) except self._associated_model.DoesNotExist: return try: template = models.DocumentTemplate.objects.get( pk=self.cleaned_data.get("document_template") ) except models.DocumentTemplate.DoesNotExist: return return template.publish(c_object)