Ejemplo n.º 1
0
 def related_field_paths_filter(join_model):
     related_filtered_fields = limited_api_fields(join_model)
     return related_field_paths(
         feature_class.objects,
         join_model,
         for_value_queryset=is_join_queryset,
         field_names=related_filtered_fields,
         excludes=Feature.API_EXCLUDED_FIELD_NAMES if not filtered_fields else [])
    def dynamic_join_model_class(self, join_models, related_field_names):
        """
            Creates an unmanaged subclass of the feature class with extra fields to represent the
            the fields of the join_models. This also adds fields for any fields specified in the
            related_model_lookup. This is not for join models but ForeignKeys such as BuiltForm
            These latter fields must be specified explicitly because the main model and join models
            can't populate their foreign keys from the query because the query has to be
            a ValuesQuerySet in order to do the join. So we create id versions of the fields here
            (e.g. built_form_id) which the query can fill and then use that to manually
            set the foreign key reference in the Tastypie resource.
            :param join_models: Other feature models whose attributes should be added to the subclass
            :param related_field_names: List of field names of foreign key id fields (AutoFields)

        """
        main_model_class = self.dynamic_model_class()
        manager = main_model_class.objects
        # Exclude the following field types. Since the base Feature defines an id we'll still get that, which we want
        exclude_field_types = (ForeignKey, ToManyField, OneToOneField, GeometryField)
        all_field_paths_to_fields = merge(
            # Create fields to represented foreign key id fields
            # Our query fetches these ids since it can't fetch related objects (since its a values() query)
            map_to_dict(
                lambda field_name: [field_name.replace('__', '_x_'),
                                    IntegerField(field_name.replace('__', '_x_'), null=True)],
                related_field_names
            ),
            # The join fields for each joined related model
            *map(
                lambda related_model: related_field_paths_to_fields(
                    manager,
                    related_model,
                    exclude_field_types=exclude_field_types,
                    fields=limited_api_fields(related_model),
                    separator='_x_'),
                join_models)
        )

        abstract_feature_class = resolve_module_attr(self.configuration.abstract_class_name)
        # Make sure the class name is unique to the related models and the given ConfigEntity
        related_models_unique_id = '_'.join(sorted(map(lambda related_model: related_model.__name__, join_models), ))
        dynamic_model_clazz = dynamic_model_class(
            main_model_class,
            self.db_entity.schema,
            self.db_entity.table,
            class_name="{0}{1}{2}{3}Join".format(
                abstract_feature_class.__name__,
                self.db_entity.id,
                self.config_entity.id,
                related_models_unique_id),
            fields=all_field_paths_to_fields,
            class_attrs=self.configuration.class_attrs or {},
            related_class_lookup=self.configuration.related_class_lookup or {},
            is_managed=False,
            cacheable=False)
        logger.info("Created dynamic join model class %s" % dynamic_model_clazz)
        logger.debug("Created with model fields %s" % map(lambda field: field.name, dynamic_model_clazz._meta.fields))
        logger.debug("Created with related and join fields %s" % all_field_paths_to_fields)
        return dynamic_model_clazz
Ejemplo n.º 3
0
    def values_query_set(self, query_set=None):
        """
            Returns a ValuesQuerySet based on the query_set and the related_models.
            The given query_set returns Feature class instances. We want the dictionaries with the related models
            joined in
            :param query_set. Optional instance QuerySet to use as the basis for producing the ValueQuerySet.
            If omitted then create_query_set is used to generate it
        """

        feature_class = self.feature_class
        query_set = query_set or self.create_query_set(feature_class.objects)

        # Combine the fields of the join models
        join_models = self.resolve_join_models()
        filtered_fields = limited_api_fields(feature_class)
        is_join_queryset = self.joins and len(self.joins) > 0

        # Limit the returned fields for the main model and related models
        # based on the api_include property on the model. If that property
        # is null, return everything except the wkb_geometry

        def related_field_paths_filter(join_model):
            related_filtered_fields = limited_api_fields(join_model)
            return related_field_paths(
                feature_class.objects,
                join_model,
                for_value_queryset=is_join_queryset,
                field_names=related_filtered_fields,
                excludes=Feature.API_EXCLUDED_FIELD_NAMES if not filtered_fields else [])

        all_field_paths = \
            model_field_paths(
                feature_class,
                for_value_queryset=is_join_queryset,
                field_names=filtered_fields,
                excludes=Feature.API_EXCLUDED_FIELD_NAMES if not filtered_fields else []) + \
            flat_map(
                related_field_paths_filter,
                join_models)

        # Convert the queryset to values with all main and related field paths
        # This makes us lose the model instance
        indistinct_query_set = query_set.values(*all_field_paths)

        # We need the result_map at this point so create it and save the results
        # This is a side effect but all callers need it immediately afterward anyway
        self.result_map = self.create_result_map(indistinct_query_set)

        # Prevent duplicates that occur by joining two feature tables via a primary geography table,
        # since there can be many primary geography features per main feature

        # To limit duplicate checks find the foreign key to each related model in the geographies join table
        # Remove the _id portion since the actual Django field always omits it
        # TODO We can use the following line instead once someone verifies that the only possible values here are
        # _id for non primary geographies and __id for primary geographies
        # lambda attr[:-len('_id')] if not attr.endswith('__id') else attr
        # This corrects the problem that the replace might not replace only at the end of the string
        related_foreign_key_attributes = map(
            lambda attr: attr.replace('_id', '') if not attr.endswith('__id') else attr,
            self.result_map.django_join_model_attributes
        )
        # If we don't have joins, no need for distinct.
        if not is_join_queryset:
            return indistinct_query_set

        # Limit the distinct clause to the pk and the join foreign keys.
        key_attributes = ['pk'] + related_foreign_key_attributes
        return indistinct_query_set.distinct(*key_attributes)
Ejemplo n.º 4
0
    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
        )
Ejemplo n.º 5
0
    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
        )