diff -r 000000000000 -r b97547f5f1fa web/views/editcontroller.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/web/views/editcontroller.py Wed Nov 05 15:52:50 2008 +0100 @@ -0,0 +1,347 @@ +"""The edit controller, handling form submitting. + +:organization: Logilab +:copyright: 2001-2008 LOGILAB S.A. (Paris, FRANCE), all rights reserved. +:contact: http://www.logilab.fr/ -- mailto:contact@logilab.fr +""" +__docformat__ = "restructuredtext en" +from decimal import Decimal + +from rql.utils import rqlvar_maker + +from cubicweb import Binary, ValidationError, typed_eid +from cubicweb.web import INTERNAL_FIELD_VALUE, RequestError, NothingToEdit +from cubicweb.web.controller import parse_relations_descr +from cubicweb.web.views.basecontrollers import ViewController + + +class ToDoLater(Exception): + """exception used in the edit controller to indicate that a relation + can't be handled right now and have to be handled later + """ + +class EditController(ViewController): + id = 'edit' + + def publish(self, rset=None, fromjson=False): + """edit / create / copy / delete entity / relations""" + self.fromjson = fromjson + req = self.req + form = req.form + for key in form: + # There should be 0 or 1 action + if key.startswith('__action_'): + cbname = key[1:] + try: + callback = getattr(self, cbname) + except AttributeError: + raise ValidationError(None, + {None: req._('invalid action %r' % key)}) + else: + return callback() + self._default_publish() + self.reset() + + def _default_publish(self): + req = self.req + form = req.form + # no specific action, generic edition + self._to_create = req.data['eidmap'] = {} + self._pending_relations = [] + todelete = self.req.get_pending_deletes() + toinsert = self.req.get_pending_inserts() + try: + methodname = form.pop('__method', None) + for eid in req.edited_eids(): + formparams = req.extract_entity_params(eid) + if methodname is not None: + entity = req.eid_rset(eid).get_entity(0, 0) + method = getattr(entity, methodname) + method(formparams) + eid = self.edit_entity(formparams) + except (RequestError, NothingToEdit): + if '__linkto' in form and 'eid' in form: + self.execute_linkto() + elif not ('__delete' in form or '__insert' in form or todelete or toinsert): + raise ValidationError(None, {None: req._('nothing to edit')}) + # handle relations in newly created entities + if self._pending_relations: + for rschema, formparams, x, entity in self._pending_relations: + self.handle_relation(rschema, formparams, x, entity, True) + + # XXX this processes *all* pending operations of *all* entities + if form.has_key('__delete'): + todelete += req.list_form_param('__delete', form, pop=True) + if todelete: + self.delete_relations(parse_relations_descr(todelete)) + if form.has_key('__insert'): + toinsert = req.list_form_param('__insert', form, pop=True) + if toinsert: + self.insert_relations(parse_relations_descr(toinsert)) + self.req.remove_pending_operations() + + def edit_entity(self, formparams, multiple=False): + """edit / create / copy an entity and return its eid""" + etype = formparams['__type'] + entity = self.vreg.etype_class(etype)(self.req, None, None) + entity.eid = eid = self._get_eid(formparams['eid']) + edited = self.req.form.get('__maineid') == formparams['eid'] + # let a chance to do some entity specific stuff. + entity.pre_web_edit() + # create a rql query from parameters + self.relations = [] + self.restrictions = [] + # process inlined relations at the same time as attributes + # this is required by some external source such as the svn source which + # needs some information provided by those inlined relation. Moreover + # this will generate less write queries. + for rschema in entity.e_schema.subject_relations(): + if rschema.is_final(): + self.handle_attribute(entity, rschema, formparams) + elif rschema.inlined: + self.handle_inlined_relation(rschema, formparams, entity) + execute = self.req.execute + if eid is None: # creation or copy + if self.relations: + rql = 'INSERT %s X: %s' % (etype, ','.join(self.relations)) + else: + rql = 'INSERT %s X' % etype + if self.restrictions: + rql += ' WHERE %s' % ','.join(self.restrictions) + try: + # get the new entity (in some cases, the type might have + # changed as for the File --> Image mutation) + entity = execute(rql, formparams).get_entity(0, 0) + eid = entity.eid + except ValidationError, ex: + # ex.entity may be an int or an entity instance + self._to_create[formparams['eid']] = ex.entity + if self.fromjson: + ex.entity = formparams['eid'] + raise + self._to_create[formparams['eid']] = eid + elif self.relations: # edition of an existant entity + varmaker = rqlvar_maker() + var = varmaker.next() + while var in formparams: + var = varmaker.next() + rql = 'SET %s WHERE X eid %%(%s)s' % (','.join(self.relations), var) + if self.restrictions: + rql += ', %s' % ','.join(self.restrictions) + formparams[var] = eid + execute(rql, formparams) + for rschema in entity.e_schema.subject_relations(): + if rschema.is_final() or rschema.inlined: + continue + self.handle_relation(rschema, formparams, 'subject', entity) + for rschema in entity.e_schema.object_relations(): + if rschema.is_final(): + continue + self.handle_relation(rschema, formparams, 'object', entity) + if edited: + self.notify_edited(entity) + if formparams.has_key('__delete'): + todelete = self.req.list_form_param('__delete', formparams, pop=True) + self.delete_relations(parse_relations_descr(todelete)) + if formparams.has_key('__cloned_eid'): + entity.copy_relations(formparams['__cloned_eid']) + if formparams.has_key('__insert'): + toinsert = self.req.list_form_param('__insert', formparams, pop=True) + self.insert_relations(parse_relations_descr(toinsert)) + if edited: # only execute linkto for the main entity + self.execute_linkto(eid) + return eid + + def _action_apply(self): + self._default_publish() + self.reset() + + def _action_cancel(self): + errorurl = self.req.form.get('__errorurl') + if errorurl: + self.req.cancel_edition(errorurl) + return self.reset() + + def _action_delete(self): + self.delete_entities(self.req.edited_eids(withtype=True)) + return self.reset() + + def _needs_edition(self, rtype, formparams): + """returns True and and the new value if `rtype` was edited""" + editkey = 'edits-%s' % rtype + if not editkey in formparams: + return False, None # not edited + value = formparams.get(rtype) or None + if (formparams.get(editkey) or None) == value: + return False, None # not modified + if value == INTERNAL_FIELD_VALUE: + value = None + return True, value + + def handle_attribute(self, entity, rschema, formparams): + """append to `relations` part of the rql query to edit the + attribute described by the given schema if necessary + """ + attr = rschema.type + edition_needed, value = self._needs_edition(attr, formparams) + if not edition_needed: + return + # test if entity class defines a special handler for this attribute + custom_edit = getattr(entity, 'custom_%s_edit' % attr, None) + if custom_edit: + custom_edit(formparams, value, self.relations) + return + attrtype = rschema.objects(entity.e_schema)[0].type + # on checkbox or selection, the field may not be in params + if attrtype == 'Boolean': + value = bool(value) + elif attrtype == 'Decimal': + value = Decimal(value) + elif attrtype == 'Bytes': + # if it is a file, transport it using a Binary (StringIO) + if formparams.has_key('__%s_detach' % attr): + # drop current file value + value = None + # no need to check value when nor explicit detach nor new file submitted, + # since it will think the attribut is not modified + elif isinstance(value, unicode): + # file modified using a text widget + value = Binary(value.encode(entity.text_encoding(attr))) + else: + # (filename, mimetype, stream) + val = Binary(value[2].read()) + if not val.getvalue(): # usually an unexistant file + value = None + else: + # XXX suppose a File compatible schema + val.filename = value[0] + if entity.has_format(attr): + key = '%s_format' % attr + formparams[key] = value[1] + self.relations.append('X %s_format %%(%s)s' + % (attr, key)) + if entity.e_schema.has_subject_relation('name') \ + and not formparams.get('name'): + formparams['name'] = value[0] + self.relations.append('X name %(name)s') + value = val + elif value is not None: + if attrtype in ('Date', 'Datetime', 'Time'): + try: + value = self.parse_datetime(value, attrtype) + except ValueError: + raise ValidationError(entity.eid, + {attr: self.req._("invalid date")}) + elif attrtype == 'Password': + # check confirmation (see PasswordWidget for confirmation field name) + confirmval = formparams.get(attr + '-confirm') + if confirmval != value: + raise ValidationError(entity.eid, + {attr: self.req._("password and confirmation don't match")}) + # password should *always* be utf8 encoded + value = value.encode('UTF8') + else: + # strip strings + value = value.strip() + elif attrtype == 'Password': + # skip None password + return # unset password + formparams[attr] = value + self.relations.append('X %s %%(%s)s' % (attr, attr)) + + def _relation_values(self, rschema, formparams, x, entity, late=False): + """handle edition for the (rschema, x) relation of the given entity + """ + rtype = rschema.type + editkey = 'edit%s-%s' % (x[0], rtype) + if not editkey in formparams: + return # not edited + try: + values = self._linked_eids(self.req.list_form_param(rtype, formparams), late) + except ToDoLater: + self._pending_relations.append((rschema, formparams, x, entity)) + return + origvalues = set(typed_eid(eid) for eid in self.req.list_form_param(editkey, formparams)) + return values, origvalues + + def handle_inlined_relation(self, rschema, formparams, entity, late=False): + """handle edition for the (rschema, x) relation of the given entity + """ + try: + values, origvalues = self._relation_values(rschema, formparams, + 'subject', entity, late) + except TypeError: + return # not edited / to do later + if values == origvalues: + return # not modified + attr = str(rschema) + if values: + formparams[attr] = iter(values).next() + self.relations.append('X %s %s' % (attr, attr.upper())) + self.restrictions.append('%s eid %%(%s)s' % (attr.upper(), attr)) + elif entity.has_eid(): + self.handle_relation(rschema, formparams, 'subject', entity, late) + + def handle_relation(self, rschema, formparams, x, entity, late=False): + """handle edition for the (rschema, x) relation of the given entity + """ + try: + values, origvalues = self._relation_values(rschema, formparams, x, + entity, late) + except TypeError: + return # not edited / to do later + etype = entity.e_schema + if values == origvalues: + return # not modified + if x == 'subject': + desttype = rschema.objects(etype)[0] + card = rschema.rproperty(etype, desttype, 'cardinality')[0] + subjvar, objvar = 'X', 'Y' + else: + desttype = rschema.subjects(etype)[0] + card = rschema.rproperty(desttype, etype, 'cardinality')[1] + subjvar, objvar = 'Y', 'X' + eid = entity.eid + if x == 'object' or not rschema.inlined or not values: + # this is not an inlined relation or no values specified, + # explicty remove relations + for reid in origvalues.difference(values): + rql = 'DELETE %s %s %s WHERE X eid %%(x)s, Y eid %%(y)s' % ( + subjvar, rschema, objvar) + self.req.execute(rql, {'x': eid, 'y': reid}, ('x', 'y')) + rql = 'SET %s %s %s WHERE X eid %%(x)s, Y eid %%(y)s' % ( + subjvar, rschema, objvar) + for reid in values.difference(origvalues): + self.req.execute(rql, {'x': eid, 'y': reid}, ('x', 'y')) + + def _get_eid(self, eid): + # should be either an int (existant entity) or a variable (to be + # created entity) + assert eid or eid == 0, repr(eid) # 0 is a valid eid + try: + return typed_eid(eid) + except ValueError: + try: + return self._to_create[eid] + except KeyError: + self._to_create[eid] = None + return None + + def _linked_eids(self, eids, late=False): + """return a list of eids if they are all known, else raise ToDoLater + """ + result = set() + for eid in eids: + if not eid: # AutoCompletionWidget + continue + eid = self._get_eid(eid) + if eid is None: + if not late: + raise ToDoLater() + # eid is still None while it's already a late call + # this mean that the associated entity has not been created + raise Exception('duh') + result.add(eid) + return result + +