#!/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.contrib.contenttypes.models import ContentType 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 import models import widgets from ishtar_common.utils import MultiValueDict # from formwizard.forms import NamedUrlSessionFormWizard class NamedUrlSessionFormWizard(forms.Form): def __init__(self, form_list, condition_list={}, url_name=''): 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(unicode(arg())) else: my_args.append(unicode(arg)) kwargs['args'] = my_args return reverse(*args, **kwargs) reverse_lazy = lazy(my_reverse, unicode) regexp_name = re.compile(r"^[,:/\w\-'\"() \&\[\]@]+$", re.UNICODE) name_validator = validators.RegexValidator( regexp_name, _(u"Enter a valid name consisting of letters, spaces and hyphens."), 'invalid') class FloatField(forms.FloatField): """ Allow the use of comma for separating float fields """ def clean(self, value): if value and (isinstance(value, unicode) or isinstance(value, str)): value = value.replace(',', '.').replace('%', '') return super(FloatField, self).clean(value) class FinalForm(forms.Form): final = True form_label = _(u"Confirm") class FinalDeleteForm(FinalForm): confirm_msg = " " confirm_end_msg = _(u"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': (forms.FloatField, None), 'D': (DateField, None), 'B': (forms.NullBooleanField, None), 'C': (widgets.Select2DynamicField, None), } class CustomForm(object): form_admin_name = "" form_slug = "" need_user_for_initialization = True 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) 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 for idx, field in enumerate(field_items): idx = (idx + 1) * 10 key, c_field = field for k in sorted(new_fields.keys()): if idx - 10 <= k < idx: alt_key, alt_field = new_fields.pop(k) alt_field.order_number = k fields[alt_key] = alt_field c_field.order_number = idx fields[key] = c_field 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 def are_available(self, keys): for k in keys: if k not in self.fields: return False return True 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 = ContentType.objects.get(app_label=app_name, model=model_name) ct_class = ct.model_class() choices = set() splitted_key = key[len('data__'):].split('__') for obj in ct_class.objects.filter( data__has_key=key[len('data__'):]).all(): value = obj.data for k in splitted_key: value = value[k] choices.add(value) choices = [('', '')] + [(v, v) for v in sorted(list(choices))] return choices @classmethod def _get_json_fields(cls, custom_form): fields = [] for field in custom_form.json_fields.order_by('order').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] 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(attrs={"class": "form-control"}) if field_cls == widgets.Select2DynamicField: attrs['choices'] = cls._get_dynamic_choices(key) f = field_cls(**attrs) fields.append((field.order or 1, key, f)) return fields @classmethod def check_custom_form(cls, current_user): if not current_user: return True, [], [] base_q = {"form": cls.form_slug, 'available': True} # order is important : try for user, user type then all query_dicts = [] if current_user: dct = base_q.copy() dct.update({'users__pk': current_user.pk}) query_dicts = [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) if user and 'pk' in self.fields: self.fields['pk'].widget.user = user 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 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(0, 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 [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() 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): self.label = label self.collapse = collapse self.level = level def render(self): if not self.collapse: return mark_safe(u"{label}".format( label=self.label, level=self.level )) html = u"""
""".format(label=self.label, slug=slugify(self.label), level=self.level) return mark_safe(html) def render_end(self): if not self.collapse: return "" return mark_safe(u"""
""") class IshtarForm(forms.Form): TYPES = [] # FieldType list PROFILE_FILTER = {} # profile key associated to field list HEADERS = {} # field key associated to FormHeader instance def __init__(self, *args, **kwargs): super(IshtarForm, self).__init__(*args, **kwargs) if self.PROFILE_FILTER: profile = models.get_current_profile() for profile_key in self.PROFILE_FILTER: if not getattr(profile, profile_key): for field_key in self.PROFILE_FILTER[profile_key]: self.fields.pop(field_key) if getattr(self, 'confirm', False): return for field in self.TYPES: self._init_type(field) for k in self.fields: if not hasattr(self.fields[k].widget, 'NO_FORM_CONTROL'): cls = 'form-control' if 'class' in self.fields[k].widget.attrs: cls = self.fields[k].widget.attrs['class'] + " " + cls self.fields[k].widget.attrs['class'] = cls widget = self.fields[k].widget 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' 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 headers(self, key): if key not in self.HEADERS: return self.current_header = self.HEADERS[key] return self.current_header class TableSelect(IshtarForm): def __init__(self, *args, **kwargs): super(TableSelect, self).__init__(*args, **kwargs) ALT_NAMES = {} if hasattr(self, '_model') and hasattr(self._model, "ALT_NAMES"): ALT_NAMES = self._model.ALT_NAMES 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 if k in ALT_NAMES: self.fields[k].alt_name = ALT_NAMES[k][0] else: self.fields[k].alt_name = k key = self.fields.keys()[0] self.fields[key].widget.attrs['autofocus'] = 'autofocus' def get_input_ids(self): return self.fields.keys() def get_now(): format = formats.get_format('DATE_INPUT_FORMATS')[0] value = datetime.datetime.now().strftime(format) return value class ClosingDateFormSelection(IshtarForm): form_label = _("Closing date") end_date = DateField(label=_(u"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 get_form_selection( class_name, label, key, model, base_form, get_url, not_selected_error=_(u"You should select an item."), new=False, new_message=_(u"Add a new item"), get_full_url=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 """ 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) attrs[key] = forms.IntegerField( label="", required=False, validators=[models.valid_id(model)], widget=widgets.DataTable(reverse_lazy(get_url), base_form, model, **widget_kwargs)) def clean(self): cleaned_data = self.cleaned_data if self._main_key not in cleaned_data \ or not cleaned_data[self._main_key]: raise forms.ValidationError(self._not_selected_error) return cleaned_data attrs['clean'] = clean attrs['SEARCH_AND_SELECT'] = True return type(class_name, (forms.Form,), attrs) def get_data_from_formset(data): """ convert ['formname-wizardname-1-public_domain': [u'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 = [] def __init__(self, *args, **kwargs): self.items = kwargs.pop('items') self.confirm = kwargs.pop('confirm') super(QAForm, self).__init__(*args, **kwargs) for k in 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): self.fields[k].rendered_value = getattr( self, "_get_" + k)(kwargs['data'][k]) elif hasattr(self.fields[k], "choices"): values = [] for v in kwargs['data'].getlist(k): values.append( dict(self.fields[k].choices)[int(v)]) self.fields[k].rendered_value = mark_safe( u" ; ".join(values)) if k not in self.REPLACE_FIELDS: self.fields[k].label = unicode(self.fields[k].label) + \ unicode(u" - append to existing") else: self.fields[k].label = unicode(self.fields[k].label) + \ unicode(u" - replace") def _set_value(self, item, base_key): value = self.cleaned_data[base_key] if not value: return key = base_key[len("qa_"):] field = item._meta.get_field(key) if getattr(field, 'related_model', None): if type(value) == list: value = [field.related_model.objects.get(pk=v) for v in value] else: value = field.related_model.objects.get(pk=value) 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: if getattr(item, key): value = getattr(item, key) + u"\n" + value setattr(item, key, 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.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_object_name': 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)