0001"""
0002SQLObject-wrapping schema
0003"""
0004import sqlobject
0005import schema
0006import validators
0007from api import Invalid
0008from declarative import classinstancemethod
0009
0010class SQLSchema(schema.Schema):
0011
0012    """
0013    SQLSchema objects are FormEncode schemas that are attached to
0014    specific instances or classes.
0015    
0016    In ``.from_python(object)`` these schemas serialize SQLObject
0017    instances to dictionaries of values, or to empty dictionaries
0018    (when serializing a class).  The object passed in should either be
0019    None (new or default object) or the object to edit.
0020    
0021    In ``.to_python`` these either create new objects (when no ``id``
0022    field is present) or edit an object by the included id.  The
0023    returned value is the created object.
0024
0025    SQLObject validators are applied to the input, as is notNone
0026    restrictions.  Also column restrictions and defaults are applied.
0027    Note that you can add extra fields to this schema, and they will
0028    be applied before the SQLObject validators and restrictions.  This
0029    means you can use, for instance, ``validators.DateConverter()``
0030    (assigning it to the same name as the SQLObject class's date
0031    column) to have this serialize date columns to/from strings.
0032
0033    You can override ``update_object`` to change the actual
0034    instantiation.
0035
0036    The basic idea is that a SQLSchema 'wraps' a class or instance
0037    (most typically a class).  So it would look like::
0038
0039        class PersonSchema(SQLSchema):
0040            wrap = Person
0041
0042        ps = PersonSchema()
0043        form_defaults = ps.from_python(None)
0044        new_object = ps.to_python(form_input)
0045        form_defaults = ps.from_python(aPerson)
0046        edited_person = ps.to_python(edited_form_input)
0047
0048    To override the encoding and decoding, use ``update_object`` and
0049    ``get_current``.  In this example, lets say that we take a single
0050    name field instead of a first_name and last_name (which is what
0051    the database has)::
0052
0053        class PersonSchema(SQLSchema):
0054            wrap = Person
0055            
0056            def update_object(self, columns, extra, state):
0057                name = extra.pop('name')
0058                fname, lname = name.split(None, 1)
0059                columns['first_name'] = fname
0060                columns['last_name'] = lname
0061                return super(PersonSchema).update_object(
0062                    columns, extra, state)
0063
0064            def get_current(self, obj, state):
0065                value = super(PersonSchema).get_current(obj, state)
0066                value['name'] = '%(first_name)s %(last_name)s' % value
0067                del value['first_name']
0068                del value['last_name']
0069            
0070    """
0071
0072
0073    # This is the object that gets wrapped, either a class (this is a
0074    # creating schema) or an instance (this is an updating schema):
0075    wrap = None
0076
0077    # If this is true, then to_python calls that include an id
0078    # will cause an object update if this schema wraps a class.
0079    allow_edit = True
0080
0081    # If this is true, then the IDs will be signed; you must also
0082    # give a secret if that is true.
0083    sign_id = False
0084    # This can be any object with a __str__ method:
0085    # @@: Should we just take a signer validator?
0086    secret = None
0087
0088    # The SQLObject schema will pick these up:
0089    allow_extra_fields = True
0090    filter_extra_fields = False
0091    ignore_key_missing = True
0092
0093    messages = {
0094        'invalidID': 'The id is not valid: %(error)s',
0095        'badID': 'The id %(value)r did not match the expected id',
0096        'notNone': 'You may not provide None for that value',
0097        }
0098
0099    def __initargs__(self, new_attrs):
0100        schema.Schema.__initargs__(self, new_attrs)
0101        if self.sign_id:
0102            self._signer = validators.SignedString(secret=self.secret)
0103
0104    def is_empty(self, value):
0105        # For this class, None has special meaning, and isn't empty
0106        return False
0107
0108    #@classinstancemethod
0109    def object(self, cls):
0110        """
0111        Returns the object this schema wraps
0112        """
0113        me = self or cls
0114        assert me.wrap is not None, (
0115            "You must give %s an object to wrap" % me)
0116        if isinstance(me.wrap, (list, tuple)):
0117            # Special lazy case...
0118            assert len(me.wrap) == 2, (
0119                "Lists/tuples must be (class, obj_id); not %r"
0120                % me.wrap)
0121            return me.wrap[0].get(me.wrap[1])
0122        else:
0123            return me.wrap
0124
0125    object = classinstancemethod(object)
0126
0127    #@classinstancemethod
0128    def instance(self, cls):
0129        """
0130        Returns true if we wrap a SQLObject instance, false if
0131        we wrap a SQLObject class
0132        """
0133        me = self or cls
0134        assert me.wrap is not None, (
0135            "You must give %s an object to wrap" % me)
0136        if isinstance(me.wrap, (list, tuple)):
0137            return True
0138        elif isinstance(me.wrap, sqlobject.SQLObject):
0139            return True
0140        else:
0141            return False
0142
0143    instance = classinstancemethod(instance)
0144
0145    def _from_python(self, obj, state):
0146        if obj is None:
0147            obj = self.object()
0148        if isinstance(obj, sqlobject.SQLObject):
0149            value_dict = self.get_current(obj, state)
0150        else:
0151            value_dict = self.get_defaults(obj, state)
0152        result = schema.Schema._from_python(self, value_dict, state)
0153        if 'id' in result and self.sign_id:
0154            result['id'] = self._signer.from_python(result['id'])
0155        return result
0156
0157    def _to_python(self, value_dict, state):
0158        value_dict = value_dict.copy()
0159        add_values = {}
0160        if self.instance() or value_dict.get('id'):
0161            if not self.instance() and not self.allow_edit:
0162                raise Invalid(self.message('editNotAllowed', state, value=value_dict['id']),
0163                              value_dict['id'], state)
0164            if 'id' not in value_dict:
0165                raise Invalid(self.message('missingValue', state),
0166                              None, state)
0167            id = value_dict.pop('id')
0168            if self.sign_id:
0169                id = self._signer.to_python(id)
0170            try:
0171                id = self.object().sqlmeta.idType(id)
0172            except ValueError, e:
0173                raise Invalid(self.message('invalidID', state, error=e),
0174                              id, state)
0175            add_values['id'] = id
0176        elif 'id' in value_dict and not value_dict['id']:
0177            # Empty id, which is okay and means we are creating
0178            # an object
0179            del value_dict['id']
0180        result = schema.Schema._to_python(self, value_dict, state)
0181        result, extra = self._to_python_dictionary(result, state)
0182        result.update(add_values)
0183        return self.update_object(result, extra, state)
0184
0185    def update_object(self, columns, extra_fields, state):
0186        """
0187        Actually do the action, like create or update an object.
0188        """
0189        if extra_fields:
0190            errors = {}
0191            for key in extra_fields.keys():
0192                errors[key] = Invalid(
0193                    self.message('notExpected', state, name=repr(key)),
0194                    columns, state)
0195            raise Invalid(
0196                schema.format_compound_error(errors),
0197                columns, state,
0198                error_dict=errors)
0199        obj = self.object()
0200        create = False
0201        if self.instance():
0202            if obj.id != columns['id']:
0203                raise Invalid(self.message('badID', state, value=columns['id']),
0204                              columns['id'], state)
0205            del columns['id']
0206        elif 'id' in columns:
0207            obj = obj.get(columns['id'])
0208            del columns['id']
0209        else:
0210            create = True
0211        if create:
0212            obj = obj(**columns)
0213        else:
0214            obj.set(**columns)
0215        return obj
0216
0217    def _to_python_dictionary(self, value_dict, state):
0218        obj = self.object()
0219        sqlmeta = obj.sqlmeta
0220        columns = sqlmeta.columns
0221        extra = value_dict.copy()
0222        found = []
0223        for name, value in value_dict.items():
0224            if name not in columns:
0225                continue
0226            found.append(name)
0227            del extra[name]
0228            if columns[name].validator:
0229                # We throw the result away, but let the exception
0230                # get through
0231                columns[name].validator.to_python(value, state)
0232            if columns[name].notNone and value is None:
0233                # This isn't present in the validator information
0234                exc = Invalid(self.message('notNone', state),
0235                              value, state)
0236                raise Invalid(
0237                    '%s: %s' % (name, exc),
0238                    value_dict, state,
0239                    error_dict={name: exc})
0240
0241        if not isinstance(obj, sqlobject.SQLObject):
0242            for name, column in columns.items():
0243                if (name not in found
0244                    and column.default is sqlobject.col.NoDefault):
0245                    exc = Invalid(self.message('missingValue', state),
0246                                  value_dict, state)
0247                    raise Invalid(
0248                        '%s: %s' % (name, exc),
0249                        value_dict, state,
0250                        error_dict={name: exc})
0251        for key in extra:
0252            del value_dict[key]
0253        return value_dict, extra
0254
0255    def get_current(self, obj, state):
0256        if hasattr(obj.sqlmeta, 'asDict'):
0257            # Added in 0.8
0258            result = obj.sqlmeta.asDict()
0259        else:
0260            result = {}
0261            for key in obj.sqlmeta.columns:
0262                result[key] = getattr(obj, key)
0263            result['id'] = obj.id
0264        return result
0265
0266    def get_defaults(self, soClass, state):
0267        # @@: Should this take into account column defaults?
0268        # Yes!  Hmm... need to fix.
0269        return {}