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
0074
0075 wrap = None
0076
0077
0078
0079 allow_edit = True
0080
0081
0082
0083 sign_id = False
0084
0085
0086 secret = None
0087
0088
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
0106 return False
0107
0108
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
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
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
0178
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
0230
0231 columns[name].validator.to_python(value, state)
0232 if columns[name].notNone and value is None:
0233
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
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
0268
0269 return {}