Esempio n. 1
0
    def validate(cls, data):
        """Validates data against the model.

        Args:
            data (dict): Data to validate, may be None.

        Returns:
            dict: Validated data or None if `data` is None.

        Raises:
            ModelError: The given data doesn't fit the model.
        """
        if data is None:
            return None
        for key in data:
            if key not in cls.attributes:
                raise ModelError(cls, "no attribute named '%s'" % key)
        validated = {}
        for attr, props in cls.attributes.iteritems():
            # Check required fields and defaults
            try:
                validated[attr] = data[attr]
            except KeyError:
                if 'required' in props:
                    if props['required']:
                        raise ModelError(
                            cls, "'%s' is required but was not defined" % attr)
                elif 'default' in props:
                    validated[attr] = props['default']
            # Check collections
            if 'collection' in props:
                value = data.get(attr, [])
                if not value:
                    value = []
                elif not isinstance(value, list):
                    raise ModelError(
                        cls, "Value supplied for '%s' is not a list: %r" %
                        (attr, value))
                else:
                    for eid in value:
                        try:
                            int(eid)
                        except ValueError:
                            raise ModelError(
                                cls, "Invalid non-integer ID '%s' in '%s'" %
                                (eid, attr))
                validated[attr] = value
            # Check model associations
            elif 'model' in props:
                value = data.get(attr, None)
                if value is not None:
                    try:
                        if int(value) != value:
                            raise ValueError
                    except ValueError:
                        raise ModelError(
                            cls, "Invalid non-integer ID '%s' in '%s'" %
                            (value, attr))
                    validated[attr] = value
        return validated
Esempio n. 2
0
    def _construct_relationships(cls):
        primary_key = None
        for attr, props in cls.attributes.iteritems():
            model_attr_name = cls.name + "." + attr
            if 'collection' in props and 'via' not in props:
                raise ModelError(cls, "%s: collection does not define 'via'" % model_attr_name)
            if 'via' in props and not ('collection' in props or 'model' in props):
                raise ModelError(cls, "%s: defines 'via' property but not 'model' or 'collection'" % model_attr_name)
            if not isinstance(props.get('unique', False), bool):
                raise ModelError(cls, "%s: invalid value for 'unique'" % model_attr_name)
            if not isinstance(props.get('description', ''), basestring):
                raise ModelError(cls, "%s: invalid value for 'description'" % model_attr_name)
            if props.get('primary_key', False):
                if primary_key is not None:
                    raise ModelError(cls, "%s: primary key previously specified as %s" % (model_attr_name, primary_key))
                primary_key = attr

            via = props.get('via', None)
            foreign_cls = props.get('model', props.get('collection', None))
            if not foreign_cls:
                continue
            try:
                if not issubclass(foreign_cls, Model):
                    raise TypeError
            except TypeError:
                raise ModelError(cls, "%s: Invalid foreign model controller: %r" % (model_attr_name, foreign_cls))
            
            forward = (foreign_cls, via)
            reverse = (cls, attr)
            if not via:
                foreign_cls.references.add(reverse)
            else:
                foreign_cls.associations[via] = reverse
                foreign_model_attr_name = foreign_cls.name + "." + via
                try:
                    via_props = foreign_cls.attributes[via]
                except KeyError:
                    raise ModelError(cls, "%s: 'via' references undefined attribute '%s'" % 
                                     (model_attr_name, foreign_model_attr_name))
                via_attr_model = via_props.get('model', via_props.get('collection', None))
                if not via_attr_model:
                    raise ModelError(cls, "%s: 'via' on non-model attribute '%s'" %
                                     (model_attr_name, foreign_model_attr_name))
                if via_attr_model is not cls:
                    raise ModelError(cls, "Attribute '%s' referenced by 'via' in '%s' "
                                     "does not define 'collection' or 'model' of type '%s'" %
                                     (foreign_model_attr_name, attr, cls.name))
                try:
                    existing = cls.associations[attr]
                except KeyError:
                    cls.associations[attr] = forward
                else:
                    if existing != forward:
                        raise ModelError(cls, "%s: conflicting associations: '%s' vs. '%s'" % 
                                         (model_attr_name, existing, forward))
Esempio n. 3
0
 def _associate(self, record, foreign_model, affected, via):
     """Associates a record with another record.
     
     Args:
         record (Record): Record to associate.
         foreign_model (Model): Foreign record's data model.
         affected (list): Identifiers for the records that will be updated to associate with `record`.
         via (str): The name of the associated foreign attribute.
     """
     _heavy_debug("Adding %s to '%s' in %s(eids=%s)", record.eid, via,
                  foreign_model.name, affected)
     if not isinstance(affected, list):
         affected = [affected]
     with self.storage as database:
         for key in affected:
             foreign_record = database.get(key,
                                           table_name=foreign_model.name)
             if not foreign_record:
                 raise ModelError(foreign_model,
                                  "No record with ID '%s'" % key)
             if 'model' in foreign_model.attributes[via]:
                 updated = record.eid
             elif 'collection' in foreign_model.attributes[via]:
                 updated = list(set(foreign_record[via] + [record.eid]))
             else:
                 raise InternalError(
                     "%s.%s has neither 'model' nor 'collection'" %
                     (foreign_model.name, via))
             foreign_model.controller(database).update({via: updated}, key)
Esempio n. 4
0
    def update(self, data, keys):
        """Change recorded data and update associations.
        
        The behavior depends on the type of `keys`:
            * Record.ElementIdentifier: update the record with that element identifier.
            * dict: update all records with attributes matching `keys`.
            * list or tuple: apply update to all records matching the elements of `keys`.
            * ``bool(keys) == False``: raise ValueError.
            
        Invokes the `on_update` callback **after** the data is modified.  If this callback raises
        an exception then the operation is reverted.

        Args:
            data (dict): New data for existing records.
            keys: Fields or element identifiers to match.
        """
        for attr in data:
            if attr not in self.model.attributes:
                raise ModelError(self.model, "no attribute named '%s'" % attr)
        with self.storage as database:
            # Get the list of affected records **before** updating the data so foreign keys are correct
            old_records = self.search(keys)
            database.update(data, keys, table_name=self.model.name)
            changes = {}
            for model in old_records:
                changes[model.eid] = {
                    attr: (model.get(attr), new_value)
                    for attr, new_value in data.iteritems()
                    if not (attr in model and model.get(attr) == new_value)
                }
                for attr, foreign in self.model.associations.iteritems():
                    try:
                        # 'collection' attribute is iterable
                        new_foreign_keys = set(data[attr])
                    except TypeError:
                        # 'model' attribute is not iterable, so make a tuple
                        new_foreign_keys = set((data[attr], ))
                    except KeyError:
                        continue
                    try:
                        # 'collection' attribute is iterable
                        old_foreign_keys = set(model[attr])
                    except TypeError:
                        # 'model' attribute is not iterable, so make a tuple
                        old_foreign_keys = set((model[attr], ))
                    except KeyError:
                        old_foreign_keys = set()
                    foreign_cls, via = foreign
                    added = list(new_foreign_keys - old_foreign_keys)
                    deled = list(old_foreign_keys - new_foreign_keys)
                    if added:
                        self._associate(model, foreign_cls, added, via)
                    if deled:
                        self._disassociate(model, foreign_cls, deled, via)
            updated_records = self.search(keys)
            for model in updated_records:
                model.check_compatibility(model)
                model.on_update(changes[model.eid])
Esempio n. 5
0
 def key_attribute(cls):
     # pylint: disable=attribute-defined-outside-init
     try:
         return cls._key_attribute
     except AttributeError:
         for attr, props in cls.attributes.iteritems():
             if 'primary_key' in props:
                 cls._key_attribute = attr
                 break
         else:
             raise ModelError(cls, "No attribute has the 'primary_key' property set to 'True'")
         return cls._key_attribute
Esempio n. 6
0
 def _populate_attribute(self, model, attr, defaults):
     try:
         props = model.attributes[attr]
     except KeyError:
         raise ModelError(model, "no attribute '%s'" % attr)
     if not defaults or 'default' not in props:
         value = model[attr]
     else:
         value = model.get(attr, props['default'])
     try:
         foreign = props['model']
     except KeyError:
         try:
             foreign = props['collection']
         except KeyError:
             return value
         else:
             return foreign.controller(self.storage).search(value)
     else:
         return foreign.controller(self.storage).one(value)
Esempio n. 7
0
    def unset(self, fields, keys):
        """Unset recorded data fields and update associations.
        
        The behavior depends on the type of `keys`:
            * Record.ElementIdentifier: update the record with that element identifier.
            * dict: update all records with attributes matching `keys`.
            * list or tuple: apply update to all records matching the elements of `keys`.
            * ``bool(keys) == False``: raise ValueError.

        Invokes the `on_update` callback **after** the data is modified.  If this callback raises
        an exception then the operation is reverted.

        Args:
            fields (list): Names of fields to unset.
            keys: Fields or element identifiers to match.
        """
        for attr in fields:
            if attr not in self.model.attributes:
                raise ModelError(self.model, "no attribute named '%s'" % attr)
        with self.storage as database:
            # Get the list of affected records **before** updating the data so foreign keys are correct
            old_records = self.search(keys)
            database.unset(fields, keys, table_name=self.model.name)
            changes = {}
            for model in old_records:
                changes[model.eid] = {
                    attr: (model.get(attr), None)
                    for attr in fields if attr in model
                }
                for attr, foreign in self.model.associations.iteritems():
                    if attr in fields:
                        foreign_cls, via = foreign
                        old_foreign_keys = model.get(attr, None)
                        if old_foreign_keys:
                            self._disassociate(model, foreign_cls,
                                               old_foreign_keys, via)
            updated_records = self.search(keys)
            for model in updated_records:
                model.check_compatibility(model)
                model.on_update(changes[model.eid])