#!/usr/bin/env python # -*- coding: utf-8 -*- # Copyright (C) 2010-2011 É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 from itertools import groupby from django.core.urlresolvers import reverse from django.core import validators 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, loader from django.db.models import Max from django import forms from django.core.mail import send_mail from django.forms.formsets import formset_factory, BaseFormSet, \ DELETION_FIELD_NAME from django.contrib.auth.models import User from django.contrib.sites.models import Site from formwizard.forms import NamedUrlSessionFormWizard import models import widgets from ishtar import settings reverse_lazy = lazy(reverse, unicode) def clean_duplicated(formset, key_names): """Checks for duplicated.""" if any(formset.errors): return items = [] for i in range(0, formset.total_form_count()): form = formset.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, \ _("There are identical items.") items.append(item) 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 WarehouseForm(forms.Form): name = forms.CharField(label=_(u"Name"), max_length=40, validators=[name_validator]) warehouse_type = forms.ChoiceField(label=_(u"Warehouse type"), choices=models.WarehouseType.get_types()) person_in_charge = forms.IntegerField(label=_(u"Person in charge"), widget=widgets.JQueryAutoComplete( reverse_lazy('autocomplete-person'), associated_model=models.Person), validators=[models.valid_id(models.Person)], required=False) comment = forms.CharField(label=_(u"Comment"), widget=forms.Textarea, required=False) address = forms.CharField(label=_(u"Address"), widget=forms.Textarea, required=False) address_complement = forms.CharField(label=_(u"Address complement"), widget=forms.Textarea, required=False) postal_code = forms.CharField(label=_(u"Postal code"), max_length=10, required=False) town = forms.CharField(label=_(u"Town"), max_length=30, required=False) country = forms.CharField(label=_(u"Country"), max_length=30, required=False) phone = forms.CharField(label=_(u"Phone"), max_length=18, required=False) mobile_phone = forms.CharField(label=_(u"Town"), max_length=18, required=False) def save(self, user): dct = self.cleaned_data dct['history_modifier'] = user dct['warehouse_type'] = models.WarehouseType.objects.get( pk=dct['warehouse_type']) if 'person_in_charge' in dct and dct['person_in_charge']: dct['person_in_charge'] = models.Person.objects.get( pk=dct['person_in_charge']) new_item = models.Warehouse(**dct) new_item.save() return new_item 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 SearchWizard(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 = ['search.html'] return templates 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) last_form = final_form_list[-1] context.update({'datas':self.get_formated_datas(final_form_list)}) if hasattr(last_form, 'confirm_msg'): context.update({'confirm_msg':last_form.confirm_msg}) if hasattr(last_form, 'confirm_end_msg'): context.update({'confirm_end_msg':last_form.confirm_end_msg}) 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 not cleaned_data: continue if form_datas: form_datas.append(("", "", "spacer")) items = hasattr(base_form, 'fields') and \ base_form.fields.keyOrder or cleaned_data.keys() for key in items: lbl = None if key.startswith('hidden_'): continue 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 not value and value != False: continue if type(value) == bool: if value == True: value = _("Yes") elif value == False: value = _("No") elif key in associated_models: item = associated_models[key].objects.get(pk=value) if hasattr(item, 'short_label'): value = item.short_label() else: value = unicode(item) 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, whole_associated_models = {}, [], [] 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'): multi = False if form.forms: frm = form.forms[0] if hasattr(frm, 'base_model') and frm.base_model: whole_associated_models.append(frm.base_model) else: whole_associated_models += associated_models.keys() fields = frm.fields.copy() if 'DELETE' in fields: fields.pop('DELETE') multi = len(fields) > 1 if multi: assert hasattr(frm, 'base_model'), \ u"Must define a base_model for " + unicode(frm.__class__) for frm in form.forms: if not frm.is_valid(): continue vals = {} if "DELETE" in frm.cleaned_data: if frm.cleaned_data["DELETE"]: continue frm.cleaned_data.pop('DELETE') for key in frm.cleaned_data: value = frm.cleaned_data[key] if not value and value != False: continue if key in associated_models: value = associated_models[key].objects.get(pk=value) if multi: vals[key] = value else: m2m.append((key, value)) if multi and vals: m2m.append((frm.base_model, vals)) elif type(form.cleaned_data) == dict: for key in form.cleaned_data: if key.startswith('hidden_'): continue value = form.cleaned_data[key] if key in associated_models: if value: value = associated_models[key].objects.get(pk=value) else: value = None dct[key] = value return self.save_model(dct, m2m, whole_associated_models, request, storage, form_list, return_object) def get_saved_model(self): """ Permit a distinguo when saved model is not the base selected model """ return self.model def get_current_saved_object(self, request, storage): """ Permit a distinguo when saved model is not the base selected model """ return self.get_current_object(request, storage) def save_model(self, dct, m2m, whole_associated_models, request, storage, form_list, return_object): dct = self.get_extra_model(dct, request, storage, form_list) obj = self.get_current_saved_object(request, storage) # manage dependant items other_objs = {} for k in dct.keys(): if '__' not in k: continue vals = k.split('__') assert len(vals) == 2, "Only one level of dependant item is managed" dependant_item, key = vals if dependant_item not in other_objs: other_objs[dependant_item] = {} other_objs[dependant_item][key] = dct.pop(k) if obj: for k in dct: if k.startswith('pk'): continue setattr(obj, k, dct[k]) for dependant_item in other_objs: c_item = getattr(obj, dependant_item) # manage ManyToMany if only one associated if hasattr(c_item, "all"): c_items = c_item.all() if len(c_items) != 1: continue c_item = c_items[0] if c_item: # to check # for k in other_objs[dependant_item]: setattr(c_item, k, other_objs[dependant_item][k]) c_item.save() else: m = getattr(self.model, dependant_item) if hasattr(m, 'related'): c_item = m.related.model(**other_objs[dependant_item]) setattr(obj, dependant_item, c_item) obj.save() obj.save() else: adds = {} for dependant_item in other_objs: m = getattr(self.model, dependant_item) model = m.field.rel.to c_dct = other_objs[dependant_item].copy() if hasattr(model, 'history'): c_dct['history_modifier'] = request.user c_item = model(**c_dct) c_item.save() if hasattr(m, 'through'): adds[dependant_item] = c_item elif hasattr(m, 'field'): dct[dependant_item] = c_item if 'pk' in dct: dct.pop('pk') obj = self.get_saved_model()(**dct) obj.save() for k in adds: getattr(obj, k).add(adds[k]) # necessary to manage interaction between models like # material_index management for baseitems obj.save() m2m_items = {} for model in whole_associated_models: getattr(obj, model+'s').clear() for key, value in m2m: if key not in m2m_items: if type(key) == dict: vals = [] for item in getattr(obj, key+'s').all(): v = {} for k in value.keys(): v[k] = getattr(item, k) vals.append(v) m2m_items[key] = vals else: m2m_items[key] = getattr(obj, key+'s').all() if value not in m2m_items[key]: if type(value) == dict: model = getattr(obj, key+'s').model if issubclass(model, models.BaseHistorizedItem): value['history_modifier'] = request.user value = model.objects.create(**value) value.save() getattr(obj, key+'s').add(value) # necessary to manage interaction between models like # material_index management for baseitems obj.save() res = render_to_response('wizard_done.html', {}, context_instance=RequestContext(request)) return return_object and (obj, res) or res def get_deleted(self, keys): """ Get the deleted and non-deleted items in formsets """ not_to_delete, to_delete = set(), set() for key in keys: items = key.split('-') if len(items) < 2 or items[-2] in to_delete: continue idx = items[-2] try: int(idx) except: continue if items[-1] == u'DELETE': to_delete.add(idx) if idx in not_to_delete: not_to_delete.remove(idx) elif idx not in not_to_delete: not_to_delete.add(idx) return (to_delete, not_to_delete) 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 to_delete, not_to_delete = self.get_deleted(data.keys()) # raz deleted fields for key in data.keys(): items = key.split('-') if len(items) < 2 or items[-2] not in to_delete: continue data.pop(key) if to_delete: # reorganize for idx, number in enumerate(sorted(not_to_delete)): idx = unicode(idx) if idx == number: continue for key in data.keys(): items = key.split('-') if len(items) > 2 and number == items[-2]: items[-2] = unicode(idx) k = u'-'.join(items) data[k] = data.pop(key)[0] # get a form key base_key = form.form.base_fields.keys()[0] init = self.get_form_initial(request, storage, step) total_field = len([key for key in data.keys() if base_key in key.split('-') and data[key]]) if init and not to_delete: total_field = max((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 """ post_data = request.POST.copy() if request.POST.has_key('form_prev_step'): try: step_number = int(request.POST['form_prev_step']) post_data['form_prev_step'] = self.get_form_list(request, storage).keys()[step_number] except ValueError: pass request.POST = post_data return super(Wizard, self).process_post_request(request, storage, *args, **kwargs) @classmethod def session_has_key(cls, request, storage, form_key, key=None, multi=None): """ Check if the session has value of a specific form and (if provided) of a key """ test = storage.prefix in request.session \ and 'step_data' in request.session[storage.prefix] \ and form_key in request.session[storage.prefix]['step_data'] if not key or not test: return test key = key.startswith(form_key) and key or \ not multi and form_key + '-' + key or \ form_key + '-0-' + key #only check if the first field is available return key in request.session[storage.prefix]['step_data'][form_key] @classmethod def session_get_value(cls, request, storage, form_key, key, multi=False): """ Get the value of a specific form """ if not cls.session_has_key(request, storage, form_key, key, multi): return if not multi: key = key.startswith(form_key) and key or form_key + '-' + key return request.session[storage.prefix]['step_data'][form_key][key] vals = [] for k in request.session[storage.prefix]['step_data'][form_key]: if k.startswith(form_key) and k.endswith(key) and \ request.session[storage.prefix]['step_data'][form_key][k]: vals.append(request.session[storage.prefix]['step_data']\ [form_key][k]) return vals 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 try: idx = int(self.session_get_value(request, storage, main_form_key, 'pk')) current_obj = self.model.objects.get(pk=idx) except(TypeError, ValueError, ObjectDoesNotExist): pass return current_obj def get_form_initial(self, request, storage, step): current_obj = self.get_current_object(request, storage) current_step = storage.get_current_step() or self.get_first_step( request, storage) if step.startswith('selec-') and step in self.form_list \ and 'pk' in self.form_list[step].associated_models: model_name = self.form_list[step].associated_models['pk' ].__name__.lower() if step == current_step: self.reset_wizard(request, storage) val = model_name in request.session and request.session[model_name] if val: return {'pk':val} elif current_obj: return self.get_instanced_init(current_obj, request, storage, step) current_form = self.form_list[current_step] if hasattr(current_form, 'currents'): initial = {} for key in current_form.currents: model_name = current_form.currents[key].__name__.lower() val = model_name in request.session and \ request.session[model_name] if val: initial[key] = val if initial: return initial 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] # make the current object the default item for the session obj_name = obj.__class__.__name__.lower() # prefer a specialized name if available prefixes = storage.prefix.split('_') if len(prefixes) > 1 and prefixes[-2].startswith(obj_name): obj_name = prefixes[-2] request.session[obj_name] = unicode(obj.pk) initial = {} if request.POST or (step in request.session[storage.prefix] and\ request.session[storage.prefix]['step_data'][step]): return {} if hasattr(c_form, 'base_fields'): for base_field in c_form.base_fields.keys(): fields = base_field.split('__') value = obj for field in fields: if not hasattr(value, field) or \ getattr(value, field) == None: value = obj break value = getattr(value, field) if value == obj: continue if hasattr(value, 'pk'): value = value.pk if value in (True, False): initial[base_field] = value elif value != None: initial[base_field] = unicode(value) elif hasattr(c_form, 'management_form'): initial = [] key = current_step.split('-')[0] if not hasattr(obj, key): return initial keys = c_form.form.base_fields.keys() for child_obj in getattr(obj, key).order_by('pk').all(): if not keys: break vals = {} 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 if value != None: vals[field] = unicode(value) if vals: initial.append(vals) return initial def get_now(): format = formats.get_format('DATE_INPUT_FORMATS')[0] value = datetime.datetime.now().strftime(format) return value class DeletionWizard(Wizard): def get_formated_datas(self, forms): datas = super(DeletionWizard, self).get_formated_datas(forms) self.current_obj = None for form in forms: if not hasattr(form, "cleaned_data"): continue for key in form.cleaned_data: if key == 'pk': model = form.associated_models['pk'] self.current_obj = model.objects.get(pk=form.cleaned_data['pk']) if not self.current_obj: return datas res = {} for field in self.model._meta.fields + self.model._meta.many_to_many: if field.name not in self.fields: continue value = getattr(self.current_obj, field.name) if not value: continue if hasattr(value, 'all'): value = ", ".join([unicode(item) for item in value.all()]) if not value: continue else: value = unicode(value) res[field.name] = (field.verbose_name, value, '') if not datas and self.fields: datas = [['', []]] for field in self.fields: if field in res: datas[0][1].append(res[field]) return datas def done(self, request, storage, form_list, **kwargs): obj = self.get_current_object(request, storage) obj.delete() return render_to_response('wizard_delete_done.html', {}, context_instance=RequestContext(request)) class ClosingWizard(Wizard): # "close" an item # to be define in the overloaded class model = None fields = [] def get_formated_datas(self, forms): datas = super(ClosingWizard, self).get_formated_datas(forms) self.current_obj = None for form in forms: if not hasattr(form, "cleaned_data"): continue for key in form.cleaned_data: if key == 'pk': model = form.associated_models['pk'] self.current_obj = model.objects.get( pk=form.cleaned_data['pk']) if not self.current_obj: return datas res = {} for field in self.model._meta.fields + self.model._meta.many_to_many: if field.name not in self.fields: continue value = getattr(self.current_obj, field.name) if not value: continue if hasattr(value, 'all'): value = ", ".join([unicode(item) for item in value.all()]) if not value: continue else: value = unicode(value) res[field.name] = (field.verbose_name, value, '') if not datas and self.fields: datas = [['', []]] for field in self.fields: if field in res: datas[0][1].append(res[field]) return datas def done(self, request, storage, form_list, **kwargs): obj = self.get_current_object(request, storage) for form in form_list: if form.is_valid(): if 'end_date' in form.cleaned_data and hasattr(obj, 'end_date'): obj.end_date = form.cleaned_data['end_date'] obj.save() return render_to_response('wizard_closing_done.html', {}, context_instance=RequestContext(request))