source: main/branches/3D/openPLM/plmapp/forms.py @ 662

Revision 662, 24.2 KB checked in by pcosquer, 8 years ago (diff)

3D branch: merge changes from trunk (rev [661])

Line 
1############################################################################
2# openPLM - open source PLM
3# Copyright 2010 Philippe Joulaud, Pierre Cosquer
4#
5# This file is part of openPLM.
6#
7#    openPLM is free software: you can redistribute it and/or modify
8#    it under the terms of the GNU General Public License as published by
9#    the Free Software Foundation, either version 3 of the License, or
10#    (at your option) any later version.
11#
12#    openPLM is distributed in the hope that it will be useful,
13#    but WITHOUT ANY WARRANTY; without even the implied warranty of
14#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15#    GNU General Public License for more details.
16#
17#    You should have received a copy of the GNU General Public License
18#    along with Foobar.  If not, see <http://www.gnu.org/licenses/>.
19#
20# Contact :
21#    Philippe Joulaud : ninoo.fr@gmail.com
22#    Pierre Cosquer : pierre.cosquer@insa-rennes.fr
23################################################################################
24
25import re
26
27from django import forms
28from django.conf import settings
29from django.forms.formsets import formset_factory, BaseFormSet
30from django.forms.models import modelform_factory, modelformset_factory
31from django.contrib.auth.models import User, Group
32from django.forms import ValidationError
33from django.utils.translation import ugettext_lazy as _
34from django.contrib.sites.models import Site
35from django.utils.functional import memoize
36
37import openPLM.plmapp.models as m
38from openPLM.plmapp.units import UNITS, DEFAULT_UNIT
39from openPLM.plmapp.controllers import rx_bad_ref, DocumentController
40from openPLM.plmapp.controllers.user import UserController
41from openPLM.plmapp.controllers.group import GroupController
42from openPLM.plmapp.widgets import JQueryAutoComplete
43from openPLM.plmapp.encoding import ENCODINGS
44
45class PLMObjectForm(forms.Form):
46    u"""
47    A formulaire that identifies a :class:`PLMObject`.
48    """
49
50    type = forms.CharField()
51    reference = forms.CharField()
52    revision = forms.CharField()
53
54
55def _clean_reference(self):
56    data = self.cleaned_data["reference"]
57    if rx_bad_ref.search(data):
58        raise ValidationError(_("Bad reference: '#', '?', '/' and '..' are not allowed"))
59    return re.sub("\s+", " ", data.strip(" "))
60
61def _clean_revision(self):
62    data = self.cleaned_data["revision"]
63    if rx_bad_ref.search(data):
64        raise ValidationError(_("Bad revision: '#', '?', '/' and '..' are not allowed"))
65    return re.sub("\s+", " ", data.strip(" "))
66
67INVALID_GROUP = _("Bad group, check that the group exists and that you belong"
68        " to this group.")
69
70def auto_complete_fields(form, cls):
71    """
72    Replaces textinputs field of *form* with auto complete fields.
73
74    :param form: a :class:`Form` instance or class
75    :param cls: class of the source that provides suggested values
76    """
77    for field, form_field in form.base_fields.iteritems():
78        if field not in ("reference", "revision") and \
79                isinstance(form_field.widget, forms.TextInput):
80            source = '/ajax/complete/%s/%s/' % (cls.__name__, field)
81            form_field.widget = JQueryAutoComplete(source)
82
83def get_new_reference(cls, start=0):
84    u"""
85    Returns a new reference for creating a :class:`.PLMObject` of type
86    *cls*.
87
88    The formatting is ``PART_000XX`` if *cls* is a subclass of :class:`.Part`
89    and ``DOC_000XX`` otherwise.
90   
91    The number is the count of Parts or Documents plus *start* plus 1.
92    It is incremented while an object with the same reference aleady exists.
93    *start* can be used to create several creation forms at once.
94
95    .. note::
96        The returned referenced may not be valid if a new object has been
97        created after the call to this function.
98    """
99    if issubclass(cls, m.Part):
100        base_cls, name = m.Part, "PART"
101    else:
102        base_cls, name = m.Document, "DOC"
103    nb = base_cls.objects.count() + start + 1
104    reference = "%s_%05d" % (name, nb)
105    while base_cls.objects.filter(reference=reference).exists():
106        nb += 1
107        reference = "%s_%05d" % (name, nb)
108    return reference
109
110def get_initial_creation_data(cls, start=0):
111    u"""
112    Returns initial data to create a new object (from :func:`get_creation_form`).
113
114    :param cls: class of the created object
115    :param start: used to generate the reference,  see :func:`get_new_reference`
116    """
117    if issubclass(cls, m.PLMObject):
118        data = {
119                'reference' : get_new_reference(cls),
120                'revision' : 'a',
121                'lifecycle' : str(m.get_default_lifecycle().pk),
122        }
123    else:
124        data = {}
125    return data
126
127def get_creation_form(user, cls=m.PLMObject, data=None, start=0):
128    u"""
129    Returns a creation form suitable to creates an object
130    of type *cls*.
131
132    The returned form can be used, if it is valid, with the function
133    :meth:`~plmapp.controllers.PLMObjectController.create_from_form`
134    to create a :class:`~plmapp.models.PLMObject` and his associated
135    :class:`~plmapp.controllers.PLMObjectController`.
136
137    If *data* is provided, it will be used to fill the form.
138
139    *start* is used if *data* is ``None``, it's usefull if you need to show
140    several initial creation forms at once and you want different references.
141    """
142    Form = get_creation_form.cache.get(cls)
143    if Form is None:
144        fields = cls.get_creation_fields()
145        Form = modelform_factory(cls, fields=fields, exclude=('type', 'state'))
146        # replace textinputs with autocomplete inputs, see ticket #66
147        auto_complete_fields(Form, cls)
148        if issubclass(cls, m.PLMObject):
149            Form.clean_reference = _clean_reference
150            Form.clean_revision = _clean_revision
151            def _clean(self):
152                cleaned_data = self.cleaned_data
153                ref = cleaned_data.get("reference", "")
154                rev = cleaned_data.get("revision", "")
155                if cls.objects.filter(type=cls.__name__, revision=rev, reference=ref):
156                    raise ValidationError(_("An object with the same type, reference and revision already exists"))
157                return cleaned_data
158            Form.clean = _clean
159        get_creation_form.cache[cls] = Form
160    if data is None:
161        form = Form(initial=get_initial_creation_data(cls, start))
162    else:
163        form = Form(data=data)
164    if issubclass(cls, m.PLMObject):
165        # display only valid groups
166        groups = user.groups.all().values_list("id", flat=True)
167        field = form.fields["group"]
168        field.queryset = m.GroupInfo.objects.filter(id__in=groups)
169        field.error_messages["invalid_choice"] = INVALID_GROUP
170    return form
171get_creation_form.cache = {}
172       
173def get_modification_form(cls=m.PLMObject, data=None, instance=None):
174    Form = get_modification_form.cache.get(cls)
175    if Form is None:
176        fields = cls.get_modification_fields()
177        Form = modelform_factory(cls, fields=fields)
178        auto_complete_fields(Form, cls)
179        get_modification_form.cache[cls] = Form
180    if data:
181        return Form(data)
182    elif instance:
183        return Form(instance=instance)
184    else:
185        return Form()
186get_modification_form.cache = {}
187
188def integerfield_clean(value):
189    if value:
190        value = value.replace(" ", "")
191        value_validated = re.search(r'^([><]?)(\-?\d*)$',value)
192        if value_validated:
193            return value_validated.groups()
194        else:
195            raise ValidationError("Number or \"< Number\" or \"> Number\"")
196    return None
197
198class TypeForm(forms.Form):
199    LIST = m.get_all_users_and_plmobjects_with_level()
200    type = forms.TypedChoiceField(choices=LIST)
201
202class TypeFormWithoutUser(forms.Form):
203    LIST_WO_USER = m.get_all_plmobjects_with_level()
204    type = forms.TypedChoiceField(choices=LIST_WO_USER,
205            label=_("Select a type"))
206
207class TypeSearchForm(TypeForm):
208    pass
209
210class FakeItems(object):
211    def __init__(self, values):
212        self.values = values
213    def items(self):
214        return self.values
215
216def get_search_form(cls=m.PLMObject, data=None):
217    Form = get_search_form.cache.get(cls)
218    if Form is None:
219        if issubclass(cls, (m.PLMObject, m.GroupInfo)):
220            fields = set(cls.get_creation_fields())
221            fields.update(set(cls.get_modification_fields()))
222            fields.difference_update(("type", "lifecycle", "group"))
223        else:
224            fields = set(["username", "first_name", "last_name"])
225        fields_dict = {}
226        for field in fields:
227            model_field = cls._meta.get_field(field)
228            form_field = model_field.formfield()
229            form_field.help_text = ""
230            if isinstance(form_field.widget, forms.Textarea):
231                form_field.widget = forms.TextInput(attrs={'title':"You can use * charactere(s) to enlarge your research.", 'value':"*"})
232            if isinstance(form_field.widget, forms.TextInput):
233                source = '/ajax/complete/%s/%s/' % (cls.__name__, field)
234                form_field.widget = JQueryAutoComplete(source,
235                    attrs={'title':"You can use * charactere(s) to enlarge your research.", 'value':"*"})
236            if isinstance(form_field, forms.fields.IntegerField) and isinstance(form_field.widget, forms.TextInput):
237                form_field.widget = forms.TextInput(attrs={'title':"Please enter a whole number. You can use < or > to enlarge your research."})
238            form_field.required = False
239            fields_dict[field] = form_field
240            if isinstance(form_field, forms.fields.IntegerField):
241                form_field.clean = integerfield_clean
242
243        def search(self, query_set=None):
244            if self.is_valid():
245                query = {}
246                for field in self.changed_data:
247                    model_field = cls._meta.get_field(field)
248                    form_field = model_field.formfield()
249                    value =  self.cleaned_data[field]
250                    if value is None or (isinstance(value, basestring) and value.isspace()):
251                        continue
252                    if isinstance(form_field, forms.fields.CharField)\
253                                    and isinstance(form_field.widget, (forms.TextInput, forms.Textarea)):
254                        value_list = re.split(r"\s*\*\s*", value)
255                        if len(value_list)==1:
256                            query["%s__iexact"%field]=value_list[0]
257                        else :
258                            if value_list[0]:
259                                query["%s__istartswith"%field]=value_list[0]
260                            if value_list[-1]:
261                                query["%s__iendswith"%field]=value_list[-1]
262                            for value_item in value_list[1:-1]:
263                                if value_item:
264                                    query["%s__icontains"%field]=value_item
265                    elif isinstance(form_field, forms.fields.IntegerField)\
266                                    and isinstance(form_field.widget, (forms.TextInput, forms.Textarea)):
267                        sign, value_str = self.cleaned_data[field]
268                        cr = "%s__%s" %(field, {"" : "exact", ">" : "gt", "<" : "lt"}[sign])
269                        query[cr]= int(value_str)
270                    else:
271                        query[field] = self.cleaned_data[field]
272                    query_set = query_set.filter(**query)
273                if query_set is not None:
274                    return query_set
275                else:
276                    return []
277        fields_list = fields_dict.items()
278        for ref, field in fields_list:
279            if ref=='reference':
280                fields_list.remove((ref, field))
281                fields_list.insert(0, (ref, field))
282                break
283        ordered_fields_list = FakeItems(fields_list)
284        Form = type("Search%sForm" % cls.__name__,
285                    (forms.BaseForm,),
286                    {"base_fields" : ordered_fields_list, "search" : search})
287        get_search_form.cache[cls] = Form
288    if data is not None:
289        return Form(data=data, empty_permitted=True)
290    else:
291        return Form(empty_permitted=True)
292get_search_form.cache = {}   
293
294class SimpleSearchForm(forms.Form):
295    q = forms.CharField(label=_("Query"), required=False)
296
297    def search(self, *models):
298        from haystack.query import EmptySearchQuerySet
299        from openPLM.plmapp.search import SmartSearchQuerySet
300
301        if self.is_valid():
302            query = self.cleaned_data["q"].strip()
303            sqs = SmartSearchQuerySet().highlight().models(*models)
304            if not query or query == "*":
305                return sqs
306            results = sqs.auto_query(query)
307            return results
308        else:
309            return EmptySearchQuerySet()
310       
311
312class AddChildForm(PLMObjectForm):
313    quantity = forms.FloatField()
314    order = forms.IntegerField()
315    unit = forms.ChoiceField(choices=UNITS, initial=DEFAULT_UNIT)
316
317
318class DisplayChildrenForm(forms.Form):
319    LEVELS = (("all", "All levels",),
320              ("first", "First level",),
321              ("last", "Last level"),)
322    level = forms.ChoiceField(choices=LEVELS, widget=forms.RadioSelect())
323    date = forms.SplitDateTimeField(required=False)
324
325class ModifyChildForm(forms.ModelForm):
326    delete = forms.BooleanField(required=False, initial=False)
327    parent = forms.ModelChoiceField(queryset=m.Part.objects.all(),
328                                   widget=forms.HiddenInput())
329    child = forms.ModelChoiceField(queryset=m.Part.objects.all(),
330                                   widget=forms.HiddenInput())
331    quantity = forms.FloatField(widget=forms.TextInput(attrs={'size':'4'}))
332    order = forms.IntegerField(widget=forms.TextInput(attrs={'size':'2'}))
333    class Meta:
334        model = m.ParentChildLink
335        fields = ["order", "quantity", "unit", "child", "parent",]
336
337ChildrenFormset = modelformset_factory(m.ParentChildLink,
338                                       form=ModifyChildForm, extra=0)
339def get_children_formset(controller, data=None):
340    if data is None:
341        queryset = m.ParentChildLink.objects.filter(parent=controller,
342                                                    end_time__exact=None)
343        formset = ChildrenFormset(queryset=queryset)
344    else:
345        formset = ChildrenFormset(data=data)
346    return formset
347
348class AddRevisionForm(forms.Form):
349    revision = forms.CharField()
350    clean_revision = _clean_revision
351   
352class AddRelPartForm(PLMObjectForm):
353    pass
354   
355class ModifyRelPartForm(forms.ModelForm):
356    delete = forms.BooleanField(required=False, initial=False)
357    document = forms.ModelChoiceField(queryset=m.Document.objects.all(),
358                                   widget=forms.HiddenInput())
359    part = forms.ModelChoiceField(queryset=m.Part.objects.all(),
360                                   widget=forms.HiddenInput())
361    class Meta:
362        model = m.DocumentPartLink
363        fields = ["document", "part"]
364       
365RelPartFormset = modelformset_factory(m.DocumentPartLink,
366                                      form=ModifyRelPartForm, extra=0)
367def get_rel_part_formset(controller, data=None):
368    if data is None:
369        queryset = controller.get_attached_parts()
370        formset = RelPartFormset(queryset=queryset)
371    else:
372        formset = RelPartFormset(data=data)
373    return formset
374
375class AddFileForm(forms.Form):
376    filename = forms.FileField()
377   
378class ModifyFileForm(forms.ModelForm):
379    delete = forms.BooleanField(required=False, initial=False)
380    document = forms.ModelChoiceField(queryset=m.Document.objects.all(),
381                                   widget=forms.HiddenInput())
382    class Meta:
383        model = m.DocumentFile
384        fields = ["document"]
385       
386FileFormset = modelformset_factory(m.DocumentFile, form=ModifyFileForm, extra=0)
387def get_file_formset(controller, data=None):
388    if data is None:
389        queryset = controller.files
390        formset = FileFormset(queryset=queryset)
391    else:
392        formset = FileFormset(data=data)
393    return formset
394
395class AddDocCadForm(PLMObjectForm):
396    pass
397   
398class ModifyDocCadForm(forms.ModelForm):
399    delete = forms.BooleanField(required=False, initial=False)
400    part = forms.ModelChoiceField(queryset=m.Part.objects.all(),
401                                   widget=forms.HiddenInput())
402    document = forms.ModelChoiceField(queryset=m.Document.objects.all(),
403                                   widget=forms.HiddenInput())
404    class Meta:
405        model = m.DocumentPartLink
406        fields = ["part", "document"]
407       
408DocCadFormset = modelformset_factory(m.DocumentPartLink,
409                                     form=ModifyDocCadForm, extra=0)
410def get_doc_cad_formset(controller, data=None):
411    if data is None:
412        queryset = controller.get_attached_documents()
413        formset = DocCadFormset(queryset=queryset)
414    else:
415        formset = DocCadFormset(data=data)
416    return formset
417
418
419class NavigateFilterForm(forms.Form):
420    only_search_results = forms.BooleanField(initial=False,
421                required=False, label=_("only search results"))
422    prog = forms.ChoiceField(choices=(("dot", _("Hierarchical")),
423                                      ("neato", _("Radial 1")),
424                                      ("twopi", _("Radial 2")),
425                                      ),
426                             required=False, initial="dot",
427                             label=_("layout"))
428    doc_parts = forms.CharField(initial="", required="",
429                                widget=forms.HiddenInput())
430    update = forms.BooleanField(initial=False, required=False,
431           widget=forms.HiddenInput() )
432
433class PartNavigateFilterForm(NavigateFilterForm):
434    child = forms.BooleanField(initial=True, required=False, label=_("child"))
435    parents = forms.BooleanField(initial=True, required=False, label=_("parents"))
436    doc = forms.BooleanField(initial=True, required=False, label=_("doc"))
437    cad = forms.BooleanField(required=False, label=_("cad"))
438    owner = forms.BooleanField(required=False, label=_("owner"))
439    signer = forms.BooleanField(required=False, label=_("signer"))
440    notified = forms.BooleanField(required=False, label=_("notified"))
441
442class DocNavigateFilterForm(NavigateFilterForm):
443    part = forms.BooleanField(initial=True, required=False, label=_("part"))
444    owner = forms.BooleanField(required=False, label=_("owner"))
445    signer = forms.BooleanField(required=False, label=_("signer"))
446    notified = forms.BooleanField(required=False, label=_("notified"))
447
448class UserNavigateFilterForm(NavigateFilterForm):
449    owned = forms.BooleanField(initial=True, required=False, label=_("owned"))
450    to_sign = forms.BooleanField(required=False, label=_("to sign"))
451    request_notification_from = forms.BooleanField(required=False, label=_("request notification from"))
452
453class GroupNavigateFilterForm(NavigateFilterForm):
454    owner = forms.BooleanField(required=False, label=_("owner"))
455    user = forms.BooleanField(required=False, label=_("user"))
456    part = forms.BooleanField(initial=True, required=False, label=_("part"))
457    doc = forms.BooleanField(initial=True, required=False, label=_("doc"))
458
459def get_navigate_form(obj):
460    if isinstance(obj, UserController):
461        cls = UserNavigateFilterForm
462    elif isinstance(obj, DocumentController):
463        cls = DocNavigateFilterForm
464    elif isinstance(obj, GroupController):
465        cls = GroupNavigateFilterForm
466    else:
467        cls = PartNavigateFilterForm
468    return cls
469
470
471class OpenPLMUserChangeForm(forms.ModelForm):
472    #username = forms.RegexField(widget=forms.HiddenInput())
473    class Meta:
474        model = User
475        exclude = ('username','is_staff', 'is_active', 'is_superuser', 'last_login', 'date_joined', 'groups', 'user_permissions', 'password')
476
477class SelectUserForm(forms.Form):
478    type = forms.CharField(label=_("Type"), initial="User")
479    username = forms.CharField(label=_("Username"))
480   
481   
482class ModifyUserForm(forms.Form):
483    delete = forms.BooleanField(required=False, initial=False)
484    user = forms.ModelChoiceField(queryset=User.objects.all(),
485                                   widget=forms.HiddenInput())
486    group = forms.ModelChoiceField(queryset=Group.objects.all(),
487                                   widget=forms.HiddenInput())
488   
489    @property
490    def user_data(self):
491        return self.initial["user"]
492
493UserFormset = formset_factory(ModifyUserForm, extra=0)
494def get_user_formset(controller, data=None):
495    if data is None:
496        queryset = controller.user_set.exclude(id=controller.owner.id)
497        initial = [dict(group=controller.object, user=user)
498                for user in queryset]
499        formset = UserFormset(initial=initial)
500    else:
501        formset = UserFormset(data)
502    return formset
503
504
505class SponsorForm(forms.ModelForm):
506    sponsor = forms.ModelChoiceField(queryset=User.objects.all(),
507            required=True, widget=forms.HiddenInput())
508    warned = forms.BooleanField(initial=False, required=False,
509                                widget=forms.HiddenInput())
510
511    class Meta:
512        model = User
513        fields = ('username', 'first_name', 'last_name', 'email', 'groups')
514
515    def __init__(self, *args, **kwargs):
516        sponsor = kwargs.pop("sponsor", None)
517        super(SponsorForm, self).__init__(*args, **kwargs)
518        if "sponsor" in self.data:
519            sponsor = int(self.data["sponsor"])
520        if sponsor is not None:
521            qset = m.GroupInfo.objects.filter(owner__id=sponsor)
522            self.fields["groups"].queryset = qset
523        self.fields["groups"].help_text = _("The new user will belong to the selected groups")
524        for key, field in self.fields.iteritems():
525            if key != "warned":
526                field.required = True
527
528    def clean_email(self):
529        email = self.cleaned_data["email"]
530        if email and bool(User.objects.filter(email=email)):
531            raise forms.ValidationError(_(u'Email address must be unique.'))
532        try:
533            # checks *email*
534            if settings.RESTRICT_EMAIL_TO_DOMAINS:
535                # i don't know if a domain can contains a '@'
536                domain = email.rsplit("@", 1)[1]
537                if domain not in Site.objects.values_list("domain", flat=True):
538                    raise forms.ValidationError(_(u"Email's domain not valid"))
539        except AttributeError:
540            # restriction disabled if the setting is not set
541            pass
542        return email
543 
544    def clean(self):
545        super(SponsorForm, self).clean()
546        if not self.cleaned_data.get("warned", False):
547            first_name = self.cleaned_data["first_name"]
548            last_name = self.cleaned_data["last_name"]
549            homonyms = User.objects.filter(first_name=first_name, last_name=last_name)
550            if homonyms:
551                self.data = self.data.copy()
552                self.data["warned"] = "on"
553                error = _(u"Warning! There are homonyms: %s!") % \
554                    u", ".join(u.username for u in homonyms)
555                raise forms.ValidationError(error)
556        return self.cleaned_data
557
558_inv_qset = m.Invitation.objects.filter(state=m.Invitation.PENDING)
559class InvitationForm(forms.Form):
560    invitation = forms.ModelChoiceField(queryset=_inv_qset,
561            required=True, widget=forms.HiddenInput())
562
563class CSVForm(forms.Form):
564    file = forms.FileField()
565    encoding = forms.TypedChoiceField(initial="utf_8", choices=ENCODINGS)
566
567
568def get_headers_formset(Importer):
569    class CSVHeaderForm(forms.Form):
570        HEADERS = Importer.get_headers()
571        header = forms.TypedChoiceField(choices=zip(HEADERS, HEADERS),
572                required=False)
573
574    class BaseHeadersFormset(BaseFormSet):
575
576        def clean(self):
577            if any(self.errors):
578                return
579            headers = []
580            for form in self.forms:
581                header = form.cleaned_data['header']
582                if header == u'None':
583                    header = None
584                if header and header in headers:
585                    raise forms.ValidationError(_("Columns must have distinct headers."))
586                headers.append(header)
587            for field in Importer.REQUIRED_HEADERS:
588                if field not in headers:
589                    raise forms.ValidationError(Importer.get_missing_headers_msg())
590            self.headers = headers
591
592    return formset_factory(CSVHeaderForm, extra=0, formset=BaseHeadersFormset)
593
594get_headers_formset = memoize(get_headers_formset, {}, 1)
595
596from openPLM.plmapp.archive import ARCHIVE_FORMATS
597class ArchiveForm(forms.Form):
598    format = forms.TypedChoiceField(choices=zip(ARCHIVE_FORMATS, ARCHIVE_FORMATS))
599
Note: See TracBrowser for help on using the repository browser.