def to_python(self, value): """ Resolve the model from the class name and id """ if value is not None: try: value = dbsafe_decode(value, self.compress) except: # If the value is a definite pickle; and an error is raised in # de-pickling it should be allowed to propogate. if isinstance(value, PickledObject): raise else: # If the value was encoded (not from cache, convert it here back to our # desired format) if value: if isinstance(value, _ObjectWrapper): unwrapped_value = value._obj else: unwrapped_value = value return map_dict_to_dict( lambda key, inner_dict: [ key, map_dict_to_dict( lambda inner_key, model_dict: [ inner_key, self.model_from_class_name_and_pk( model_dict) ], inner_dict) ], unwrapped_value) # Value from cache, leave alone return value
def to_python(self, value): """ Resolve the model from the class name and id """ if value is not None: try: value = dbsafe_decode(value, self.compress) except: # If the value is a definite pickle; and an error is raised in # de-pickling it should be allowed to propogate. if isinstance(value, PickledObject): raise else: # If the value was encoded (not from cache, convert it here back to our # desired format) if value: if isinstance(value, _ObjectWrapper): unwrapped_value = value._obj else: unwrapped_value = value return map_dict_to_dict(lambda key, inner_dict: [key, map_dict_to_dict(lambda inner_key, model_dict: [inner_key, self.model_from_class_name_and_pk(model_dict)], inner_dict )], unwrapped_value) # Value from cache, leave alone return value
def create_result_map(self, related_models=[], map_path_segments={}): """ Given the field_paths of the queryset, returns a ResultMap instance. ResultMap.result_fields is a list of field_paths minus specifically omitted ones-- the parent id and geometry column. ResultMap.title_lookup is a lookup from the field_path to a title appropriate for the user. The generated title uses '.' in place of '__' ResultMap.value_map is a lookup from the field_path to a property path that describes how to convert the value to a more human readable form. This is used to convert instances to a readable label and dates, etc, to a more readable format. :param: related_models: pass the related_models represented in the query results so that unneeded paraent reference fields can be removed from the result fields :param: map_path_segments: An optional dict that matches segments of the field_paths. The value corresponding the key is the name to convert it to for the title. If the value is None it will be eliminated from the path when it is rejoined with '.' """ result_paths = self.model_result_paths(related_models) # Get a mapping of the each result field_path to its field class path along # with the related model of that field, if the field is a ForeignKey or AutoField result_field_path_lookup = self.model_result_field_path_field_lookup( related_models, True) join_models = map(lambda model: full_module_path(model.__base__), related_models) return ResultMap( # Replace '__' with '_x_'. We can't use __ because it confuses tastypie result_fields=map(lambda path: string.replace(path, '__', '_x_'), result_paths), # Create a lookup from field name to title # The only processing we do to the title is to remove the middle path title_lookup=map_to_dict( lambda path: [ # Replace '__' with '_x_'. We can't use __ because it confuses tastypie string.replace(path, '__', '_x_'), # match each segment to map_path_segments or failing that return the segment # remove segments that map to None '__'.join( compact( map( lambda segment: map_path_segments.get( segment, segment), path.split('__')))) ], result_paths), field_lookup=map_dict_to_dict( lambda field_path, tup: [field_path, tup[0]], result_field_path_lookup), related_model_lookup=compact_dict( map_dict_to_dict(lambda field_path, tup: [field_path, tup[1]], result_field_path_lookup)), join_models=join_models, )
def get_db_prep_value(self, value, connection=None, prepared=False): """ Simplify the model instance to an id and class name to avoid expensive pickling """ mapped_dict = map_dict_to_dict(lambda key, inner_dict: [key, map_dict_to_dict(lambda inner_key, model_instance: [inner_key, dict( class_name=resolvable_model_name(model_instance.__class__), pk=self.pk_of_model(model_instance) )] if model_instance else None, # Should never be null, but sometimes i inner_dict )], value) return super(SelectionModelsPickledObjectField, self).get_db_prep_value(mapped_dict, connection, prepared)
def dehydrate(self, bundle): """ Override to add in the __label attributes. These are the human readable version of attributes that represent objects or more appropriate formatting, such as for dates :param bundle: :return: """ # This is a dynamic field create at the time of creating the FeatureResource subclass result_field_lookup = self.result_field_lookup # Map the fields that need special human-readable representation # to labels. We iterate through the result_field_lookup that # the LayerSelection generated for this. # Not the conditional statement is for the "corner" case # of patching Features with joins specified. Patches always # ignore the JoinFeature class and use the regular Feature class, # however the result_field_lookup still contains join fields--ignore them bundle.data['__labels'] = compact_dict(map_dict_to_dict( lambda field_name, field_lambda: [ field_name, field_lambda(getattr(bundle.obj, self.client_field_name(field_name))) ] if '__' not in field_name else None, result_field_lookup )) return super(FeatureResource, self).dehydrate(bundle)
def full_dehydrate(self, bundle, for_list=False): # Convert the dict to the unmanaged join_feature_class instance # Since our JoinFeatureClass uses the client field names, we must map them back to server names # To do the lookup below field_name_lookup = map_to_dict( lambda field: [string.replace(field.name, self.FIELD_RELATION_CLIENT, self.FIELD_RELATION_SERVER), True], join_feature_class._meta.fields) # Map mapping fields from __ to _x_ so Tastypie|Django isn't confused by them dct = map_dict_to_dict( lambda key, value: [self.client_field_name(key), value] if field_name_lookup.get(key) else None, bundle.obj) obj = join_feature_class() for key, value in dct.items(): setattr(obj, key, value) # The object needs a pk to create its resource_uri setattr(obj, 'pk', obj.id) new_bundle = self.build_bundle(obj=obj, request=bundle.request) # This isn't automatically included like for the normal FeatureResource # The unique id must be unique across all joined Feature classes new_bundle.data['the_unique_id'] = '_'.join( [obj.the_unique_id] +\ map(lambda attr: str(getattr(obj, attr)), self.join_model_attributes) ) return super(JoinFeatureResource, self).full_dehydrate(new_bundle, for_list)
def unbundle(bundle): if isinstance(bundle, Bundle): return map_dict_to_dict(lambda attr, value: [attr, unbundle(value)], bundle.data) elif is_list_or_tuple(bundle): return map(lambda value: unbundle(value), bundle) else: return bundle
def full_dehydrate(self, bundle, for_list=False): # Convert the dict to the unmanaged join_feature_class instance # Since our JoinFeatureClass uses the client field names, we must map them back to server names # To do the lookup below field_name_lookup = map_to_dict( lambda field: [ string.replace(field.name, self.FIELD_RELATION_CLIENT, self.FIELD_RELATION_SERVER), True ], join_feature_class._meta.fields) # Map mapping fields from __ to _x_ so Tastypie|Django isn't confused by them dct = map_dict_to_dict( lambda key, value: [self.client_field_name(key), value] if field_name_lookup.get(key) else None, bundle.obj) obj = join_feature_class() for key, value in dct.items(): setattr(obj, key, value) # The object needs a pk to create its resource_uri setattr(obj, 'pk', obj.id) new_bundle = self.build_bundle(obj=obj, request=bundle.request) # This isn't automatically included like for the normal FeatureResource # The unique id must be unique across all joined Feature classes new_bundle.data['the_unique_id'] = '_'.join( [obj.the_unique_id] +\ map(lambda attr: str(getattr(obj, attr)), self.join_model_attributes) ) return super(JoinFeatureResource, self).full_dehydrate(new_bundle, for_list)
def dehydrate(self, bundle): """ Override to add in the __label attributes. These are the human readable version of attributes that represent objects or more appropriate formatting, such as for dates :param bundle: :return: """ # This is a dynamic field create at the time of creating the FeatureResource subclass result_field_lookup = self.result_field_lookup # Map the fields that need special human-readable representation # to labels. We iterate through the result_field_lookup that # the LayerSelection generated for this. # Not the conditional statement is for the "corner" case # of patching Features with joins specified. Patches always # ignore the JoinFeature class and use the regular Feature class, # however the result_field_lookup still contains join fields--ignore them bundle.data['__labels'] = compact_dict( map_dict_to_dict( lambda field_name, field_lambda: [ field_name, field_lambda( getattr(bundle.obj, self.client_field_name(field_name)) ) ] if '__' not in field_name else None, result_field_lookup)) return super(FeatureResource, self).dehydrate(bundle)
def create_result_map(self, related_models=[], map_path_segments={}): """ Given the field_paths of the queryset, returns a ResultMap instance. ResultMap.result_fields is a list of field_paths minus specifically omitted ones-- the parent id and geometry column. ResultMap.title_lookup is a lookup from the field_path to a title appropriate for the user. The generated title uses '.' in place of '__' ResultMap.value_map is a lookup from the field_path to a property path that describes how to convert the value to a more human readable form. This is used to convert instances to a readable label and dates, etc, to a more readable format. :param: related_models: pass the related_models represented in the query results so that unneeded paraent reference fields can be removed from the result fields :param: map_path_segments: An optional dict that matches segments of the field_paths. The value corresponding the key is the name to convert it to for the title. If the value is None it will be eliminated from the path when it is rejoined with '.' """ result_paths = self.model_result_paths(related_models) # Get a mapping of the each result field_path to its field class path along # with the related model of that field, if the field is a ForeignKey or AutoField result_field_path_lookup = self.model_result_field_path_field_lookup(related_models, True) join_models = map(lambda model: full_module_path(model.__base__), related_models) return ResultMap( # Replace '__' with '_x_'. We can't use __ because it confuses tastypie result_fields=map(lambda path: string.replace(path, '__', '_x_'), result_paths), # Create a lookup from field name to title # The only processing we do to the title is to remove the middle path title_lookup=map_to_dict( lambda path: [ # Replace '__' with '_x_'. We can't use __ because it confuses tastypie string.replace(path, '__', '_x_'), # match each segment to map_path_segments or failing that return the segment # remove segments that map to None '__'.join(compact( map( lambda segment: map_path_segments.get(segment, segment), path.split('__') ) )) ], result_paths ), field_lookup=map_dict_to_dict(lambda field_path, tup: [field_path, tup[0]], result_field_path_lookup), related_model_lookup=compact_dict(map_dict_to_dict(lambda field_path, tup: [field_path, tup[1]], result_field_path_lookup)), join_models=join_models, )
def unbundle(bundle): if isinstance(bundle, Bundle): return map_dict_to_dict( lambda attr, value: [attr, unbundle(value)], bundle.data) elif is_list_or_tuple(bundle): return map(lambda value: unbundle(value), bundle) else: return bundle
def related_descriptors(self): """ Returns the existing related field descriptors that were created by create_related_fields and assigned to the dynamic feature class :return: A dict keyed by field name, valued by the ManyToMany field or equivalent """ dynamic_model_class = self.dynamic_model_class() return map_dict_to_dict( lambda field_name, related_field_configuration: [field_name, getattr(dynamic_model_class, field_name)], self.configuration.related_fields or {})
def create_related_fields(self): """ Create ForeignKey and ManyFields for each self.configuration.related_fields :return: """ return map_dict_to_dict( lambda field_name, related_field_configuration: self.create_related_field( field_name, related_field_configuration), self.configuration.related_fields or {})
def owned_db_entities(self, **query_kwargs): """ Returns non-adopted DbEntity instances """ return map( lambda db_entity_interest: db_entity_interest.db_entity, self.owned_db_entity_interests( # add a db_entity__ prefix since the kwargs are desiged for a DbEntity, not the DbEntityInterest **map_dict_to_dict(lambda key, value: ['db_entity__%s' % key, value], query_kwargs) ) )
def owned_db_entities(self, **query_kwargs): """ Returns non-adopted DbEntity instances """ return map( lambda db_entity_interest: db_entity_interest.db_entity, self.owned_db_entity_interests( # add a db_entity__ prefix since the kwargs are desiged for a DbEntity, not the DbEntityInterest **map_dict_to_dict( lambda key, value: ['db_entity__%s' % key, value], query_kwargs)))
def get_db_prep_value(self, value, connection=None, prepared=False): """ Simplify the model instance to an id and class name to avoid expensive pickling """ mapped_dict = map_dict_to_dict( lambda key, inner_dict: [ key, map_dict_to_dict( lambda inner_key, model_instance: [ inner_key, dict(class_name=resolvable_model_name(model_instance. __class__), pk=self.pk_of_model(model_instance)) ] if model_instance else None, # Should never be null, but sometimes i inner_dict) ], value) return super(SelectionModelsPickledObjectField, self).get_db_prep_value(mapped_dict, connection, prepared)
def result_field_lookup(self): """ Returns a lookup that maps some field names to a lambda that describes how to create a human readable form of the field value. This is used by the FeatureResource :return: """ if not self.result_map: # If a query has never been stored, we need to sync to an empty query self.sync_to_query() return compact_dict( map_dict_to_dict(self.field_map, self.result_map.field_lookup))
def result_field_lookup(self): """ Returns a lookup that maps some field names to a lambda that describes how to create a human readable form of the field value. This is used by the FeatureResource :return: """ if not self.result_map: # If a query has never been stored, we need to sync to an empty query self.sync_to_query() return compact_dict(map_dict_to_dict( self.field_map, self.result_map.field_lookup))
def feature(bundle): # Get the current version of the Feature instance feature_instance = feature_class.objects.get(id=bundle.obj.field_dict['id']) # Update it (without saving). This doesn't take care of our related fields feature_instance.__dict__.update(**bundle.obj.field_dict) # Take care of our related fields by settings the Revisionable mixin's _version_field_dict # We instruct the related Resource field to check this _version_field_dict # to grab the versioned value feature_instance._version_field_dict = map_dict_to_dict( lambda key, value: [key, get_versioned_version(bundle.obj, feature_class, key)], feature_class.dynamic_model_class_creator.related_descriptors()) # Set the magic version property so that we grab the right meta data feature_instance._version = bundle.obj return feature_instance
def related_field_lookup(self): """ Only for join queries Looks for any field of type AutoField and returns a key value of the related field, except for the id field. This is not for related_models being joined, but rather for models related to the feature tables, like BuiltForm For example built_form_id=AutoField maps to built_form_id=lambda built_form_id: :param The JoinFeature class :return: """ def related_field_map(field_path, related_model_class_path): """ This helps join feature_classes resolve related models, such as BuiltForms. The join feature class can't do this through the query since we have to use a values() query. Thus the query only contains built_form_id and not built_form with a reference to the built_form instance. The API needs to be able to return a property built_form=uri so we tell it how do do that here. :param field_path: :param related_model_class_path: :return: a key and value, the key is the escaped path (using _x_ for __) with the final _id removed and the value is the related model class. Ex: built_form_id results in [built_form, BuiltForm] for the main model or geographies_[scope_id]_end_state_feature__built_form_id in [geographies_[scope_id]_x_end_state_feature_x_built_form, BuiltForm] """ # For static classes you module name resolution. For dynamic classes rely on current truth # that dynamic classes are all created in the main app, since module name resolution relies # on static module files logger.debug("Resolving %s" % related_model_class_path if\ 'dynamic_subclassing' not in related_model_class_path else\ 'main.%s' % related_model_class_path.split('.')[-1]) related_model_class = resolve_module_attr(related_model_class_path) escaped_field_path = field_path.replace(r'_id$', '').replace('__', '_x_') return [escaped_field_path, related_model_class] return compact_dict( map_dict_to_dict(related_field_map, self.result_map.related_model_lookup))
def related_field_lookup(self): """ Only for join queries Looks for any field of type AutoField and returns a key value of the related field, except for the id field. This is not for related_models being joined, but rather for models related to the feature tables, like BuiltForm For example built_form_id=AutoField maps to built_form_id=lambda built_form_id: :param The JoinFeature class :return: """ def related_field_map(field_path, related_model_class_path): """ This helps join feature_classes resolve related models, such as BuiltForms. The join feature class can't do this through the query since we have to use a values() query. Thus the query only contains built_form_id and not built_form with a reference to the built_form instance. The API needs to be able to return a property built_form=uri so we tell it how do do that here. :param field_path: :param related_model_class_path: :return: a key and value, the key is the escaped path (using _x_ for __) with the final _id removed and the value is the related model class. Ex: built_form_id results in [built_form, BuiltForm] for the main model or geographies_[scope_id]_end_state_feature__built_form_id in [geographies_[scope_id]_x_end_state_feature_x_built_form, BuiltForm] """ # For static classes you module name resolution. For dynamic classes rely on current truth # that dynamic classes are all created in the main app, since module name resolution relies # on static module files logger.debug("Resolving %s" % related_model_class_path if\ 'dynamic_subclassing' not in related_model_class_path else\ 'main.%s' % related_model_class_path.split('.')[-1]) related_model_class = resolve_module_attr(related_model_class_path) escaped_field_path = field_path.replace(r'_id$', '').replace('__', '_x_') return [escaped_field_path, related_model_class] return compact_dict(map_dict_to_dict( related_field_map, self.result_map.related_model_lookup))
def resolve_resource_class(cls, model_class, related_descriptors=None, queryset=None, base_resource_class=None, object_class=None, no_cache=False, use_version_fields=False, only_abstract_resources=True, additional_fields_dict=None, is_join_query=False, limit_fields=None, for_template=False): """ Match the model_class to an existing resource class by iterating through subclasses of self.__class__ If no match occurs generate one if db_entity is specified, else None :param model_class: :param related_descriptors: Optionally provided a dictionary of related descriptors of the model_class to use to create related resource fields in the case of dynamic resource creation. If unspecified, related_descriptors are found using the base resource class's resolve_related_descriptors method :param no_cache: Don't use cached resources. Specifying queryset also prevents the cache being consulted :param use_version_fields: Special case for versioned models. Instructs the resource class creation to create dynamic fields to look in the model_class's Revisionable._version_field_dict to resolve the value of a field before looking at the database (current object) version of the field. :param additional_fields_dict. Optional dict that gives the resource class additional fields with values. This is not for related fields, rather special cases like passing the layer_selection to the FeatureResource. :param for_template: Currently just used for Features, but this indicates a template version of the resource is being sought :param is_join_query: True if the is a join query table or similar. This means that the queryset isn't actually valid yet :return: The matching or created resource """ # Never seek a cached resource if a queryset or no_cache is specified allow_caching = queryset is None and not no_cache cls.logger.info("Resolving Resource Class for model_class %s with query %s and allow_caching %s and only_abstract_resources %s and base_resource_class %s", model_class.__name__, queryset, allow_caching, only_abstract_resources, base_resource_class) if not base_resource_class and allow_caching: # Try to get a cached resource if a base_resource_class is present and we allow caching # of this resource. We never allow caching if the queryset changes from request to request # Instead we subclass the best matching existing resource class resources = FootprintResource.match_existing_resources(model_class) if len(resources) > 1: logging.warn("Too many resources for model class: %s. Resources; %s" % (model_class, resources)) if (len(resources) > 0): cls.logger.info("Found existing resource class %s for model class %s" % (resources[0], model_class)) # Done, return the existing resource return resources[0] # If we don't allow caching or no resource was found if only_abstract_resources: # See if we can match an abstract resource to be our base. # We look for the best matching resource who's model class matches or is a superclass or our model class. base_resource_classes = FootprintResource.match_existing_resources(model_class, only_abstract_resources=True) cls.logger.info("Matching abstract base resource classes: %s" % base_resource_classes) base_resource_class = base_resource_classes[0] if len(base_resource_classes)==1 else base_resource_class else: # Fist try to find a concrete one that we already created concrete_base_resource_classes = FootprintResource.match_existing_resources(model_class, allow_abstract_resources=False) if len(concrete_base_resource_classes)==1: # Found a concrete resource class that was already created for this model # If this isn't a join query we can return this resource class base_resource_class = concrete_base_resource_classes[0] if not is_join_query: return base_resource_class else: # Try to find an abstract one all_base_resource_classes = FootprintResource.match_existing_resources(model_class, only_abstract_resources=True) cls.logger.info("Matching abstract base resource classes: %s" % all_base_resource_classes) base_resource_class = all_base_resource_classes[0] if len(all_base_resource_classes)==1 else base_resource_class if is_join_query: # Join queries must pass in the limited fields from the layer_selection.result_map.result_fields limit_api_fields = limit_fields else: # Non-join queries find their fields from the model_class limit_api_fields = limited_api_fields(model_class, for_template=for_template) resource_class = base_resource_class or cls if base_resource_class: cls.logger.info("Resolved base_resource_class: %s" % base_resource_class) else: cls.logger.info("Could not resolve a base_resource_class. Using resource class %s" % cls) related_descriptors = related_descriptors or \ resource_class.resolve_related_descriptors(model_class) or \ {} cls.logger.info("Creating a resource subclass of %s for model_class %s with the following related descriptors %s" % ( resource_class, model_class, ', '.join(map_dict( lambda related_field_name, related_descriptor: related_field_name, related_descriptors )))) related_fields = merge(additional_fields_dict or {}, map_dict_to_dict( lambda related_field_name, relationship_dict: cls.related_resource_field( related_field_name, merge( dict( # This is passed in, but might be overriden in the relationship_dict use_version_fields=use_version_fields ), # Already a relationship dict relationship_dict if isinstance(relationship_dict, dict) else dict(), # Not a relationship dict, just a descriptor, wrap it dict(descriptor=relationship_dict) if not isinstance(relationship_dict, dict) else dict(), ), # Always allow cached related resource classes. # I think this is always safe no_cache=False), related_descriptors)) meta = merge( dict(fields=limit_api_fields) if limit_api_fields else dict(excludes=(resource_class.Meta.excludes if hasattr( resource_class.Meta, 'excludes') else []) + cls.dynamic_excludes( model_class)), dict(queryset=queryset, object_class=object_class) if queryset else dict()) return get_dynamic_resource_class( resource_class, model_class, # Created related fields from the related_descriptors fields=related_fields, # Optionally set the fields and queryset on the Meta class meta_fields=meta )