#!/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 """ import datetime import re import types from django import forms 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.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 class CustomForm(object): form_admin_name = "" form_slug = "" need_user_for_initialization = True def __init__(self, *args, **kwargs): current_user = None if 'user' in kwargs: try: current_user = kwargs.pop('user').ishtaruser except AttributeError: pass super(CustomForm, self).__init__(*args, **kwargs) available, excluded = self.check_availability_and_excluded_fields( 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) 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 check_availability_and_excluded_fields(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) excluded_lst = [] 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] if not form.enabled: return False, [] for excluded in form.excluded_fields.all(): # could have be filtered previously excluded_lst.append(excluded.field) break return True, excluded_lst @classmethod def get_custom_fields(cls): if hasattr(cls, 'base_fields'): fields = cls.base_fields else: # formset fields = cls.form.base_fields customs = [] for key in fields: field = fields[key] # cannot customize display of required and hidden field # field with no label are also rejected if field.required or field.widget.is_hidden or not field.label: continue customs.append((key, field.label)) return sorted(customs, key=lambda x: x[1]) class FormSet(CustomForm, BaseFormSet): 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 = widgets.DeleteWidget() 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): self.label = label self.level = level def render(self): return mark_safe(u"{label}".format( label=self.label, level=self.level )) 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) 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() class TableSelect(IshtarForm): def __init__(self, *args, **kwargs): super(TableSelect, self).__init__(*args, **kwargs) # no field is required for search for k in self.fields: self.fields[k].required = False cls = 'form-control' if k == 'search_vector': cls += " search-vector" self.fields[k].widget.attrs['class'] = cls 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 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)