diff options
author | Étienne Loks <etienne.loks@peacefrogs.net> | 2011-06-24 14:37:16 +0200 |
---|---|---|
committer | Étienne Loks <etienne.loks@peacefrogs.net> | 2011-06-24 14:37:16 +0200 |
commit | 05c6d94c9547377c9979e9d860c6618ee898ef6e (patch) | |
tree | 6b4fc14f42da9d91ab2bb4b989ffeeb42947392f /ishtar/ishtar_base/forms.py | |
parent | 2d008477cb66ec3e356fd9153afba7affede249c (diff) | |
download | Ishtar-05c6d94c9547377c9979e9d860c6618ee898ef6e.tar.bz2 Ishtar-05c6d94c9547377c9979e9d860c6618ee898ef6e.zip |
Sources creation for Operation (refs #497) - restructuration (refs #57)
Diffstat (limited to 'ishtar/ishtar_base/forms.py')
-rw-r--r-- | ishtar/ishtar_base/forms.py | 736 |
1 files changed, 736 insertions, 0 deletions
diff --git a/ishtar/ishtar_base/forms.py b/ishtar/ishtar_base/forms.py new file mode 100644 index 000000000..29d9ab4d6 --- /dev/null +++ b/ishtar/ishtar_base/forms.py @@ -0,0 +1,736 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# Copyright (C) 2010-2011 Étienne Loks <etienne.loks_AT_peacefrogsDOTnet> + +# 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 <http://www.gnu.org/licenses/>. + +# 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) + +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: + value = value.replace(',', '.').replace('%', '') + return super(FloatField, self).clean(value) + +class FinalForm(forms.Form): + final = True + form_label = _(u"Confirm") + +class FormSet(BaseFormSet): + 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) + 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)) + +def get_form_selection(class_name, label, key, model, base_form, get_url, + not_selected_error=_(u"You should select an item.")): + """ + 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 + """ + attrs = {'_main_key':key, + '_not_selected_error':not_selected_error, + 'form_label':label, + 'associated_models':{key:model}, + 'currents':{key:model},} + attrs[key] = forms.IntegerField(label="", required=False, + widget=widgets.JQueryJqGrid(reverse_lazy(get_url), + base_form(), model), validators=[models.valid_id(model)]) + 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 + return type(class_name, (forms.Form,), attrs) |