web/views/editcontroller.py
changeset 0 b97547f5f1fa
child 884 969c16600fb3
child 1162 f210dce0dc47
equal deleted inserted replaced
-1:000000000000 0:b97547f5f1fa
       
     1 """The edit controller, handling form submitting.
       
     2 
       
     3 :organization: Logilab
       
     4 :copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved.
       
     5 :contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr
       
     6 """
       
     7 __docformat__ = "restructuredtext en"
       
     8 from decimal import Decimal
       
     9 
       
    10 from rql.utils import rqlvar_maker
       
    11 
       
    12 from cubicweb import Binary, ValidationError, typed_eid
       
    13 from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit
       
    14 from cubicweb.web.controller import parse_relations_descr
       
    15 from cubicweb.web.views.basecontrollers import ViewController
       
    16 
       
    17 
       
    18 class ToDoLater(Exception):
       
    19     """exception used in the edit controller to indicate that a relation
       
    20     can't be handled right now and have to be handled later
       
    21     """
       
    22 
       
    23 class EditController(ViewController):
       
    24     id = 'edit'
       
    25 
       
    26     def publish(self, rset=None, fromjson=False):
       
    27         """edit / create / copy / delete entity / relations"""
       
    28         self.fromjson = fromjson
       
    29         req = self.req
       
    30         form = req.form
       
    31         for key in form:
       
    32             # There should be 0 or 1 action
       
    33             if key.startswith('__action_'):
       
    34                 cbname = key[1:]
       
    35                 try:
       
    36                     callback = getattr(self, cbname)
       
    37                 except AttributeError:
       
    38                     raise ValidationError(None,
       
    39                                           {None: req._('invalid action %r' % key)})
       
    40                 else:
       
    41                     return callback()
       
    42         self._default_publish()
       
    43         self.reset()
       
    44 
       
    45     def _default_publish(self):
       
    46         req = self.req
       
    47         form = req.form
       
    48         # no specific action, generic edition
       
    49         self._to_create = req.data['eidmap'] = {}
       
    50         self._pending_relations = []
       
    51         todelete = self.req.get_pending_deletes()
       
    52         toinsert = self.req.get_pending_inserts()
       
    53         try:
       
    54             methodname = form.pop('__method', None)
       
    55             for eid in req.edited_eids():
       
    56                 formparams = req.extract_entity_params(eid)
       
    57                 if methodname is not None:
       
    58                     entity = req.eid_rset(eid).get_entity(0, 0)
       
    59                     method = getattr(entity, methodname)
       
    60                     method(formparams)
       
    61                 eid = self.edit_entity(formparams)
       
    62         except (RequestError, NothingToEdit):
       
    63             if '__linkto' in form and 'eid' in form:
       
    64                 self.execute_linkto()
       
    65             elif not ('__delete' in form or '__insert' in form or todelete or toinsert):
       
    66                 raise ValidationError(None, {None: req._('nothing to edit')})
       
    67         # handle relations in newly created entities
       
    68         if self._pending_relations:
       
    69             for rschema, formparams, x, entity in self._pending_relations:
       
    70                 self.handle_relation(rschema, formparams, x, entity, True)
       
    71             
       
    72         # XXX this processes *all* pending operations of *all* entities
       
    73         if form.has_key('__delete'):
       
    74             todelete += req.list_form_param('__delete', form, pop=True)
       
    75         if todelete:
       
    76             self.delete_relations(parse_relations_descr(todelete))
       
    77         if form.has_key('__insert'):
       
    78             toinsert = req.list_form_param('__insert', form, pop=True)
       
    79         if toinsert:
       
    80             self.insert_relations(parse_relations_descr(toinsert))
       
    81         self.req.remove_pending_operations()
       
    82         
       
    83     def edit_entity(self, formparams, multiple=False):
       
    84         """edit / create / copy an entity and return its eid"""
       
    85         etype = formparams['__type']
       
    86         entity = self.vreg.etype_class(etype)(self.req, None, None)
       
    87         entity.eid = eid = self._get_eid(formparams['eid'])
       
    88         edited = self.req.form.get('__maineid') == formparams['eid']
       
    89         # let a chance to do some entity specific stuff.
       
    90         entity.pre_web_edit() 
       
    91         # create a rql query from parameters
       
    92         self.relations = []
       
    93         self.restrictions = []
       
    94         # process inlined relations at the same time as attributes
       
    95         # this is required by some external source such as the svn source which
       
    96         # needs some information provided by those inlined relation. Moreover
       
    97         # this will generate less write queries.
       
    98         for rschema in entity.e_schema.subject_relations():
       
    99             if rschema.is_final():
       
   100                 self.handle_attribute(entity, rschema, formparams)
       
   101             elif rschema.inlined:
       
   102                 self.handle_inlined_relation(rschema, formparams, entity)
       
   103         execute = self.req.execute
       
   104         if eid is None: # creation or copy
       
   105             if self.relations: 
       
   106                 rql = 'INSERT %s X: %s' % (etype, ','.join(self.relations))
       
   107             else:
       
   108                 rql = 'INSERT %s X' % etype
       
   109             if self.restrictions:
       
   110                 rql += ' WHERE %s' % ','.join(self.restrictions)
       
   111             try:
       
   112                 # get the new entity (in some cases, the type might have 
       
   113                 # changed as for the File --> Image mutation)
       
   114                 entity = execute(rql, formparams).get_entity(0, 0)
       
   115                 eid = entity.eid
       
   116             except ValidationError, ex:
       
   117                 # ex.entity may be an int or an entity instance
       
   118                 self._to_create[formparams['eid']] = ex.entity
       
   119                 if self.fromjson:
       
   120                     ex.entity = formparams['eid']
       
   121                 raise
       
   122             self._to_create[formparams['eid']] = eid
       
   123         elif self.relations: # edition of an existant entity
       
   124             varmaker = rqlvar_maker()
       
   125             var = varmaker.next()
       
   126             while var in formparams:
       
   127                 var = varmaker.next()
       
   128             rql = 'SET %s WHERE X eid %%(%s)s' % (','.join(self.relations), var)
       
   129             if self.restrictions:
       
   130                 rql += ', %s' % ','.join(self.restrictions)
       
   131             formparams[var] = eid
       
   132             execute(rql, formparams)
       
   133         for rschema in entity.e_schema.subject_relations():
       
   134             if rschema.is_final() or rschema.inlined:
       
   135                 continue
       
   136             self.handle_relation(rschema, formparams, 'subject', entity)
       
   137         for rschema in entity.e_schema.object_relations():
       
   138             if rschema.is_final():
       
   139                 continue
       
   140             self.handle_relation(rschema, formparams, 'object', entity)
       
   141         if edited:
       
   142             self.notify_edited(entity)
       
   143         if formparams.has_key('__delete'):
       
   144             todelete = self.req.list_form_param('__delete', formparams, pop=True)
       
   145             self.delete_relations(parse_relations_descr(todelete))
       
   146         if formparams.has_key('__cloned_eid'):
       
   147             entity.copy_relations(formparams['__cloned_eid'])
       
   148         if formparams.has_key('__insert'):
       
   149             toinsert = self.req.list_form_param('__insert', formparams, pop=True)
       
   150             self.insert_relations(parse_relations_descr(toinsert))
       
   151         if edited: # only execute linkto for the main entity
       
   152             self.execute_linkto(eid)
       
   153         return eid
       
   154 
       
   155     def _action_apply(self):
       
   156         self._default_publish()
       
   157         self.reset()
       
   158             
       
   159     def _action_cancel(self):
       
   160         errorurl = self.req.form.get('__errorurl')
       
   161         if errorurl:
       
   162             self.req.cancel_edition(errorurl)
       
   163         return self.reset()
       
   164 
       
   165     def _action_delete(self):
       
   166         self.delete_entities(self.req.edited_eids(withtype=True))
       
   167         return self.reset()
       
   168 
       
   169     def _needs_edition(self, rtype, formparams):
       
   170         """returns True and and the new value if `rtype` was edited"""
       
   171         editkey = 'edits-%s' % rtype
       
   172         if not editkey in formparams:
       
   173             return False, None # not edited
       
   174         value = formparams.get(rtype) or None
       
   175         if (formparams.get(editkey) or None) == value:
       
   176             return False, None # not modified
       
   177         if value == INTERNAL_FIELD_VALUE:
       
   178             value = None        
       
   179         return True, value
       
   180 
       
   181     def handle_attribute(self, entity, rschema, formparams):
       
   182         """append to `relations` part of the rql query to edit the
       
   183         attribute described by the given schema if necessary
       
   184         """
       
   185         attr = rschema.type
       
   186         edition_needed, value = self._needs_edition(attr, formparams)
       
   187         if not edition_needed:
       
   188             return
       
   189         # test if entity class defines a special handler for this attribute
       
   190         custom_edit = getattr(entity, 'custom_%s_edit' % attr, None)
       
   191         if custom_edit:
       
   192             custom_edit(formparams, value, self.relations)
       
   193             return
       
   194         attrtype = rschema.objects(entity.e_schema)[0].type
       
   195         # on checkbox or selection, the field may not be in params
       
   196         if attrtype == 'Boolean':
       
   197             value = bool(value)
       
   198         elif attrtype == 'Decimal':
       
   199             value = Decimal(value)
       
   200         elif attrtype == 'Bytes':
       
   201             # if it is a file, transport it using a Binary (StringIO)
       
   202             if formparams.has_key('__%s_detach' % attr):
       
   203                 # drop current file value
       
   204                 value = None
       
   205             # no need to check value when nor explicit detach nor new file submitted,
       
   206             # since it will think the attribut is not modified
       
   207             elif isinstance(value, unicode):
       
   208                 # file modified using a text widget
       
   209                 value = Binary(value.encode(entity.text_encoding(attr)))
       
   210             else:
       
   211                 # (filename, mimetype, stream)
       
   212                 val = Binary(value[2].read())
       
   213                 if not val.getvalue(): # usually an unexistant file
       
   214                     value = None
       
   215                 else:
       
   216                     # XXX suppose a File compatible schema
       
   217                     val.filename = value[0]
       
   218                     if entity.has_format(attr):
       
   219                         key = '%s_format' % attr
       
   220                         formparams[key] = value[1]
       
   221                         self.relations.append('X %s_format %%(%s)s'
       
   222                                               % (attr, key))
       
   223                     if entity.e_schema.has_subject_relation('name') \
       
   224                            and not formparams.get('name'):
       
   225                         formparams['name'] = value[0]
       
   226                         self.relations.append('X name %(name)s')
       
   227                     value = val
       
   228         elif value is not None:
       
   229             if attrtype in ('Date', 'Datetime', 'Time'):
       
   230                 try:
       
   231                     value = self.parse_datetime(value, attrtype)
       
   232                 except ValueError:
       
   233                     raise ValidationError(entity.eid,
       
   234                                           {attr: self.req._("invalid date")})
       
   235             elif attrtype == 'Password':
       
   236                 # check confirmation (see PasswordWidget for confirmation field name)
       
   237                 confirmval = formparams.get(attr + '-confirm')
       
   238                 if confirmval != value:
       
   239                     raise ValidationError(entity.eid,
       
   240                                           {attr: self.req._("password and confirmation don't match")})
       
   241                 # password should *always* be utf8 encoded
       
   242                 value = value.encode('UTF8')
       
   243             else:
       
   244                 # strip strings
       
   245                 value = value.strip()
       
   246         elif attrtype == 'Password':
       
   247             # skip None password
       
   248             return # unset password
       
   249         formparams[attr] = value
       
   250         self.relations.append('X %s %%(%s)s' % (attr, attr))
       
   251 
       
   252     def _relation_values(self, rschema, formparams, x, entity, late=False):
       
   253         """handle edition for the (rschema, x) relation of the given entity
       
   254         """
       
   255         rtype = rschema.type
       
   256         editkey = 'edit%s-%s' % (x[0], rtype)
       
   257         if not editkey in formparams:
       
   258             return # not edited
       
   259         try:
       
   260             values = self._linked_eids(self.req.list_form_param(rtype, formparams), late)
       
   261         except ToDoLater:
       
   262             self._pending_relations.append((rschema, formparams, x, entity))
       
   263             return
       
   264         origvalues = set(typed_eid(eid) for eid in self.req.list_form_param(editkey, formparams))
       
   265         return values, origvalues
       
   266 
       
   267     def handle_inlined_relation(self, rschema, formparams, entity, late=False):
       
   268         """handle edition for the (rschema, x) relation of the given entity
       
   269         """
       
   270         try:
       
   271             values, origvalues = self._relation_values(rschema, formparams,
       
   272                                                        'subject', entity, late)
       
   273         except TypeError:
       
   274             return # not edited / to do later
       
   275         if values == origvalues:
       
   276             return # not modified
       
   277         attr = str(rschema)
       
   278         if values:
       
   279             formparams[attr] = iter(values).next()
       
   280             self.relations.append('X %s %s' % (attr, attr.upper()))
       
   281             self.restrictions.append('%s eid %%(%s)s' % (attr.upper(), attr))
       
   282         elif entity.has_eid():
       
   283             self.handle_relation(rschema, formparams, 'subject', entity, late)
       
   284         
       
   285     def handle_relation(self, rschema, formparams, x, entity, late=False):
       
   286         """handle edition for the (rschema, x) relation of the given entity
       
   287         """
       
   288         try:
       
   289             values, origvalues = self._relation_values(rschema, formparams, x,
       
   290                                                        entity, late)
       
   291         except TypeError:
       
   292             return # not edited / to do later
       
   293         etype = entity.e_schema
       
   294         if values == origvalues:
       
   295             return # not modified
       
   296         if x == 'subject':
       
   297             desttype = rschema.objects(etype)[0]
       
   298             card = rschema.rproperty(etype, desttype, 'cardinality')[0]
       
   299             subjvar, objvar = 'X', 'Y'
       
   300         else:
       
   301             desttype = rschema.subjects(etype)[0]
       
   302             card = rschema.rproperty(desttype, etype, 'cardinality')[1]
       
   303             subjvar, objvar = 'Y', 'X'
       
   304         eid = entity.eid
       
   305         if x == 'object' or not rschema.inlined or not values:
       
   306             # this is not an inlined relation or no values specified,
       
   307             # explicty remove relations
       
   308             for reid in origvalues.difference(values):
       
   309                 rql = 'DELETE %s %s %s WHERE X eid %%(x)s, Y eid %%(y)s' % (
       
   310                     subjvar, rschema, objvar)
       
   311                 self.req.execute(rql, {'x': eid, 'y': reid}, ('x', 'y'))
       
   312         rql = 'SET %s %s %s WHERE X eid %%(x)s, Y eid %%(y)s' % (
       
   313             subjvar, rschema, objvar)
       
   314         for reid in values.difference(origvalues):
       
   315             self.req.execute(rql, {'x': eid, 'y': reid}, ('x', 'y'))
       
   316     
       
   317     def _get_eid(self, eid):
       
   318         # should be either an int (existant entity) or a variable (to be
       
   319         # created entity)
       
   320         assert eid or eid == 0, repr(eid) # 0 is a valid eid
       
   321         try:
       
   322             return typed_eid(eid)
       
   323         except ValueError:
       
   324             try:
       
   325                 return self._to_create[eid]
       
   326             except KeyError:
       
   327                 self._to_create[eid] = None
       
   328                 return None
       
   329 
       
   330     def _linked_eids(self, eids, late=False):
       
   331         """return a list of eids if they are all known, else raise ToDoLater
       
   332         """
       
   333         result = set()
       
   334         for eid in eids:
       
   335             if not eid: # AutoCompletionWidget
       
   336                 continue
       
   337             eid = self._get_eid(eid)
       
   338             if eid is None:
       
   339                 if not late:
       
   340                     raise ToDoLater()
       
   341                 # eid is still None while it's already a late call
       
   342                 # this mean that the associated entity has not been created
       
   343                 raise Exception('duh')
       
   344             result.add(eid)
       
   345         return result
       
   346         
       
   347