class MediumResource(FootprintResource): content = PickledDictField(attribute='limited_content', null=True, blank=True, default=lambda:{}, readonly=True) style_attributes = fields.ToManyField(StyleAttributeResource, 'style_attributes', full=True, null=True) class Meta(FootprintResource.Meta): always_return_data = True resource_name = 'medium' queryset = LayerStyle.objects.all()
class BuiltFormExampleResource(FootprintResource): content = PickledDictField(attribute='content', null=True, blank=True, default=lambda: {}) class Meta(FootprintResource.Meta): always_return_data = True queryset = BuiltFormExample.objects.all() resource_name = 'built_form_example'
class PresentationMediumResource(FootprintResource): """ The through class between Presentation and Medium, a list of which are loaded by a PresentationResource instance to give the user access to the corresponding Medium and also the important db_entity method, which returns the selected DbEntity interest of the PresentationMedium's db_entity_key """ # The db_entity--We don't expose the DbEntityInterest to the client db_entity = fields.ToOneField(DbEntityResource, attribute='db_entity', null=False) # Return the full Medium medium = fields.ToOneField(MediumResource, attribute='medium', null=False, full=True) # The configuration of items not directly related to the Medium, such as graph labels. These are usually also # editable by the user. configuration = PickledDictField(attribute='configuration', null=True, blank=True, default=lambda: {}) visible_attributes = ListField(attribute='visible_attributes', null=True, blank=True) creator = fields.ToOneField(UserResource, 'creator', full=True, null=True, readonly=True) updater = fields.ToOneField(UserResource, 'updater', full=True, null=True, readonly=True) def dehydrate_medium_context(self, bundle): # Remove data that isn't needed by the API return remove_keys(['attributes']) def hydrate(self, bundle): """ Set the user who created the Layer :param bundle: :return: """ if not bundle.obj.id: bundle.obj.creator = self.resolve_user(bundle.request.GET) bundle.obj.updater = self.resolve_user(bundle.request.GET) return super(PresentationMediumResource, self).hydrate(bundle) def full_hydrate(self, bundle): super(PresentationMediumResource, self).full_hydrate(bundle) if not bundle.data.get( 'id' ) and bundle.obj.db_entity_interest.db_entity.origin_instance: # If cloning, copy the medium_context.attributes config_entity = bundle.obj.db_entity_interest.config_entity origin_db_entity = bundle.obj.db_entity_interest.db_entity.origin_instance presentation_medium = PresentationMedium.objects.get( presentation__config_entity=config_entity, db_entity_key=origin_db_entity.key) bundle.data['medium']['attributes'] = presentation_medium.medium[ 'attributes'] return bundle class Meta(FootprintResource.Meta): resource_name = 'presentation_medium' always_return_data = True queryset = PresentationMedium.objects.all() excludes = ['rendered_medium']
class DbEntityResource( FootprintResource, TagResourceMixin, TimestampResourceMixin, CloneableResourceMixin, PermissionResourceMixin, CategoryResourceMixin): hosts = fields.ListField('hosts', null=True) def get_object_list(self, request): return self.permission_get_object_list(request, super(DbEntityResource, self).get_object_list(request)) config_entity = fields.ToOneField(ConfigEntityResource, 'config_entity', full=False) # This gets sent by the client and is used to set the url. # It is marked readonly so that tastypie doesn't try to find a matching # DbEntity attribute using it. I don't know how to tell tastypie to just map this # value to url upload_id = fields.CharField(null=True, readonly=True) # FeatureClassConfiguration isn't a model class, so we just pickle it feature_class_configuration = PickledDictField(attribute='feature_class_configuration_as_dict', null=True) layer = fields.ToOneField('footprint.main.resources.layer_resources.LayerResource', 'layer', null=True, readonly=True) def hydrate_feature_class_configuration(self, bundle): if bundle.obj.id > 0: del bundle.data['feature_class_configuration'] return bundle # Describes the structure of the Feature class # TODO this should replace feature_fields of the DbEntityResource once we can model # Tastypie's schema object as a non-model class an make this a toOne relationship # That way the schema info will only be downloaded when requested #feature_schema = PickledDictField(attribute='feature_class', null=True, readonly=True) #def dehydrate_feature_schema(self, bundle): # return FeatureResource().dynamic_resource_subclass(db_entity=bundle.obj)().build_schema() # FeatureBehavior is a settable property of DbEntity, since the relationship is actually defined # from FeatureBehavior to DbEntity. feature_behavior = ToOneField(FeatureBehaviorResource, attribute='feature_behavior', null=True) _content_type_ids = None _perm_ids = None def lookup_kwargs_with_identifiers(self, bundle, kwargs): """ Override to remove feature_behavior from the lookup_kwargs, since it is actually defined in reverse--feature_behavior has a db_entity """ return remove_keys( super(DbEntityResource, self).lookup_kwargs_with_identifiers(bundle, kwargs), ['feature_behavior']) def full_hydrate(self, bundle): hydrated_bundle = super(DbEntityResource, self).full_hydrate(bundle) # If new, Ensure the db_entity schema matches that of the config_entity # This happens after all hydration since it depends on two different fields if not hydrated_bundle.obj.id: hydrated_bundle.obj.schema = hydrated_bundle.obj._config_entity.schema() return hydrated_bundle def hydrate(self, bundle): if not bundle.data.get('id'): bundle.obj.creator = self.resolve_user(bundle.request.GET) # Update the key if this is a new instance but the key already is in use while DbEntity.objects.filter(key=bundle.data['key']).count() > 0: bundle.data['key'] = increment_key(bundle.data['key']) # Set this field to 0 so we can track post save progress and know when # the DbEntity is completely ready bundle.obj.setup_percent_complete = 0 bundle.obj.key = bundle.data['key'] bundle.obj.updater = self.resolve_user(bundle.request.GET) return bundle def dehydrate_url(self, bundle): # Use the upload_id to create a source url for the db_entity if bundle.data['url'].startswith('postgres'): # Never show a postgres url return 'Preconfigured Layer' else: return bundle.data['url'] def hydrate_url(self, bundle): # Use the upload_id to create a source url for the db_entity if bundle.data.get('upload_id', False): bundle.data['url'] = 'file:///tmp/%s' % bundle.data['upload_id'] return bundle def dehydrate_feature_class_configuration(self, bundle): return remove_keys(bundle.data['feature_class_configuration'], ['class_attrs', 'related_class_lookup']) if\ bundle.data['feature_class_configuration'] else None class Meta(FootprintResource.Meta): always_return_data = True queryset = DbEntity.objects.filter(deleted=False, setup_percent_complete=100) excludes=['table', 'query', 'hosts', 'group_by'] resource_name= 'db_entity' filtering = { "id": ALL, }
class LayerSelectionResource(DynamicResource): """ An abstract resource class that is subclassed by the resources.py wrapper to match a particular layer_id """ # Writable Fields query_strings = PickledDictField( attribute='query_strings', null=True, blank=True, default=lambda: dict( aggregates_string=None, filter_string=None, group_by_string=None)) bounds = CustomGeometryApiField(attribute='bounds', null=True, blank=True, default=lambda: {}) filter = PickledDictField(attribute='filter', null=True, blank=True) group_bys = PickledDictField(attribute='group_bys', null=True, blank=True) joins = fields.ListField(attribute='joins', null=True, blank=True) aggregates = PickledDictField(attribute='aggregates', null=True, blank=True) # Readonly Fields # Unique id for the Client across all LayerSelections in the system--a combination of the layer id and user id unique_id = fields.CharField(attribute='unique_id', null=False, readonly=True) user = fields.ToOneField(UserResource, 'user', readonly=True, full=False) # The number of features in selected_features. The LayerSelection doesn't need to include Features # or their ids. Features will be downloaded separately via a FeatureResource request with the # LayerSelection unique_id as a parameter features_count = fields.IntegerField(attribute='features_count', readonly=True, default=0) # Profiles result_fields, a result title lookup, and result mapping result_map = PickledDictField(attribute='result_map', null=True, readonly=True) summary_results = fields.ListField(attribute='summary_results', null=True, blank=True, readonly=True, default=[]) summary_fields = fields.ListField(attribute='summary_fields', null=True, blank=True, readonly=True, default=[]) summary_field_title_lookup = PickledDictField( attribute='summary_field_title_lookup', null=True, blank=True, readonly=True) query_sql = fields.CharField(attribute='query_sql', null=True, readonly=True) summary_query_sql = fields.CharField(attribute='summary_query_sql', null=True, readonly=True) # The layer instance is not a LayerSelection field, but a property of the LayerSelection subclass @using_bundle_cache def selection_layer_queryset(bundle): return bundle.obj.__class__.layer selection_layer = fields.ToOneField(LayerResource, attribute=selection_layer_queryset, readonly=True, full=False) selection_extent = CustomGeometryApiField(attribute='selection_extent', null=True, blank=True, default=lambda: {}, readonly=True) # TODO remove filter_by_selection = fields.BooleanField(attribute='filter_by_selection', default=False) # TODO unused selection_options = PickledDictField( attribute='selection_options', null=True, blank=True, default=lambda: dict(constrain_to_bounds=True, constrain_to_previous_results=False)) def full_hydrate(self, bundle, for_list=False): """ Clear the previous bounds or query if the other is sent :param bundle: :return: """ # Remove the computed properties. Some or all will be set bundle.obj.summary_results = None bundle.obj.summary_fields = None bundle.obj.summary_field_title_lookup = None # Call super to populate the bundle.obj bundle = super(LayerSelectionResource, self).full_hydrate(bundle) # Default these to None for attr in ['filter', 'aggregates', 'group_bys', 'joins', 'bounds']: if not bundle.data.get(attr, None): setattr(bundle.obj, attr, None) # Update the features and related derived fields to the queryset bundle.obj.sync_to_query() return bundle def query_data_specified(self, data): return data.get('query', None) def create_subclass(self, params, **kwargs): """ Subclasses the LayerSelectionResource instance's class for the given config_entity and layer. :param params Must contain a 'config_entity__id' and 'layer__id' :return: """ layer = self.resolve_layer(params) config_entity = self.resolve_config_entity(params) logger.debug( "Resolving LayerSelection for config_entity %s, layer %s" % (config_entity.key, layer.db_entity.key)) layer_selection_class = get_or_create_layer_selection_class_for_layer( layer, config_entity) # Have the LayerPublisher create the LayerSelection instance for the user if needed update_or_create_layer_selections_for_layer( layer, users=[self.resolve_user(params)]) if not layer_selection_class: raise Exception( "Layer with db_entity_key %s has no feature_class. Its LayerSelections should not be requested" % layer.db_entity_key) return get_dynamic_resource_class(self.__class__, layer_selection_class) def search_params(self, params): """ :param params :return: """ user = get_user_model().objects.get(username=params['username']) return dict(user__id=user.id) def resolve_config_entity(self, params): """ If the ConfigEntity param is specified it gets precedence. Otherwise use the Layer param :param params: :return: """ if params.get('config_entity__id'): return ConfigEntity.objects.get_subclass( id=params['config_entity__id']) else: return Layer.objects.get(id=params['layer__id']).config_entity def create_layer_from_layer_selection(self, params): """ Used to create a new Layer from the current LayerSelection features :param params: :return: """ # Resolve the source layer from the layer_selection__id source_layer = self.resolve_layer(params) config_entity = source_layer.config_entity db_entity = source_layer.db_entity_interest.db_enitty feature_class = FeatureClassCreator(config_entity, db_entity).dynamic_model_class() layer = Layer.objects.get(presentation__config_entity=config_entity, db_entity_key=db_entity.key) layer_selection = get_or_create_layer_selection_class_for_layer( layer, config_entity, False).objects.all()[0] # TODO no need to do geojson here feature_dict = dict(type="Feature") feature_dicts = map( lambda feature: deep_merge(feature_dict, {"geometry": geojson.loads(feature.wkb_geometry.json)}), layer_selection.selected_features or feature_class.objects.all()) json = dict({"type": "FeatureCollection", "features": feature_dicts}) db_entity_configuration = update_or_create_db_entity( config_entity, **dict(class_scope=FutureScenario, name='Import From Selection Test', key='import_selection_test', url='file://notusingthis')) self.make_geojson_db_entity(config_entity, db_entity_configuration, data=json) class Meta(DynamicResource.Meta): abstract = True filtering = { # There is only one instance per user_id. This should always be specified for GETs "user": ALL_WITH_RELATIONS, "id": ALL } always_return_data = True # We don't want to deliver this, the user only sees and manipulates the bounds excludes = ['geometry'] resource_name = 'layer_selection'
def dynamic_resource_subclass(self, layer_selection=None, db_entity=None, feature_class=None, config_entity=None, metadata=None, params=None, **kwargs): """ Creates the dynamic Feature Resource class by passing in a layer_selection, db_entity, or feature_class :param layer_selection: Required if db_entity or metadata aren't present :param db_entity: Required if layer_selection or metadata aren't present :param metadata: Required along with config_entity if layer_selection or db_entity aren't present :param kwargs: :return: """ feature_class_configuration = None if layer_selection: # Coming in relative to a LayerSelection, which puts us in the context of the LayerSelection's # feature query for this Feature subclass layer = layer_selection.layer # If we pass in a ConfigEntity it means we want to scope the Feature class to its scope. # The ConfigEntity defaults to that of the Layer, but we can override it to be a lower # scope to make sure that we have access to lower DbEntities of performing joins config_entity = config_entity.subclassed if config_entity else layer.config_entity.subclassed logger.debug( "Resolving FeatureResource subclass for layer_selection: {0}, config_entity: {1}" .format(layer_selection.unique_id, config_entity.id)) # Resolve the dynamic Feature class with the given config_entity so that we can access all DbEntities # of the ConfigEntity for joins feature_class = config_entity.db_entity_feature_class( layer.db_entity.key) elif db_entity: # Coming in relative to a DbEntity, meaning we don't care about a particular LayerSelection's # feature query for this Feature subclass config_entity = db_entity.config_entity logger.debug( "Resolving FeatureResource subclass for db_entity: {0}, config_entity: {1}" .format(db_entity.id, config_entity.id)) # Resolve the dynamic Feature class with the given config_entity so that we can access all DbEntities # of the ConfigEntity for joins feature_class = config_entity.db_entity_feature_class( db_entity.key) elif metadata: # Coming in with metadata, meaning this is and uploaded or ArcGis table with no DbEntity yet # We need to construct a FeatureClass from the metadata logger.debug( "Resolving FeatureResource subclass for metadata: {0}, config_entity: {1}" .format(metadata, config_entity.id)) feature_class_creator = FeatureClassCreator(config_entity) feature_class_configuration = feature_class_creator.feature_class_configuration_from_metadata( metadata['schema']) feature_class = FeatureClassCreator( config_entity, feature_class_configuration).dynamic_model_class() if not feature_class_configuration: # If we didn't already ensure all dynamic model classes have been created # This only need to run once to get all dynamic feature subclasses into memory, # in case they are needed by an association, join, or something similar feature_class_creator = FeatureClassCreator.from_dynamic_model_class( feature_class) feature_class_creator.ensure_dynamic_models() logger.debug("Resolving resource for Feature subclass: {0}".format( feature_class)) # Resolve the FeatureResource subclass based on the given Feature subclass # If self is already a subclass, just return self # Else, return a preconfigured subclass or one dynamically created. The latter will probably be the only way in the future. # If not already subclassed is_singleton_feature = issubclass(self.__class__, SingletonFeatureResourceMixin) is_template_feature = self.__class__ == TemplateFeatureResource if self.__class__ in [ FeatureResource, TemplateFeatureResource, FeatureCategoryAttributeResource, FeatureQuantitativeAttributeResource ]: if is_singleton_feature or params.get('is_feature_attribute'): queryset = feature_class.objects.none() elif kwargs.get('method', None) == 'PATCH': # It's possible to PATCH with an active join query. # But we don't want to use a join query when patching queryset = feature_class.objects.all() else: # Get the queryset stored by the layer_selection or an empty query if we don't have a layer_selection queryset = layer_selection.selected_features_or_values if\ layer_selection else \ feature_class.objects.none() if layer_selection and not (is_singleton_feature or kwargs.get( 'query_may_be_empty')) and queryset.count() == 0: raise Exception( "Unexpected empty queryset for layer_selection features: %s" % queryset.query) is_values_queryset = isinstance(queryset, ValuesQuerySet) #returns queryset ordered by the table id queryset = queryset.order_by('id') if is_values_queryset: join_feature_class = layer_selection.create_join_feature_class( ) if is_values_queryset else feature_class logger.info("Created join_feature_class: %s" % join_feature_class) # Force the queryset to our new class so that Tastypie can map the dict results to it queryset.model = join_feature_class return self.__class__.resolve_resource_class( join_feature_class, queryset=queryset, base_resource_class=self.join_feature_resource_class( join_feature_class), additional_fields_dict=dict( # Pass these to the feature resource to help it resolve # field mappings and add related fields (just need for join_feature_class) # Use the layer_selection if it exists since it might have filtered or extra query fields result_field_lookup=(layer_selection or db_entity).result_field_lookup if not metadata else {}, related_field_lookup=( layer_selection or db_entity).related_field_lookup if not metadata else {}, # We use these in the FeatureResource to create a unique id for each join Feature join_model_attributes=layer_selection and layer_selection.result_map.join_model_attributes), is_join_query=True, limit_fields=layer_selection.result_map['result_fields']) else: abstract_feature_resource_class = self.__class__ resource_class = abstract_feature_resource_class.resolve_resource_class( feature_class, queryset=queryset, # Give FeatureResource a reference to the layer_selection additional_fields_dict=merge( dict( # Pass this to the feature resource to help it resolve field mappings result_field_lookup=(layer_selection or db_entity). result_field_lookup if not metadata else {}), dict( # Not sure why it doesn't work to just stick this on the TemplateFeatureResource feature_fields=ListField( attribute='feature_fields', null=True, blank=True, readonly=True), feature_field_title_lookup=PickledDictField( attribute='feature_field_title_lookup', null=True, blank=True, readonly=True), ) if is_template_feature else dict()), for_template=is_template_feature) return resource_class return self