web/form.py
changeset 11057 0b59724cb3f2
parent 11052 058bb3dc685f
child 11058 23eb30449fe5
equal deleted inserted replaced
11052:058bb3dc685f 11057:0b59724cb3f2
     1 # copyright 2003-2011 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     2 # contact http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     3 #
       
     4 # This file is part of CubicWeb.
       
     5 #
       
     6 # CubicWeb is free software: you can redistribute it and/or modify it under the
       
     7 # terms of the GNU Lesser General Public License as published by the Free
       
     8 # Software Foundation, either version 2.1 of the License, or (at your option)
       
     9 # any later version.
       
    10 #
       
    11 # CubicWeb is distributed in the hope that it will be useful, but WITHOUT
       
    12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
       
    13 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
       
    14 # details.
       
    15 #
       
    16 # You should have received a copy of the GNU Lesser General Public License along
       
    17 # with CubicWeb.  If not, see <http://www.gnu.org/licenses/>.
       
    18 """abstract form classes for CubicWeb web client"""
       
    19 __docformat__ = "restructuredtext en"
       
    20 
       
    21 from warnings import warn
       
    22 
       
    23 from six import add_metaclass
       
    24 
       
    25 from logilab.common.decorators import iclassmethod
       
    26 from logilab.common.deprecation import deprecated
       
    27 
       
    28 from cubicweb.appobject import AppObject
       
    29 from cubicweb.view import NOINDEX, NOFOLLOW
       
    30 from cubicweb.web import httpcache, formfields, controller, formwidgets as fwdgs
       
    31 
       
    32 class FormViewMixIn(object):
       
    33     """abstract form view mix-in"""
       
    34     category = 'form'
       
    35     http_cache_manager = httpcache.NoHTTPCacheManager
       
    36     add_to_breadcrumbs = False
       
    37 
       
    38     def html_headers(self):
       
    39         """return a list of html headers (eg something to be inserted between
       
    40         <head> and </head> of the returned page
       
    41 
       
    42         by default forms are neither indexed nor followed
       
    43         """
       
    44         return [NOINDEX, NOFOLLOW]
       
    45 
       
    46     def linkable(self):
       
    47         """override since forms are usually linked by an action,
       
    48         so we don't want them to be listed by appli.possible_views
       
    49         """
       
    50         return False
       
    51 
       
    52 
       
    53 ###############################################################################
       
    54 
       
    55 class metafieldsform(type):
       
    56     """metaclass for FieldsForm to retrieve fields defined as class attributes
       
    57     and put them into a single ordered list: '_fields_'.
       
    58     """
       
    59     def __new__(mcs, name, bases, classdict):
       
    60         allfields = []
       
    61         for base in bases:
       
    62             if hasattr(base, '_fields_'):
       
    63                 allfields += base._fields_
       
    64         clsfields = (item for item in classdict.items()
       
    65                      if isinstance(item[1], formfields.Field))
       
    66         for fieldname, field in sorted(clsfields, key=lambda x: x[1].creation_rank):
       
    67             if not field.name:
       
    68                 field.set_name(fieldname)
       
    69             allfields.append(field)
       
    70         classdict['_fields_'] = allfields
       
    71         return super(metafieldsform, mcs).__new__(mcs, name, bases, classdict)
       
    72 
       
    73 
       
    74 class FieldNotFound(Exception):
       
    75     """raised by field_by_name when a field with the given name has not been
       
    76     found
       
    77     """
       
    78 
       
    79 @add_metaclass(metafieldsform)
       
    80 class Form(AppObject):
       
    81     __registry__ = 'forms'
       
    82 
       
    83     parent_form = None
       
    84     force_session_key = None
       
    85     domid = 'form'
       
    86     copy_nav_params = False
       
    87     control_fields = set( ('__form_id', '__errorurl', '__domid',
       
    88                            '__redirectpath', '_cwmsgid',
       
    89                            ) )
       
    90 
       
    91     def __init__(self, req, rset=None, row=None, col=None,
       
    92                  submitmsg=None, mainform=True, **kwargs):
       
    93         # process kwargs first so we can properly pass them to Form and match
       
    94         # order expectation (ie cw_extra_kwargs populated almost first)
       
    95         hiddens, extrakw = self._process_kwargs(kwargs)
       
    96         # now call ancestor init
       
    97         super(Form, self).__init__(req, rset=rset, row=row, col=col, **extrakw)
       
    98         # then continue with further specific initialization
       
    99         self.fields = list(self.__class__._fields_)
       
   100         for key, val in hiddens:
       
   101             self.add_hidden(key, val)
       
   102         if mainform:
       
   103             formid = kwargs.pop('formvid', self.__regid__)
       
   104             self.add_hidden(u'__form_id', formid)
       
   105             self._posting = self._cw.form.get('__form_id') == formid
       
   106         if mainform:
       
   107             self.add_hidden(u'__errorurl', self.session_key())
       
   108             self.add_hidden(u'__domid', self.domid)
       
   109             self.restore_previous_post(self.session_key())
       
   110         # XXX why do we need two different variables (mainform and copy_nav_params ?)
       
   111         if self.copy_nav_params:
       
   112             for param in controller.NAV_FORM_PARAMETERS:
       
   113                 if not param in kwargs:
       
   114                     value = req.form.get(param)
       
   115                     if value:
       
   116                         self.add_hidden(param, value)
       
   117         if submitmsg is not None:
       
   118             self.set_message(submitmsg)
       
   119 
       
   120     def _process_kwargs(self, kwargs):
       
   121         hiddens = []
       
   122         extrakw = {}
       
   123         # search for navigation parameters and customization of existing
       
   124         # attributes; remaining stuff goes in extrakwargs
       
   125         for key, val in kwargs.items():
       
   126             if key in controller.NAV_FORM_PARAMETERS:
       
   127                 hiddens.append( (key, val) )
       
   128             elif key == 'redirect_path':
       
   129                 hiddens.append( (u'__redirectpath', val) )
       
   130             elif hasattr(self.__class__, key) and not key[0] == '_':
       
   131                 setattr(self, key, val)
       
   132             else:
       
   133                 extrakw[key] = val
       
   134         return hiddens, extrakw
       
   135 
       
   136     def set_message(self, submitmsg):
       
   137         """sets a submitmsg if exists, using _cwmsgid mechanism """
       
   138         cwmsgid = self._cw.set_redirect_message(submitmsg)
       
   139         self.add_hidden(u'_cwmsgid', cwmsgid)
       
   140 
       
   141     @property
       
   142     def root_form(self):
       
   143         """return the root form"""
       
   144         if self.parent_form is None:
       
   145             return self
       
   146         return self.parent_form.root_form
       
   147 
       
   148     @property
       
   149     def form_valerror(self):
       
   150         """the validation error exception if any"""
       
   151         if self.parent_form is None:
       
   152             # unset if restore_previous_post has not be called
       
   153             return getattr(self, '_form_valerror', None)
       
   154         return self.parent_form.form_valerror
       
   155 
       
   156     @property
       
   157     def form_previous_values(self):
       
   158         """previously posted values (on validation error)"""
       
   159         if self.parent_form is None:
       
   160             # unset if restore_previous_post has not be called
       
   161             return getattr(self, '_form_previous_values', {})
       
   162         return self.parent_form.form_previous_values
       
   163 
       
   164     @property
       
   165     def posting(self):
       
   166         """return True if the form is being posted, False if it is being
       
   167         generated.
       
   168         """
       
   169         # XXX check behaviour on regeneration after error
       
   170         if self.parent_form is None:
       
   171             return self._posting
       
   172         return self.parent_form.posting
       
   173 
       
   174     @iclassmethod
       
   175     def _fieldsattr(cls_or_self):
       
   176         if isinstance(cls_or_self, type):
       
   177             fields = cls_or_self._fields_
       
   178         else:
       
   179             fields = cls_or_self.fields
       
   180         return fields
       
   181 
       
   182     @iclassmethod
       
   183     def field_by_name(cls_or_self, name, role=None):
       
   184         """Return field with the given name and role.
       
   185 
       
   186         Raise :exc:`FieldNotFound` if the field can't be found.
       
   187         """
       
   188         for field in cls_or_self._fieldsattr():
       
   189             if field.name == name and field.role == role:
       
   190                 return field
       
   191         raise FieldNotFound(name, role)
       
   192 
       
   193     @iclassmethod
       
   194     def fields_by_name(cls_or_self, name, role=None):
       
   195         """Return a list of fields with the given name and role."""
       
   196         return [field for field in cls_or_self._fieldsattr()
       
   197                 if field.name == name and field.role == role]
       
   198 
       
   199     @iclassmethod
       
   200     def remove_field(cls_or_self, field):
       
   201         """Remove the given field."""
       
   202         cls_or_self._fieldsattr().remove(field)
       
   203 
       
   204     @iclassmethod
       
   205     def append_field(cls_or_self, field):
       
   206         """Append the given field."""
       
   207         cls_or_self._fieldsattr().append(field)
       
   208 
       
   209     @iclassmethod
       
   210     def insert_field_before(cls_or_self, field, name, role=None):
       
   211         """Insert the given field before the field of given name and role."""
       
   212         bfield = cls_or_self.field_by_name(name, role)
       
   213         fields = cls_or_self._fieldsattr()
       
   214         fields.insert(fields.index(bfield), field)
       
   215 
       
   216     @iclassmethod
       
   217     def insert_field_after(cls_or_self, field, name, role=None):
       
   218         """Insert the given field after the field of given name and role."""
       
   219         afield = cls_or_self.field_by_name(name, role)
       
   220         fields = cls_or_self._fieldsattr()
       
   221         fields.insert(fields.index(afield)+1, field)
       
   222 
       
   223     @iclassmethod
       
   224     def add_hidden(cls_or_self, name, value=None, **kwargs):
       
   225         """Append an hidden field to the form. `name`, `value` and extra keyword
       
   226         arguments will be given to the field constructor. The inserted field is
       
   227         returned.
       
   228         """
       
   229         kwargs.setdefault('ignore_req_params', True)
       
   230         kwargs.setdefault('widget', fwdgs.HiddenInput)
       
   231         field = formfields.StringField(name=name, value=value, **kwargs)
       
   232         if 'id' in kwargs:
       
   233             # by default, hidden input don't set id attribute. If one is
       
   234             # explicitly specified, ensure it will be set
       
   235             field.widget.setdomid = True
       
   236         cls_or_self.append_field(field)
       
   237         return field
       
   238 
       
   239     def session_key(self):
       
   240         """return the key that may be used to store / retreive data about a
       
   241         previous post which failed because of a validation error
       
   242         """
       
   243         if self.force_session_key is None:
       
   244             return '%s#%s' % (self._cw.url(), self.domid)
       
   245         return self.force_session_key
       
   246 
       
   247     def restore_previous_post(self, sessionkey):
       
   248         # get validation session data which may have been previously set.
       
   249         # deleting validation errors here breaks form reloading (errors are
       
   250         # no more available), they have to be deleted by application's publish
       
   251         # method on successful commit
       
   252         forminfo = self._cw.session.data.pop(sessionkey, None)
       
   253         if forminfo:
       
   254             self._form_previous_values = forminfo['values']
       
   255             self._form_valerror = forminfo['error']
       
   256             # if some validation error occurred on entity creation, we have to
       
   257             # get the original variable name from its attributed eid
       
   258             foreid = self.form_valerror.entity
       
   259             for var, eid in forminfo['eidmap'].items():
       
   260                 if foreid == eid:
       
   261                     self.form_valerror.eid = var
       
   262                     break
       
   263             else:
       
   264                 self.form_valerror.eid = foreid
       
   265         else:
       
   266             self._form_previous_values = {}
       
   267             self._form_valerror = None
       
   268 
       
   269     def field_error(self, field):
       
   270         """return field's error if specified in current validation exception"""
       
   271         if self.form_valerror:
       
   272             if field.eidparam and self.edited_entity.eid != self.form_valerror.eid:
       
   273                 return None
       
   274             try:
       
   275                 return self.form_valerror.errors.pop(field.role_name())
       
   276             except KeyError:
       
   277                 if field.role and field.name in self.form_valerror:
       
   278                     warn('%s: errors key of attribute/relation should be suffixed by "-<role>"'
       
   279                          % self.form_valerror.__class__, DeprecationWarning)
       
   280                     return self.form_valerror.errors.pop(field.name)
       
   281         return None
       
   282 
       
   283     def remaining_errors(self):
       
   284         return sorted(self.form_valerror.errors.items())