cubicweb/web/views/editcontroller.py
changeset 11129 97095348b3ee
parent 11127 6464edfa95bb
parent 11057 0b59724cb3f2
child 11464 96d1aed1e2b5
equal deleted inserted replaced
11128:9b4de34ad394 11129:97095348b3ee
    29 from logilab.common.deprecation import deprecated
    29 from logilab.common.deprecation import deprecated
    30 from logilab.common.graph import ordered_nodes
    30 from logilab.common.graph import ordered_nodes
    31 
    31 
    32 from rql.utils import rqlvar_maker
    32 from rql.utils import rqlvar_maker
    33 
    33 
    34 from cubicweb import _, Binary, ValidationError
    34 from cubicweb import _, Binary, ValidationError, UnknownEid
    35 from cubicweb.view import EntityAdapter
    35 from cubicweb.view import EntityAdapter
    36 from cubicweb.predicates import is_instance
    36 from cubicweb.predicates import is_instance
    37 from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
    37 from cubicweb.web import (INTERNAL_FIELD_VALUE, RequestError, NothingToEdit,
    38                           ProcessFormError)
    38                           ProcessFormError)
    39 from cubicweb.web.views import basecontrollers, autoform
    39 from cubicweb.web.views import basecontrollers, autoform
    77 class RqlQuery(object):
    77 class RqlQuery(object):
    78     def __init__(self):
    78     def __init__(self):
    79         self.edited = []
    79         self.edited = []
    80         self.restrictions = []
    80         self.restrictions = []
    81         self.kwargs = {}
    81         self.kwargs = {}
       
    82         self.canceled = False
    82 
    83 
    83     def __repr__(self):
    84     def __repr__(self):
    84         return ('Query <edited=%r restrictions=%r kwargs=%r>' % (
    85         return ('Query <edited=%r restrictions=%r kwargs=%r>' % (
    85             self.edited, self.restrictions, self.kwargs))
    86             self.edited, self.restrictions, self.kwargs))
    86 
    87 
    87     def insert_query(self, etype):
    88     def insert_query(self, etype):
       
    89         assert not self.canceled
    88         if self.edited:
    90         if self.edited:
    89             rql = 'INSERT %s X: %s' % (etype, ','.join(self.edited))
    91             rql = 'INSERT %s X: %s' % (etype, ','.join(self.edited))
    90         else:
    92         else:
    91             rql = 'INSERT %s X' % etype
    93             rql = 'INSERT %s X' % etype
    92         if self.restrictions:
    94         if self.restrictions:
    93             rql += ' WHERE %s' % ','.join(self.restrictions)
    95             rql += ' WHERE %s' % ','.join(self.restrictions)
    94         return rql
    96         return rql
    95 
    97 
    96     def update_query(self, eid):
    98     def update_query(self, eid):
       
    99         assert not self.canceled
    97         varmaker = rqlvar_maker()
   100         varmaker = rqlvar_maker()
    98         var = next(varmaker)
   101         var = next(varmaker)
    99         while var in self.kwargs:
   102         while var in self.kwargs:
   100             var = next(varmaker)
   103             var = next(varmaker)
   101         rql = 'SET %s WHERE X eid %%(%s)s' % (','.join(self.edited), var)
   104         rql = 'SET %s WHERE X eid %%(%s)s' % (','.join(self.edited), var)
   190         # those two data variables are used to handle relation from/to entities
   193         # those two data variables are used to handle relation from/to entities
   191         # which doesn't exist at time where the entity is edited and that
   194         # which doesn't exist at time where the entity is edited and that
   192         # deserves special treatment
   195         # deserves special treatment
   193         req.data['pending_inlined'] = defaultdict(set)
   196         req.data['pending_inlined'] = defaultdict(set)
   194         req.data['pending_others'] = set()
   197         req.data['pending_others'] = set()
       
   198         req.data['pending_composite_delete'] = set()
   195         try:
   199         try:
   196             for formparams in self._ordered_formparams():
   200             for formparams in self._ordered_formparams():
   197                 eid = self.edit_entity(formparams)
   201                 eid = self.edit_entity(formparams)
   198         except (RequestError, NothingToEdit) as ex:
   202         except (RequestError, NothingToEdit) as ex:
   199             if '__linkto' in req.form and 'eid' in req.form:
   203             if '__linkto' in req.form and 'eid' in req.form:
   208         for form_, field in req.data.pop('pending_others'):
   212         for form_, field in req.data.pop('pending_others'):
   209             self.handle_formfield(form_, field)
   213             self.handle_formfield(form_, field)
   210         # then execute rql to set all relations
   214         # then execute rql to set all relations
   211         for querydef in self.relations_rql:
   215         for querydef in self.relations_rql:
   212             self._cw.execute(*querydef)
   216             self._cw.execute(*querydef)
       
   217         # delete pending composite
       
   218         for entity in req.data['pending_composite_delete']:
       
   219             entity.cw_delete()
   213         # XXX this processes *all* pending operations of *all* entities
   220         # XXX this processes *all* pending operations of *all* entities
   214         if '__delete' in req.form:
   221         if '__delete' in req.form:
   215             todelete = req.list_form_param('__delete', req.form, pop=True)
   222             todelete = req.list_form_param('__delete', req.form, pop=True)
   216             if todelete:
   223             if todelete:
   217                 autoform.delete_relations(self._cw, todelete)
   224                 autoform.delete_relations(self._cw, todelete)
   264             self.handle_formfield(form, field, rqlquery)
   271             self.handle_formfield(form, field, rqlquery)
   265         # if there are some inlined field which were waiting for this entity's
   272         # if there are some inlined field which were waiting for this entity's
   266         # creation, add relevant data to the rqlquery
   273         # creation, add relevant data to the rqlquery
   267         for form_, field in req.data['pending_inlined'].pop(entity.eid, ()):
   274         for form_, field in req.data['pending_inlined'].pop(entity.eid, ()):
   268             rqlquery.set_inlined(field.name, form_.edited_entity.eid)
   275             rqlquery.set_inlined(field.name, form_.edited_entity.eid)
   269         if self.errors:
   276         if not rqlquery.canceled:
   270             errors = dict((f.role_name(), text_type(ex)) for f, ex in self.errors)
   277             if self.errors:
   271             raise ValidationError(valerror_eid(entity.eid), errors)
   278                 errors = dict((f.role_name(), text_type(ex)) for f, ex in self.errors)
   272         if eid is None: # creation or copy
   279                 raise ValidationError(valerror_eid(entity.eid), errors)
   273             entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery)
   280             if eid is None: # creation or copy
   274         elif rqlquery.edited: # edition of an existant entity
   281                 entity.eid = eid = self._insert_entity(etype, formparams['eid'], rqlquery)
   275             self.check_concurrent_edition(formparams, eid)
   282             elif rqlquery.edited: # edition of an existant entity
   276             self._update_entity(eid, rqlquery)
   283                 self.check_concurrent_edition(formparams, eid)
       
   284                 self._update_entity(eid, rqlquery)
       
   285         else:
       
   286             self.errors = []
   277         if is_main_entity:
   287         if is_main_entity:
   278             self.notify_edited(entity)
   288             self.notify_edited(entity)
   279         if '__delete' in formparams:
   289         if '__delete' in formparams:
   280             # XXX deprecate?
   290             # XXX deprecate?
   281             todelete = req.list_form_param('__delete', formparams, pop=True)
   291             todelete = req.list_form_param('__delete', formparams, pop=True)
   285         if is_main_entity: # only execute linkto for the main entity
   295         if is_main_entity: # only execute linkto for the main entity
   286             self.execute_linkto(entity.eid)
   296             self.execute_linkto(entity.eid)
   287         return eid
   297         return eid
   288 
   298 
   289     def handle_formfield(self, form, field, rqlquery=None):
   299     def handle_formfield(self, form, field, rqlquery=None):
   290         eschema = form.edited_entity.e_schema
   300         entity = form.edited_entity
       
   301         eschema = entity.e_schema
   291         try:
   302         try:
   292             for field, value in field.process_posted(form):
   303             for field, value in field.process_posted(form):
   293                 if not (
   304                 if not (
   294                     (field.role == 'subject' and field.name in eschema.subjrels)
   305                     (field.role == 'subject' and field.name in eschema.subjrels)
   295                     or
   306                     or
   296                     (field.role == 'object' and field.name in eschema.objrels)):
   307                     (field.role == 'object' and field.name in eschema.objrels)):
   297                     continue
   308                     continue
       
   309 
   298                 rschema = self._cw.vreg.schema.rschema(field.name)
   310                 rschema = self._cw.vreg.schema.rschema(field.name)
   299                 if rschema.final:
   311                 if rschema.final:
   300                     rqlquery.set_attribute(field.name, value)
   312                     rqlquery.set_attribute(field.name, value)
       
   313                     continue
       
   314 
       
   315                 if entity.has_eid():
       
   316                     origvalues = set(data[0] for data in entity.related(field.name, field.role).rows)
   301                 else:
   317                 else:
   302                     if form.edited_entity.has_eid():
   318                     origvalues = set()
   303                         origvalues = set(entity.eid for entity in form.edited_entity.related(field.name, field.role, entities=True))
   319                 if value is None or value == origvalues:
   304                     else:
   320                     continue # not edited / not modified / to do later
   305                         origvalues = set()
   321 
   306                     if value is None or value == origvalues:
   322                 unlinked_eids = origvalues - value
   307                         continue # not edited / not modified / to do later
   323 
   308                     if rschema.inlined and rqlquery is not None and field.role == 'subject':
   324                 if unlinked_eids:
   309                         self.handle_inlined_relation(form, field, value, origvalues, rqlquery)
   325                     # Special handling of composite relation removal
   310                     elif form.edited_entity.has_eid():
   326                     self.handle_composite_removal(
   311                         self.handle_relation(form, field, value, origvalues)
   327                         form, field, unlinked_eids, value, rqlquery)
   312                     else:
   328 
   313                         form._cw.data['pending_others'].add( (form, field) )
   329                 if rschema.inlined and rqlquery is not None and field.role == 'subject':
       
   330                     self.handle_inlined_relation(form, field, value, origvalues, rqlquery)
       
   331                 elif form.edited_entity.has_eid():
       
   332                     self.handle_relation(form, field, value, origvalues)
       
   333                 else:
       
   334                     form._cw.data['pending_others'].add( (form, field) )
       
   335 
   314         except ProcessFormError as exc:
   336         except ProcessFormError as exc:
   315             self.errors.append((field, exc))
   337             self.errors.append((field, exc))
       
   338 
       
   339     def handle_composite_removal(self, form, field,
       
   340                                  removed_values, new_values, rqlquery):
       
   341         """
       
   342         In EditController-handled forms, when the user removes a composite
       
   343         relation, it triggers the removal of the related entity in the
       
   344         composite. This is where this happens.
       
   345 
       
   346         See for instance test_subject_subentity_removal in
       
   347         web/test/unittest_application.py.
       
   348         """
       
   349         rschema = self._cw.vreg.schema.rschema(field.name)
       
   350         new_value_etypes = set()
       
   351         # the user could have included nonexisting eids in the POST; don't crash.
       
   352         for eid in new_values:
       
   353             try:
       
   354                 new_value_etypes.add(self._cw.entity_from_eid(eid).cw_etype)
       
   355             except UnknownEid:
       
   356                 continue
       
   357         for unlinked_eid in removed_values:
       
   358             unlinked_entity = self._cw.entity_from_eid(unlinked_eid)
       
   359             rdef = rschema.role_rdef(form.edited_entity.cw_etype,
       
   360                                      unlinked_entity.cw_etype,
       
   361                                      field.role)
       
   362             if rdef.composite is not None:
       
   363                 if rdef.composite == field.role:
       
   364                     to_be_removed = unlinked_entity
       
   365                 else:
       
   366                     if unlinked_entity.cw_etype in new_value_etypes:
       
   367                         # This is a same-rdef re-parenting: do not remove the entity
       
   368                         continue
       
   369                     to_be_removed = form.edited_entity
       
   370                     self.info('Edition of %s is cancelled (deletion requested)',
       
   371                               to_be_removed)
       
   372                     rqlquery.canceled = True
       
   373                 self.info('Scheduling removal of %s as composite relation '
       
   374                           '%s was removed', to_be_removed, rdef)
       
   375                 form._cw.data['pending_composite_delete'].add(to_be_removed)
   316 
   376 
   317     def handle_inlined_relation(self, form, field, values, origvalues, rqlquery):
   377     def handle_inlined_relation(self, form, field, values, origvalues, rqlquery):
   318         """handle edition for the (rschema, x) relation of the given entity
   378         """handle edition for the (rschema, x) relation of the given entity
   319         """
   379         """
   320         if values:
   380         if values: