#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2010 Étienne Loks # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU 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 General Public License for more details. # You should have received a copy of the GNU General Public License # along with this program. If not, see . # See the file COPYING for details. """ Forms definition """ import datetime from django.core.urlresolvers import reverse from django.core.validators import MaxLengthValidator from django.core.exceptions import ObjectDoesNotExist from django.utils import formats from django.utils.functional import lazy from django.utils.translation import ugettext_lazy as _ from django.shortcuts import render_to_response from django.template import Context, RequestContext from django.db.models import Max from django import forms from django.forms.formsets import formset_factory, BaseFormSet, \ DELETION_FIELD_NAME from formwizard.forms import NamedUrlSessionFormWizard import models import widgets from ishtar import settings reverse_lazy = lazy(reverse, unicode) class FinalForm(forms.Form): final = True form_label = _("Confirm") class FormSet(BaseFormSet): def add_fields(self, form, index): super(FormSet, self).add_fields(form, index) form.fields[DELETION_FIELD_NAME].label = '' form.fields[DELETION_FIELD_NAME].widget = widgets.DeleteWidget() class Wizard(NamedUrlSessionFormWizard): model = None def get_wizard_name(self): """ As the class name can interfere when reused, use the url_name """ return self.url_name def get_template(self, request, storage): templates = ['default_wizard.html'] current_step = storage.get_current_step() or self.get_first_step( request, storage) if current_step == self.get_last_step(request, storage): templates = ['confirm_wizard.html'] + templates return templates def get_template_context(self, request, storage, form=None): """ Add previous and current steps to manage the wizard path """ context = super(Wizard, self).get_template_context(request, storage, form) step = self.get_first_step(request, storage) current_step = storage.get_current_step() or self.get_first_step( request, storage) context.update({'current_step':self.form_list[current_step]}) if step == current_step: return context previous_steps = [] while step: if step == current_step: break previous_steps.append(self.form_list[step]) step = self.get_next_step(request, storage, step) context.update({'previous_steps':previous_steps}) # not last step: validation if step != self.get_last_step(request, storage): return context final_form_list = [] for form_key in self.get_form_list(request, storage).keys(): form_obj = self.get_form(request, storage, step=form_key, data=storage.get_step_data(form_key), files=storage.get_step_files(form_key)) form_obj.is_valid() final_form_list.append(form_obj) context.update({'datas':self.get_formated_datas(final_form_list)}) return context def get_formated_datas(self, forms): """ Get the data to present in the last page """ datas = [] for form in forms: form_datas = [] base_form = hasattr(form, 'forms') and form.forms[0] or form associated_models = hasattr(base_form, 'associated_models') and \ base_form.associated_models or {} if not hasattr(form, 'cleaned_data') and hasattr(form, 'forms'): cleaned_datas = [frm.cleaned_data for frm in form.forms if frm.is_valid()] if not cleaned_datas: continue elif not hasattr(form, 'cleaned_data'): continue else: cleaned_datas = type(form.cleaned_data) == list and \ form.cleaned_data \ or [form.cleaned_data] for cleaned_data in cleaned_datas: if cleaned_data and form_datas: form_datas.append(("", "", "spacer")) for key in cleaned_data: lbl = None if hasattr(base_form, 'fields') and key in base_form.fields: lbl = base_form.fields[key].label if not lbl: continue value = cleaned_data[key] if key in associated_models: value = unicode(associated_models[key].objects.get( pk=value)) if not value: continue form_datas.append((lbl, value, '')) if form_datas: datas.append((form.form_label, form_datas)) return datas def get_extra_model(self, dct, request, storage, form_list): dct['history_modifier'] = request.user return dct def done(self, request, storage, form_list, return_object=False, **kwargs): """ Save to the model """ dct, m2m = {}, [] for form in form_list: if not form.is_valid(): return self.render(request, storage, form) base_form = hasattr(form, 'forms') and form.forms[0] or form associated_models = hasattr(base_form, 'associated_models') and \ base_form.associated_models or {} if hasattr(form, 'forms'): for frm in form.forms: if not frm.is_valid(): continue for key in frm.cleaned_data: if key not in associated_models: # datas not managed continue value = frm.cleaned_data[key] value = associated_models[key].objects.get(pk=value) m2m.append((key, value)) elif type(form.cleaned_data) == dict: for key in form.cleaned_data: value = form.cleaned_data[key] if key in associated_models: value = associated_models[key].objects.get(pk=value) dct[key] = value dct = self.get_extra_model(dct, request, storage, form_list) obj = self.get_current_object(request, storage) if obj: for k in dct: if k == 'pk': continue setattr(obj, k, dct[k]) else: obj = self.model(**dct) obj.save() for key, value in m2m: if value not in getattr(obj, key+'s').all(): getattr(obj, key+'s').add(value) obj.save() res = render_to_response('wizard_done.html', {}, context_instance=RequestContext(request)) return return_object and (obj, res) or res def get_form(self, request, storage, step=None, data=None, files=None): """ Manage formset """ if data: data = data.copy() if not step: step = self.determine_step(request, storage) form = self.get_form_list(request, storage)[step] if hasattr(form, 'management_form'): # manage deletion not_to_delete, to_delete = [], [] for key in data.keys(): items = key.split('-') if len(items) == 3: if items[1] not in to_delete and \ items[1] not in not_to_delete: del_key = u"%s-%s-DELETE" % (items[0], items[1]) if del_key in data and data[del_key]: to_delete.append(items[1]) else: not_to_delete.append(items[1]) if items[1] in to_delete: data.pop(key) if to_delete: # reorganize for idx, number in enumerate(sorted(not_to_delete)): if unicode(idx) == number: continue for key in data.keys(): items = key.split('-') if len(items) == 3 and items[1] == number: ck = '-'.join([items[0], unicode(idx), items[2]]) data[ck] = data.pop(key)[0] # get a form key base_key = form.form.base_fields.keys()[0] init = self.get_form_initial(request, storage, step) if not init: total_field = len([key for key in data.keys() if base_key in key.split('-') and data[key]]) else: total_field = len(init) data[step + u'-INITIAL_FORMS'] = unicode(total_field) data[step + u'-TOTAL_FORMS'] = unicode(total_field + 1) data = data or None form = super(Wizard, self).get_form(request, storage, step, data, files) return form def render_next_step(self, request, storage, form, **kwargs): """ Manage the modify or delete button in formset: next_step = current_step """ if request.POST.has_key('formset_modify') \ and request.POST['formset_modify'] \ or [key for key in request.POST.keys() if key.endswith('DELETE') and request.POST[key]]: return self.render(request, storage, form, **kwargs) return super(Wizard, self).render_next_step(request, storage, form, **kwargs) def process_post_request(self, request, storage, *args, **kwargs): """ Convert numerical step number to step name """ if request.POST.has_key('form_prev_step'): try: step_number = int(request.POST['form_prev_step']) post_data = request.POST.copy() post_data['form_prev_step'] = self.get_form_list(request, storage).keys()[step_number] request.POST = post_data except ValueError: pass return super(Wizard, self).process_post_request(request, storage, *args, **kwargs) def get_current_object(self, request, storage): """ Get the current object for an instancied wizard """ current_obj = None main_form_key = 'selec-' + self.url_name pk = main_form_key + '-pk' if storage.prefix in request.session \ and 'step_data' in request.session[storage.prefix] \ and main_form_key in request.session[storage.prefix]['step_data'] \ and pk in request.session[storage.prefix]['step_data']\ [main_form_key]: try: idx = int(request.session[storage.prefix]['step_data'] [main_form_key][pk]) current_obj = self.model.objects.get(pk=idx) except(TypeError, ObjectDoesNotExist): pass return current_obj def get_form_initial(self, request, storage, step): current_obj = self.get_current_object(request, storage) if current_obj: return self.get_instanced_init(current_obj, request, storage, step) return super(Wizard, self).get_form_initial(request, storage, step) def get_instanced_init(self, obj, request, storage, step): """ Get initial data from an init """ current_step = storage.get_current_step() or self.get_first_step( request, storage) c_form = self.form_list[current_step] initial = {} if hasattr(c_form, 'base_fields'): for field in c_form.base_fields.keys(): if hasattr(obj, field): value = getattr(obj, field) if hasattr(value, 'pk'): value = value.pk initial[field] = unicode(value) elif hasattr(c_form, 'management_form'): initial = [] key = current_step.split('-')[0] if not hasattr(obj, key): return initial for child_obj in getattr(obj, key).all(): vals = {} keys = c_form.form.base_fields.keys() if len(keys) == 1: # only one field: must be the id of the object vals[keys[0]] = unicode(child_obj.pk) else: for field in keys: if hasattr(child_obj, field): value = getattr(child_obj, field) if hasattr(value, 'pk'): value = value.pk vals[field] = unicode(value) if vals: initial.append(vals) return initial class FileWizard(Wizard): model = models.File def get_form(self, request, storage, step=None, data=None, files=None): """ Manage formset """ if data: data = data.copy() else: data = {} # manage the dynamic choice of towns if not step: step = self.determine_step(request, storage) form = self.get_form_list(request, storage)[step] town_form_key = 'towns-' + self.url_name if step.startswith('parcels-') and hasattr(form, 'management_form') \ and storage.prefix in request.session \ and 'step_data' in request.session[storage.prefix] \ and town_form_key in request.session[storage.prefix]['step_data']: towns = [] qdict = request.session[storage.prefix]['step_data'][town_form_key] for k in qdict.keys(): if k.endswith("town") and qdict[k]: try: town = models.Town.objects.get(pk=int(qdict[k])) towns.append((town.pk, unicode(town))) except (ObjectDoesNotExist, ValueError): pass data['TOWNS'] = sorted(towns, key=lambda x:x[1]) form = super(FileWizard, self).get_form(request, storage, step, data, files) return form def get_extra_model(self, dct, request, storage, form_list): dct = super(FileWizard, self).get_extra_model(dct, request, storage, form_list) models.File.objects.filter(year=dct['year']) current_ref = models.File.objects.filter(year=dct['year'] ).aggregate(Max('numeric_reference'))["numeric_reference__max"] dct['numeric_reference'] = current_ref and current_ref + 1 or 1 return dct def done(self, request, storage, form_list, **kwargs): ''' Save parcels ''' r = super(FileWizard, self).done(request, storage, form_list, return_object=True, **kwargs) if type(r) not in (list, tuple) or len(r) != 2: return r obj, res = r for form in form_list: if not hasattr(form, 'prefix') \ or not form.prefix.startswith('parcels-') \ or not hasattr(form, 'forms'): continue for frm in form.forms: if not frm.is_valid(): continue dct = frm.cleaned_data.copy() try: dct['town'] = models.Town.objects.get(pk=int(dct['town'])) except (ValueError, ObjectDoesNotExist): continue dct['associated_file'] = obj dct['operation'] = None if 'DELETE' in dct: dct.pop('DELETE') parcel = models.Parcel.objects.filter(**dct).count() if not parcel: dct['history_modifier'] = request.user parcel = models.Parcel(**dct) parcel.save() return res def get_now(): format = formats.get_format('DATE_INPUT_FORMATS')[0] value = datetime.datetime.now().strftime(format) return value class FileFormSelection(forms.Form): form_label = _("Archaelogical file") associated_models = {'pk':models.File} pk = forms.IntegerField(label=_("Archaelogical file"), widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-file'), associated_model=models.File), validators=[models.valid_id(models.File)]) class FileFormGeneral(forms.Form): form_label = _("General") associated_models = {'in_charge':models.Person, 'file_type':models.FileType} in_charge = forms.IntegerField(label=_("Person in charge"), widget=widgets.JQueryAutoComplete(reverse_lazy('autocomplete-person'), associated_model=models.Person), validators=[models.valid_id(models.Person)]) year = forms.IntegerField(label=_("Year"), initial=lambda:datetime.datetime.now().year) numeric_reference = forms.IntegerField(label=_("Numeric reference"), widget=forms.HiddenInput, required=False) internal_reference = forms.CharField(label=_(u"Internal reference"), max_length=60, validators=[models.is_unique(models.File, 'internal_reference')]) creation_date = forms.DateField(label=_(u"Creation date"), initial=get_now, widget=widgets.JQueryDate) file_type = forms.ChoiceField(label=_("File type"), choices=models.FileType.get_types()) comment = forms.CharField(label=_(u"Comment"), widget=forms.Textarea, required=False) class FileFormGeneralRO(FileFormGeneral): year = forms.IntegerField(label=_("Year"), widget=forms.TextInput(attrs={'readonly':True})) numeric_reference = forms.IntegerField(label=_("Numeric reference"), widget=forms.TextInput(attrs={'readonly':True})) internal_reference = forms.CharField(label=_(u"Internal reference"), widget=forms.TextInput(attrs={'readonly':True})) class FileFormAddress(forms.Form): form_label = _("Address") associated_models = {'town':models.Town} total_surface = forms.IntegerField(label=_("Total surface")) address = forms.CharField(label=_(u"Main address"), widget=forms.Textarea) address_complement = forms.CharField(label=_(u"Main address - complement")) postal_code = forms.CharField(label=_(u"Main address - postal code"), max_length=10) class TownForm(forms.Form): form_label = _("Towns") associated_models = {'town':models.Town} # !FIXME hard_link, reverse_lazy doen't seem to work with formsets town = forms.IntegerField(label=_(u"Town"), widget=widgets.JQueryAutoComplete("/" + settings.URL_PATH + \ 'autocomplete-town', associated_model=models.Town), validators=[models.valid_id(models.Town)]) class TownFormSet(FormSet): def clean(self): """Checks that no towns are duplicated.""" if any(self.errors): return towns = [] for i in range(0, self.total_form_count()): form = self.forms[i] if 'town' not in form.cleaned_data: continue town = form.cleaned_data['town'] if town in towns: raise forms.ValidationError, _("There are identical towns.") towns.append(town) TownFormSet = formset_factory(TownForm, can_delete=True, formset=TownFormSet) TownFormSet.form_label = _("Towns") class ParcelForm(forms.Form): form_label = _("Parcels") associated_models = {'parcel':models.Parcel, 'town':models.Town} town = forms.ChoiceField(label=_("Town"), choices=(), validators=[models.valid_id(models.Town)]) section = forms.CharField(label=_(u"Section"), validators=[MaxLengthValidator(4)]) parcel_number = forms.CharField(label=_(u"Parcel number"), validators=[MaxLengthValidator(6)]) year = forms.IntegerField(label=_("Year"), initial=lambda:datetime.datetime.now().year) def __init__(self, *args, **kwargs): towns = None if 'data' in kwargs and 'TOWNS' in kwargs['data']: towns = kwargs['data']['TOWNS'] # clean data if not "real" data prefix_value = kwargs['prefix'] + '-town' if not [k for k in kwargs['data'].keys() if k.startswith(prefix_value) and kwargs['data'][k]]: kwargs['data'] = None if 'files' in kwargs: kwargs.pop('files') super(ParcelForm, self).__init__(*args, **kwargs) if towns: self.fields['town'].choices = [('', '--')] + towns class ParcelFormSet(FormSet): def clean(self): """Checks that no parcels are duplicated.""" if any(self.errors): return parcels = [] for i in range(0, self.total_form_count()): form = self.forms[i] if not hasattr(form, 'cleaned_data')\ or 'town' not in form.cleaned_data \ or 'section' not in form.cleaned_data \ or 'parcel_number' not in form.cleaned_data: continue parcel = (form.cleaned_data['town'], form.cleaned_data['section'], form.cleaned_data['parcel_number']) if parcel in parcels: raise forms.ValidationError, _("There are identical parcels.") parcels.append(parcel) ParcelFormSet = formset_factory(ParcelForm, can_delete=True, formset=ParcelFormSet) ParcelFormSet.form_label = _("Parcels") class FileFormPreventive(forms.Form): form_label = _("Preventive informations") associated_models = {'general_contractor':models.Organization, 'saisine_type':models.SaisineType} general_contractor = forms.IntegerField(label=_(u"General contractor"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-organization'), associated_model=models.Organization), validators=[models.valid_id(models.Organization)]) total_developed_surface = forms.IntegerField( label=_("Total developed surface")) if settings.COUNTRY == 'fr': saisine_type = forms.ChoiceField(label=_("Saisine type"), choices=models.SaisineType.get_types()) reception_date = forms.DateField(label=_(u"Reception date"), initial=get_now, widget=widgets.JQueryDate) def is_preventive(form_name, file_type_key='file_type'): def func(self, request, storage): if storage.prefix not in request.session or \ 'step_data' not in request.session[storage.prefix] or \ form_name not in request.session[storage.prefix]['step_data'] or\ form_name + '-' + file_type_key not in \ request.session[storage.prefix]['step_data'][form_name]: return False try: file_type = int(request.session[storage.prefix]['step_data']\ [form_name][form_name+'-'+file_type_key]) return models.FileType.is_preventive(file_type) except ValueError: return False return func file_creation_wizard = FileWizard([ ('general-file_creation', FileFormGeneral), ('address-file_creation', FileFormAddress), ('towns-file_creation', TownFormSet), ('parcels-file_creation', ParcelFormSet), ('preventive-file_creation', FileFormPreventive), ('final-file_creation', FinalForm)], condition_list={ 'preventive-file_creation':is_preventive('general-file_creation') }, url_name='file_creation',) file_modification_wizard = FileWizard([ ('selec-file_modification', FileFormSelection), ('general-file_modification', FileFormGeneralRO), ('adress-file_modification', FileFormAddress), ('towns-file_modification', TownFormSet), ('parcels-file_modification', ParcelFormSet), ('preventive-file_modification', FileFormPreventive), ('final-file_modification', FinalForm)], condition_list={ 'preventive-file_modification':is_preventive('general-file_modif') }, url_name='file_modification',)