#!/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.translation import ugettext_lazy as _ from django.template import Context, RequestContext from django.shortcuts import render_to_response from django.forms.formsets import formset_factory, BaseFormSet from django.db.models import Max from django import forms from formwizard.forms import NamedUrlSessionFormWizard import models import widgets from ishtar import settings from django.utils.functional import lazy reverse_lazy = lazy(reverse, unicode) class FinalForm(forms.Form): final = True form_label = _("Confirm") class FormSet(BaseFormSet): pass class Wizard(NamedUrlSessionFormWizard): model = None def get_template(self, request, storage): templates = ['default_wizard.html'] current_step = storage.get_current_step() or '0' 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 '0' 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: 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'): continue cleaned_datas = type(form.cleaned_data) == list and \ form.cleaned_data \ or [form.cleaned_data] for cleaned_data in cleaned_datas: for key in cleaned_data: lbl = key if hasattr(base_form, 'fields') and key in base_form.fields: lbl = base_form.fields[key].label value = cleaned_data[key] if key in associated_models: value = unicode(associated_models[key].objects.get( pk=value)) datas.append((lbl, value)) 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: if key not in associated_models: # datas not managed continue value = frm[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.model(**dct) obj.save() for key, value in m2m: getattr(obj, key+'s').add(value) 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] total_field = len([key for key in data.keys() if base_key in key.split('-') and data[key]]) 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 button in formset: next_step = current_step """ if request.POST.has_key('formset_modify') and \ request.POST['formset_modify']: return self.render(request, storage, form, **kwargs) return super(Wizard, self).render_next_step(request, storage, form, **kwargs) 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] if step == '3' and hasattr(form, 'management_form') \ and storage.prefix in request.session \ and 'step_data' in request.session[storage.prefix] \ and '2' in request.session[storage.prefix]['step_data']: towns = [] qdict = request.session[storage.prefix]['step_data']['2'] 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 form.prefix != '3' \ 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') dct['history_modifier'] = request.user parcel = models.Parcel(**dct) parcel.save() return res class FileForm1(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.Person.valid_id]) year = forms.IntegerField(label=_("Year"), initial=lambda:datetime.datetime.now().year) internal_reference = forms.CharField(label=_(u"Internal reference"), max_length=60) creation_date = forms.DateField(label=_(u"Creation date"), initial=datetime.datetime.now) file_type = forms.ChoiceField(label=_("File type"), choices=models.FileType.get_types()) comment = forms.CharField(label=_(u"Comment"), widget=forms.Textarea, required=False) class FileForm2(forms.Form): form_label = _("Address") total_surface = forms.IntegerField(label=_("Total surface")) address = forms.CharField(label=_(u"Address"), widget=forms.Textarea) 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.Town.valid_id]) 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 = forms.ChoiceField(label=_("Town"), choices=(), validators=[models.Town.valid_id]) 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 FileForm4(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.Organization.valid_id]) 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")) 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([FileForm1, FileForm2, TownFormSet, ParcelFormSet, FileForm4, FinalForm], url_name='file_creation', condition_list={'4':is_preventive('0')})