77
88
99if sys .version_info [0 ] == 3 :
10- basestring = str
10+ str_type = str
11+ else :
12+ str_type = basestring
1113
1214
1315class ValidationError (Exception ):
@@ -67,7 +69,7 @@ def serialize(self, value):
6769
6870
6971class String (Field ):
70- base_type = basestring
72+ base_type = str_type
7173 blank_value = ''
7274 min_length = None
7375 max_length = None
@@ -96,7 +98,7 @@ def has_value(self, value):
9698
9799
98100class TrimmedString (String ):
99- base_type = basestring
101+ base_type = str_type
100102 blank_value = ''
101103
102104 def clean (self , value ):
@@ -184,7 +186,7 @@ class Email(Regex):
184186
185187 def clean (self , value ):
186188 # trim any leading/trailing whitespace before validating the email
187- if isinstance (value , basestring ):
189+ if isinstance (value , str_type ):
188190 value = value .strip ()
189191 return super (Email , self ).clean (value )
190192
@@ -345,6 +347,96 @@ def serialize(self, value):
345347 return self .schema_class (data = value ).serialize ()
346348
347349
350+ class ReferenceNotFoundError (Exception ):
351+ """Exception to be raised when a referenced object isn't found."""
352+
353+
354+ class EmbeddedReference (Dict ):
355+ """Represents an object which can be referenced by its ID.
356+
357+ This field allows one to submit a dict of values which will then create
358+ a new instance of the object, or it will update fields of an existing
359+ object if a field representing its ID (called `pk_field`) was included
360+ in the submitted dict.
361+ """
362+
363+ def __init__ (self , object_class , schema_class , pk_field = 'id' , ** kwargs ):
364+ self .object_class = object_class
365+ self .schema_class = schema_class
366+ self .pk_field = pk_field
367+ super (EmbeddedReference , self ).__init__ (schema_class , ** kwargs )
368+
369+ def clean (self , value ):
370+ # Clean the dict first.
371+ value = super (EmbeddedReference , self ).clean (value )
372+
373+ # Then, depending on whether `pk_field` is in the dict of submitted
374+ # values or not, update an existing object or create a new one.
375+ if value and self .pk_field in value :
376+ return self .clean_existing (value )
377+ return self .clean_new (value )
378+
379+ def serialize (self , obj ):
380+ obj_data = self .get_orig_data_from_existing (obj )
381+ serialized = self .schema_class (data = obj_data ).serialize ()
382+ serialized [self .pk_field ] = getattr (obj , self .pk_field )
383+ return serialized
384+
385+ def clean_new (self , value ):
386+ """Return a new object instantiated with cleaned data."""
387+ value = self .schema_class (value ).full_clean ()
388+ return self .object_class (** value )
389+
390+ def clean_existing (self , value ):
391+ """Clean the data and return an existing document with its fields
392+ updated based on the cleaned values.
393+ """
394+ existing_pk = value [self .pk_field ]
395+ try :
396+ obj = self .fetch_existing (existing_pk )
397+ except ReferenceNotFoundError :
398+ raise ValidationError ('Object does not exist.' )
399+ orig_data = self .get_orig_data_from_existing (obj )
400+
401+ # Clean the data (passing the new data dict and the original data to
402+ # the schema).
403+ value = self .schema_class (value , orig_data ).full_clean ()
404+
405+ # Set cleaned data on the object (except for the pk_field).
406+ for field_name , field_value in value .items ():
407+ if field_name != self .pk_field :
408+ setattr (obj , field_name , field_value )
409+
410+ return obj
411+
412+ def fetch_existing (self , pk ):
413+ """Fetch an existing object that corresponds to a given ID.
414+
415+ This needs to be subclassed since, depending on the object class,
416+ the fetching mechanism might be different. See implementations of
417+ SQLAEmbeddedResource and MongoEmbeddedResource for a concrete example
418+ of fetching objects from a relational and non-relational database.
419+
420+ :param str pk: ID of the object that's supposed to exist.
421+ :returns: an instance of the object class.
422+ :raises: ReferenceNotFoundError if the object doesn't exist.
423+ """
424+ raise NotImplementedError # should be subclassed
425+
426+ def get_orig_data_from_existing (self , obj ):
427+ """Return a dictionary of field names and values for a given object.
428+
429+ The values in the dictionary should be in their "cleaned" state (as
430+ in, exactly as they were set on the object, without any serialization).
431+
432+ :param object obj: existing object for which new data is currently
433+ being cleaned.
434+ :returns: dict of fields and values that are currently set on the
435+ object (before the new cleaned data is applied).
436+ """
437+ raise NotImplementedError # should be subclassed
438+
439+
348440class Choices (Field ):
349441 """
350442 A field that accepts the given choices.
@@ -366,7 +458,7 @@ def clean(self, value):
366458 if self .case_insensitive :
367459 choices = {choice .lower (): choice for choice in choices }
368460
369- if not isinstance (value , basestring ):
461+ if not isinstance (value , str_type ):
370462 raise ValidationError (u'Value needs to be a string.' )
371463
372464 if value .lower () not in choices :
0 commit comments