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)
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" ) })
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
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
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." )
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." )
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" })
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." )
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
def test_get_filterset_for_model(self): self.assertEqual(get_filterset_for_model(Device), DeviceFilterSet) self.assertEqual(get_filterset_for_model(Site), SiteFilterSet)