Пример #1
0
 def test_get_filterset_for_model(self):
     """
     Test the util function `get_filterset_for_model` returns the appropriate FilterSet, if model (as dotted string or class) provided.
     """
     self.assertEqual(get_filterset_for_model("dcim.device"),
                      DeviceFilterSet)
     self.assertEqual(get_filterset_for_model(Device), DeviceFilterSet)
     self.assertEqual(get_filterset_for_model("dcim.site"), SiteFilterSet)
     self.assertEqual(get_filterset_for_model(Site), SiteFilterSet)
Пример #2
0
    def _validate_relationship_filter_restriction(self):
        """Validate relationship association do not violate filter restrictions"""
        sides = []

        if self.relationship.destination_filter:
            sides.append("destination")

        if self.relationship.source_filter:
            sides.append("source")

        for side_name in sides:
            side = getattr(self, side_name)  # destination / source
            side_filter = getattr(self.relationship, f"{side_name}_filter")

            filterset_class = get_filterset_for_model(side.__class__)
            filterset = filterset_class(side_filter,
                                        side.__class__.objects.all())
            queryset = filterset.qs.filter(id=side.id)

            if queryset.exists() is False:
                raise ValidationError({
                    side_name:
                    (f"{side} violates {self.relationship} {side_name}_filter restriction"
                     )
                })
Пример #3
0
    def _set_object_classes(self, model):
        """
        Given the `content_type` for this group, dynamically map object classes to this instance.
        Protocol for return values:

        - True: Model and object classes mapped.
        - False: Model not yet mapped (likely because of no `content_type`)
        """

        # If object classes have already been mapped, return True.
        if getattr(self, "_object_classes_mapped", False):
            return True

        # Try to set the object classes for this model.
        try:
            self.filterset_class = get_filterset_for_model(model)
            self.filterform_class = get_form_for_model(model, form_prefix="Filter")
            self.form_class = get_form_for_model(model)
        # We expect this to happen on new instances or in any case where `model` was not properly
        # available to the caller, so always fail closed.
        except TypeError:
            logger.debug("Failed to map object classes for model %s", model)
            self.filterset_class = None
            self.filterform_class = None
            self.form_class = None
            self._object_classes_mapped = False
        else:
            self._object_classes_mapped = True

        return self._object_classes_mapped
Пример #4
0
    def get_relationships(self, include_hidden=False):
        """
        Return a dictionary of queryset for all custom relationships

        Returns:
            response {
                "source": {
                    <relationship #1>: <queryset #1>,
                    <relationship #2>: <queryset #2>,
                },
                "destination": {
                    <relationship #3>: <queryset #3>,
                    <relationship #4>: <queryset #4>,
                },
            }
        """
        src_relationships, dst_relationships = Relationship.objects.get_for_model(
            self)
        content_type = ContentType.objects.get_for_model(self)

        sides = {
            RelationshipSideChoices.SIDE_SOURCE: src_relationships,
            RelationshipSideChoices.SIDE_DESTINATION: dst_relationships,
        }

        resp = {
            RelationshipSideChoices.SIDE_SOURCE: OrderedDict(),
            RelationshipSideChoices.SIDE_DESTINATION: OrderedDict(),
        }
        for side, relationships in sides.items():
            for relationship in relationships:
                if getattr(relationship,
                           f"{side}_hidden") and not include_hidden:
                    continue

                # Determine if the relationship is applicable to this object based on the filter
                # To resolve the filter we are using the FilterSet for the given model
                # If there is no match when we query the primary key of the device along with the filter
                # Then the relationship is not applicable to this object
                if getattr(relationship, f"{side}_filter"):
                    filterset = get_filterset_for_model(self._meta.model)
                    if filterset:
                        filter_params = getattr(relationship, f"{side}_filter")
                        if not filterset(
                                filter_params,
                                self._meta.model.objects.filter(
                                    id=self.id)).qs.exists():
                            continue

                # Construct the queryset to query all RelationshipAssociation for this object and this relationship
                query_params = {"relationship": relationship}
                query_params[f"{side}_id"] = self.pk
                query_params[f"{side}_type"] = content_type

                resp[side][
                    relationship] = RelationshipAssociation.objects.filter(
                        **query_params)

        return resp
    def get_queryset(self):
        """Generate a Device QuerySet from the filter."""
        if not self.scope:
            return Device.objects.all()

        filterset_class = get_filterset_for_model(Device)
        filterset = filterset_class(self.scope, Device.objects.all())

        return filterset.qs
Пример #6
0
    def clean(self):

        if self.source_type == self.destination_type:
            raise ValidationError(
                "Not supported to have the same Objet type for Source and Destination."
            )

        # Check if source and destination filters are valid
        for side in ["source", "destination"]:
            if not getattr(self, f"{side}_filter"):
                continue

            filter = getattr(self, f"{side}_filter")
            side_model = getattr(self, f"{side}_type").model_class()
            model_name = side_model._meta.label
            if not isinstance(filter, dict):
                raise ValidationError(
                    f"Filter for {model_name} must be a dictionary")

            filterset = get_filterset_for_model(side_model)
            if not filterset:
                raise ValidationError(
                    f"Filter are not supported for {model_name} object (Unable to find a FilterSet)"
                )

            filterset_params = set(filterset.get_filters().keys())
            for key in filter.keys():
                if key not in filterset_params:
                    raise ValidationError(
                        f"'{key}' is not a valid filter parameter for {model_name} object"
                    )

        # If the model already exist, ensure that it's not possible to modify the source or destination type
        if not self._state.adding:
            nbr_existing_cras = RelationshipAssociation.objects.filter(
                relationship=self).count()

            if nbr_existing_cras and self.__class__.objects.get(
                    pk=self.pk).type != self.type:
                raise ValidationError(
                    "Not supported to change the type of the relationship when some associations"
                    " are present in the database, delete all associations first before modifying the type."
                )

            if nbr_existing_cras and self.__class__.objects.get(
                    pk=self.pk).source_type != self.source_type:
                raise ValidationError(
                    "Not supported to change the type of the source object when some associations"
                    " are present in the database, delete all associations first before modifying the source type."
                )

            elif nbr_existing_cras and self.__class__.objects.get(
                    pk=self.pk).destination_type != self.destination_type:
                raise ValidationError(
                    "Not supported to change the type of the destination object when some associations"
                    " are present in the database, delete all associations first before modifying the destination type."
                )
Пример #7
0
def register_filter_extensions(filter_extensions, plugin_name):
    """
    Register a list of PluginFilterExtension classes
    """
    from nautobot.utilities.utils import get_filterset_for_model, get_form_for_model
    from nautobot.utilities.forms.utils import add_field_to_filter_form_class

    for filter_extension in filter_extensions:
        if not issubclass(filter_extension, PluginFilterExtension):
            raise TypeError(
                f"{filter_extension} is not a subclass of extras.plugins.PluginFilterExtension!"
            )
        if filter_extension.model is None:
            raise TypeError(
                f"PluginFilterExtension class {filter_extension} does not define a valid model!"
            )

        model_filterset_class = get_filterset_for_model(filter_extension.model)
        model_filterform_class = get_form_for_model(filter_extension.model,
                                                    "Filter")

        for new_filterset_field_name, new_filterset_field in filter_extension.filterset_fields.items(
        ):
            if not new_filterset_field_name.startswith(f"{plugin_name}_"):
                raise ValueError(
                    f"Attempted to create a custom filter `{new_filterset_field_name}` that did not start with `{plugin_name}`"
                )

            try:
                model_filterset_class.add_filter(new_filterset_field_name,
                                                 new_filterset_field)
            except AttributeError:
                logger.error(
                    f"There was a conflict with filter set field `{new_filterset_field_name}`, the custom filter set field was ignored."
                )

        for new_filterform_field_name, new_filterform_field in filter_extension.filterform_fields.items(
        ):
            try:
                add_field_to_filter_form_class(
                    form_class=model_filterform_class,
                    field_name=new_filterform_field_name,
                    field_obj=new_filterform_field,
                )
            except AttributeError:
                logger.error(
                    f"There was a conflict with filter form field `{new_filterform_field_name}`, the custom filter form field was ignored."
                )
Пример #8
0
def generate_schema_type(app_name: str, model: object) -> DjangoObjectType:
    """
    Take a Django model and generate a Graphene Type class definition.

    Args:
        app_name (str): name of the application or plugin the Model is part of.
        model (object): Django Model

    Example:
        For a model with a name of "Device", the following class definition is generated:

        class DeviceType(DjangoObjectType):
            Meta:
                model = Device
                fields = ["__all__"]

        If a FilterSet exists for this model at
        '<app_name>.filters.<ModelName>FilterSet' the filterset will be stored in
        filterset_class as follows:

        class DeviceType(DjangoObjectType):
            Meta:
                model = Device
                fields = ["__all__"]
                filterset_class = DeviceFilterSet
    """

    main_attrs = {}
    meta_attrs = {"model": model, "fields": "__all__"}

    # We'll attempt to find a FilterSet corresponding to the model
    # Not all models have a FilterSet defined so the function return none if it can't find a filterset
    meta_attrs["filterset_class"] = get_filterset_for_model(model)

    main_attrs["Meta"] = type("Meta", (object, ), meta_attrs)

    schema_type = type(f"{model.__name__}Type", (DjangoObjectType, ),
                       main_attrs)
    return schema_type
    def clean(self):
        """Validate there is only one model and if there is a GraphQL query, that it is valid."""
        super().clean()

        if self.sot_agg_query:
            try:
                LOGGER.debug("GraphQL - test query: `%s`",
                             str(self.sot_agg_query))
                backend = get_default_backend()
                schema = graphene_settings.SCHEMA
                backend.document_from_string(schema, str(self.sot_agg_query))
            except GraphQLSyntaxError as error:
                raise ValidationError(str(error))  # pylint: disable=raise-missing-from

            LOGGER.debug("GraphQL - test  query start with: `%s`",
                         GRAPHQL_STR_START)
            if not str(self.sot_agg_query).startswith(GRAPHQL_STR_START):
                raise ValidationError(
                    f"The GraphQL query must start with exactly `{GRAPHQL_STR_START}`"
                )

        if self.scope:
            filterset_class = get_filterset_for_model(Device)
            filterset = filterset_class(self.scope, Device.objects.all())

            if filterset.errors:
                for key in filterset.errors:
                    error_message = ", ".join(filterset.errors[key])
                    raise ValidationError({"scope": f"{key}: {error_message}"})

            filterset_params = set(filterset.get_filters().keys())
            for key in self.scope.keys():
                if key not in filterset_params:
                    raise ValidationError({
                        "scope":
                        f"'{key}' is not a valid filter parameter for Device object"
                    })
Пример #10
0
    def clean(self):

        # Check if source and destination filters are valid
        for side in ["source", "destination"]:
            if not getattr(self, f"{side}_filter"):
                continue

            filter = getattr(self, f"{side}_filter")
            side_model = getattr(self, f"{side}_type").model_class()
            if not side_model:  # can happen if for example a plugin providing the model was uninstalled
                raise ValidationError({f"{side}_type": "Unable to locate model class"})
            model_name = side_model._meta.label
            if not isinstance(filter, dict):
                raise ValidationError({f"{side}_filter": f"Filter for {model_name} must be a dictionary"})

            filterset_class = get_filterset_for_model(side_model)
            if not filterset_class:
                raise ValidationError(
                    {
                        f"{side}_filter": f"Filters are not supported for {model_name} object (Unable to find a FilterSet)"
                    }
                )
            filterset = filterset_class(filter, side_model.objects.all())

            error_messages = []
            if filterset.errors:
                for key in filterset.errors:
                    error_messages.append(f"'{key}': " + ", ".join(filterset.errors[key]))

            filterset_params = set(filterset.get_filters().keys())
            for key in filter.keys():
                if key not in filterset_params:
                    error_messages.append(f"'{key}' is not a valid filter parameter for {model_name} object")

            if error_messages:
                raise ValidationError({f"{side}_filter": error_messages})

        if self.symmetric:
            # For a symmetric relation, source and destination attributes must be equivalent if specified
            error_messages = {}
            if self.source_type != self.destination_type:
                error_messages["destination_type"] = "Must match source_type for a symmetric relationship"
            if self.source_label != self.destination_label:
                if not self.source_label:
                    self.source_label = self.destination_label
                elif not self.destination_label:
                    self.destination_label = self.source_label
                else:
                    error_messages["destination_label"] = "Must match source_label for a symmetric relationship"
            if self.source_hidden != self.destination_hidden:
                error_messages["destination_hidden"] = "Must match source_hidden for a symmetric relationship"
            if self.source_filter != self.destination_filter:
                if not self.source_filter:
                    self.source_filter = self.destination_filter
                elif not self.destination_filter:
                    self.destination_filter = self.source_filter
                else:
                    error_messages["destination_filter"] = "Must match source_filter for a symmetric relationship"

            if error_messages:
                raise ValidationError(error_messages)

        # If the model already exist, ensure that it's not possible to modify the source or destination type
        if self.present_in_database:
            nbr_existing_cras = RelationshipAssociation.objects.filter(relationship=self).count()

            if nbr_existing_cras and self.__class__.objects.get(pk=self.pk).type != self.type:
                raise ValidationError(
                    "Not supported to change the type of the relationship when some associations"
                    " are present in the database, delete all associations first before modifying the type."
                )

            if nbr_existing_cras and self.__class__.objects.get(pk=self.pk).source_type != self.source_type:
                raise ValidationError(
                    "Not supported to change the type of the source object when some associations"
                    " are present in the database, delete all associations first before modifying the source type."
                )

            elif nbr_existing_cras and self.__class__.objects.get(pk=self.pk).destination_type != self.destination_type:
                raise ValidationError(
                    "Not supported to change the type of the destination object when some associations"
                    " are present in the database, delete all associations first before modifying the destination type."
                )
Пример #11
0
    def get_relationships(self, include_hidden=False, advanced_ui=None):
        """
        Return a dictionary of queryset for all custom relationships

        Returns:
            response {
                "source": {
                    <relationship #1>: <queryset #1>,
                    <relationship #2>: <queryset #2>,
                },
                "destination": {
                    <relationship #3>: <queryset #3>,
                    <relationship #4>: <queryset #4>,
                },
                "peer": {
                    <relationship #5>: <queryset #5>,
                    <relationship #6>: <queryset #6>,
                },
            }
        """
        src_relationships, dst_relationships = Relationship.objects.get_for_model(
            self)
        if advanced_ui is not None:
            src_relationships = src_relationships.filter(
                advanced_ui=advanced_ui)
            dst_relationships = dst_relationships.filter(
                advanced_ui=advanced_ui)
        content_type = ContentType.objects.get_for_model(self)

        sides = {
            RelationshipSideChoices.SIDE_SOURCE: src_relationships,
            RelationshipSideChoices.SIDE_DESTINATION: dst_relationships,
        }

        resp = {
            RelationshipSideChoices.SIDE_SOURCE: {},
            RelationshipSideChoices.SIDE_DESTINATION: {},
            RelationshipSideChoices.SIDE_PEER: {},
        }
        for side, relationships in sides.items():
            for relationship in relationships:
                if getattr(relationship,
                           f"{side}_hidden") and not include_hidden:
                    continue

                # Determine if the relationship is applicable to this object based on the filter
                # To resolve the filter we are using the FilterSet for the given model
                # If there is no match when we query the primary key of the device along with the filter
                # Then the relationship is not applicable to this object
                if getattr(relationship, f"{side}_filter"):
                    filterset = get_filterset_for_model(self._meta.model)
                    if filterset:
                        filter_params = getattr(relationship, f"{side}_filter")
                        if not filterset(
                                filter_params,
                                self._meta.model.objects.filter(
                                    id=self.id)).qs.exists():
                            continue

                # Construct the queryset to query all RelationshipAssociation for this object and this relationship
                query_params = {"relationship": relationship}
                if not relationship.symmetric:
                    # Query for RelationshipAssociations that this object is on the expected side of
                    query_params[f"{side}_id"] = self.pk
                    query_params[f"{side}_type"] = content_type

                    resp[side][
                        relationship] = RelationshipAssociation.objects.filter(
                            **query_params)
                else:
                    # Query for RelationshipAssociations involving this object, regardless of side
                    resp[RelationshipSideChoices.SIDE_PEER][
                        relationship] = RelationshipAssociation.objects.filter(
                            (Q(source_id=self.pk, source_type=content_type)
                             | Q(destination_id=self.pk,
                                 destination_type=content_type)),
                            **query_params,
                        )

        return resp
Пример #12
0
 def test_get_filterset_for_model(self):
     self.assertEqual(get_filterset_for_model(Device), DeviceFilterSet)
     self.assertEqual(get_filterset_for_model(Site), SiteFilterSet)