class RelationshipSerializer(serializers.ModelSerializer): url = serializers.HyperlinkedIdentityField( view_name="extras-api:relationship-detail") source_type = ContentTypeField(queryset=ContentType.objects.filter( FeatureQuery("relationships").get_query()), ) destination_type = ContentTypeField(queryset=ContentType.objects.filter( FeatureQuery("relationships").get_query()), ) class Meta: model = Relationship fields = [ "id", "url", "name", "slug", "description", "type", "source_type", "source_label", "source_hidden", "source_filter", "destination_type", "destination_label", "destination_hidden", "destination_filter", ]
class RelationshipFilterSet(BaseFilterSet): source_type = ContentTypeMultipleChoiceFilter(choices=FeatureQuery("relationships").get_choices, conjoined=False) destination_type = ContentTypeMultipleChoiceFilter( choices=FeatureQuery("relationships").get_choices, conjoined=False ) class Meta: model = Relationship fields = ["id", "name", "type", "source_type", "destination_type"]
def __init__(self, *args, feature=None, choices_as_strings=False, **kwargs): """ Construct a MultipleContentTypeField. Args: feature (str): Feature name to use in constructing a FeatureQuery to restrict the available ContentTypes. choices_as_strings (bool): If True, render selection as a list of `"{app_label}.{model}"` strings. """ if "queryset" not in kwargs: if feature is not None: kwargs["queryset"] = ContentType.objects.filter( FeatureQuery(feature).get_query()).order_by( "app_label", "model") else: kwargs["queryset"] = ContentType.objects.order_by( "app_label", "model") if "widget" not in kwargs: kwargs["widget"] = widgets.StaticSelect2Multiple() super().__init__(*args, **kwargs) if choices_as_strings: self.choices = self._string_choices_from_queryset
class WebhookSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField( view_name="extras-api:webhook-detail") content_types = ContentTypeField( queryset=ContentType.objects.filter( FeatureQuery("webhooks").get_query()).order_by( "app_label", "model"), many=True, ) class Meta: model = Webhook fields = [ "id", "url", "content_types", "name", "type_create", "type_update", "type_delete", "payload_url", "http_method", "http_content_type", "additional_headers", "body_template", "secret", "ssl_verification", "ca_file_path", ]
class RelationshipAssociationFilterSet(BaseFilterSet): relationship = django_filters.ModelMultipleChoiceFilter( field_name="relationship__slug", queryset=Relationship.objects.all(), to_field_name="slug", label="Relationship (slug)", ) source_type = ContentTypeMultipleChoiceFilter(choices=FeatureQuery("relationships").get_choices, conjoined=False) destination_type = ContentTypeMultipleChoiceFilter( choices=FeatureQuery("relationships").get_choices, conjoined=False ) class Meta: model = RelationshipAssociation fields = ["id", "relationship", "source_type", "source_id", "destination_type", "destination_id"]
class StatusFilterSet(BaseFilterSet, CreatedUpdatedFilterSet, CustomFieldFilterSet): """API filter for filtering custom status object fields.""" q = django_filters.CharFilter( method="search", label="Search", ) content_types = ContentTypeMultipleChoiceFilter( choices=FeatureQuery("statuses").get_choices, ) class Meta: model = Status fields = [ "id", "content_types", "color", "name", "slug", "created", "last_updated", ] def search(self, queryset, name, value): if not value.strip(): return queryset return queryset.filter( Q(name__icontains=value) | Q(slug__icontains=value) | Q(content_types__model__icontains=value) ).distinct()
class WebhookFilterSet(BaseFilterSet): q = django_filters.CharFilter( method="search", label="Search", ) content_types = ContentTypeMultipleChoiceFilter( choices=FeatureQuery("webhooks").get_choices, ) class Meta: model = Webhook fields = [ "name", "payload_url", "enabled", "content_types", "type_create", "type_update", "type_delete", ] def search(self, queryset, name, value): if not value.strip(): return queryset return queryset.filter( Q(name__icontains=value) | Q(payload_url__icontains=value) | Q(additional_headers__icontains=value) | Q(body_template__icontains=value) )
class ConfigContextSchemaSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField( view_name="extras-api:configcontextschema-detail") owner_content_type = ContentTypeField( queryset=ContentType.objects.filter( FeatureQuery("config_context_owners").get_query()), required=False, allow_null=True, default=None, ) owner = serializers.SerializerMethodField(read_only=True) class Meta: model = ConfigContextSchema fields = [ "id", "url", "name", "slug", "owner_content_type", "owner_object_id", "owner", "description", "data_schema", "created", "last_updated", ] @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_owner(self, obj): if obj.owner is None: return None serializer = get_serializer_for_model(obj.owner, prefix="Nested") context = {"request": self.context["request"]} return serializer(obj.owner, context=context).data
class CustomFieldSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField( view_name="extras-api:customfield-detail") content_types = ContentTypeField( queryset=ContentType.objects.filter( FeatureQuery("custom_fields").get_query()), many=True, ) type = ChoiceField(choices=CustomFieldTypeChoices) filter_logic = ChoiceField(choices=CustomFieldFilterLogicChoices, required=False) class Meta: model = CustomField fields = [ "id", "url", "content_types", "type", "name", "label", "description", "required", "filter_logic", "default", "weight", "validation_minimum", "validation_maximum", "validation_regex", ]
def wrap_model_clean_methods(): """ Helper function that wraps plugin model validator registered clean methods for all applicable models """ for model in ContentType.objects.filter( FeatureQuery("custom_validators").get_query()): model_class = model.model_class() model_class.clean = custom_validator_clean(model_class.clean)
class ConfigContextSchema(OrganizationalModel): """ This model stores jsonschema documents where are used to optionally validate config context data payloads. """ name = models.CharField(max_length=200, unique=True) description = models.CharField(max_length=200, blank=True) slug = AutoSlugField(populate_from="name", max_length=200, unique=None, db_index=True) data_schema = models.JSONField( help_text= "A JSON Schema document which is used to validate a config context object." ) # A ConfigContextSchema *may* be owned by another model, such as a GitRepository, or it may be un-owned owner_content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, limit_choices_to=FeatureQuery("config_context_owners"), default=None, null=True, blank=True, ) owner_object_id = models.UUIDField(default=None, null=True, blank=True) owner = GenericForeignKey( ct_field="owner_content_type", fk_field="owner_object_id", ) def __str__(self): if self.owner: return f"[{self.owner}] {self.name}" return self.name def get_absolute_url(self): return reverse("extras:configcontextschema", args=[self.slug]) def clean(self): """ Validate the schema """ super().clean() try: Draft7Validator.check_schema(self.data_schema) except SchemaError as e: raise ValidationError({"data_schema": e.message}) if (not isinstance(self.data_schema, dict) or "properties" not in self.data_schema or self.data_schema.get("type") != "object"): raise ValidationError({ "data_schema": "Nautobot only supports context data in the form of an object and thus the " "JSON schema must be of type object and specify a set of properties." })
class ComputedField(BaseModel, ChangeLoggedModel): """ Read-only rendered fields driven by a Jinja2 template that are applied to objects within a ContentType. """ content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, limit_choices_to=FeatureQuery("custom_fields"), ) slug = AutoSlugField(populate_from="label", help_text="Internal field name") label = models.CharField( max_length=100, help_text="Name of the field as displayed to users") description = models.CharField(max_length=200, blank=True) template = models.TextField( max_length=500, help_text="Jinja2 template code for field value") fallback_value = models.CharField( max_length=500, blank=True, help_text= "Fallback value (if any) to be output for the field in the case of a template rendering error.", ) weight = models.PositiveSmallIntegerField(default=100) objects = ComputedFieldManager() clone_fields = [ "content_type", "description", "template", "fallback_value", "weight" ] class Meta: ordering = ["weight", "slug"] unique_together = ("content_type", "label") def __str__(self): return self.label def get_absolute_url(self): return reverse("extras:computedfield", args=[self.slug]) def render(self, context): try: rendered = render_jinja2(self.template, context) # If there is an undefined variable within a template, it returns nothing # Doesn't raise an exception either most likely due to using Undefined rather # than StrictUndefined, but return fallback_value if None is returned if rendered is None: logger.warning("Failed to render computed field %s", self.slug) return self.fallback_value return rendered except Exception as exc: logger.warning("Failed to render computed field %s: %s", self.slug, exc) return self.fallback_value
class RelationshipAssociationSerializer(serializers.ModelSerializer): source_type = ContentTypeField(queryset=ContentType.objects.filter( FeatureQuery("relationships").get_query()), ) destination_type = ContentTypeField(queryset=ContentType.objects.filter( FeatureQuery("relationships").get_query()), ) relationship = NestedRelationshipSerializer() class Meta: model = RelationshipAssociation fields = [ "id", "relationship", "source_type", "source_id", "destination_type", "destination_id", ]
class DynamicGroupFilterSet(NautobotFilterSet): q = SearchFilter(filter_predicates={ "name": "icontains", "slug": "icontains", "description": "icontains", "content_type__app_label": "icontains", "content_type__model": "icontains", }, ) content_type = ContentTypeMultipleChoiceFilter( choices=FeatureQuery("dynamic_groups").get_choices, conjoined=False) class Meta: model = DynamicGroup fields = ("id", "name", "slug", "description")
class CustomFieldFilterSet(BaseFilterSet): q = SearchFilter(filter_predicates={ "name": "icontains", "label": "icontains", "description": "icontains", }, ) content_types = ContentTypeMultipleChoiceFilter( choices=FeatureQuery("custom_fields").get_choices, ) class Meta: model = CustomField fields = [ "id", "content_types", "name", "required", "filter_logic", "weight" ]
class ExportTemplateSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField( view_name="extras-api:exporttemplate-detail") content_type = ContentTypeField(queryset=ContentType.objects.filter( FeatureQuery("export_templates").get_query()), ) owner_content_type = ContentTypeField( queryset=ContentType.objects.filter( FeatureQuery("export_template_owners").get_query()), required=False, allow_null=True, default=None, ) owner = serializers.SerializerMethodField(read_only=True) class Meta: model = ExportTemplate fields = [ "id", "url", "content_type", "owner_content_type", "owner_object_id", "owner", "name", "description", "template_code", "mime_type", "file_extension", ] @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_owner(self, obj): if obj.owner is None: return None serializer = get_serializer_for_model(obj.owner, prefix="Nested") context = {"request": self.context["request"]} return serializer(obj.owner, context=context).data
class CustomLink(BaseModel, ChangeLoggedModel): """ A custom link to an external representation of a Nautobot object. The link text and URL fields accept Jinja2 template code to be rendered with an object as context. """ content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, limit_choices_to=FeatureQuery("custom_links"), ) name = models.CharField(max_length=100, unique=True) text = models.CharField( max_length=500, help_text= "Jinja2 template code for link text. Reference the object as <code>{{ obj }}</code> such as <code>{{ obj.platform.slug }}</code>. Links which render as empty text will not be displayed.", ) target_url = models.CharField( max_length=500, verbose_name="URL", help_text= "Jinja2 template code for link URL. Reference the object as <code>{{ obj }}</code> such as <code>{{ obj.platform.slug }}</code>.", ) weight = models.PositiveSmallIntegerField(default=100) group_name = models.CharField( max_length=50, blank=True, help_text="Links with the same group will appear as a dropdown menu", ) button_class = models.CharField( max_length=30, choices=CustomLinkButtonClassChoices, default=CustomLinkButtonClassChoices.CLASS_DEFAULT, help_text= "The class of the first link in a group will be used for the dropdown button", ) new_window = models.BooleanField( help_text="Force link to open in a new window") class Meta: ordering = ["group_name", "weight", "name"] def __str__(self): return self.name def get_absolute_url(self): return reverse("extras:customlink", kwargs={"pk": self.pk})
class WebhookSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField( view_name="extras-api:webhook-detail") content_types = ContentTypeField( queryset=ContentType.objects.filter( FeatureQuery("webhooks").get_query()).order_by( "app_label", "model"), many=True, ) class Meta: model = Webhook fields = [ "id", "url", "content_types", "name", "type_create", "type_update", "type_delete", "payload_url", "http_method", "http_content_type", "additional_headers", "body_template", "secret", "ssl_verification", "ca_file_path", ] def validate(self, data): validated_data = super().validate(data) conflicts = Webhook.check_for_conflicts( instance=self.instance, content_types=data.get("content_types"), payload_url=data.get("payload_url"), type_create=data.get("type_create"), type_update=data.get("type_update"), type_delete=data.get("type_delete"), ) if conflicts: raise serializers.ValidationError(conflicts) return validated_data
class CustomFieldFilterSet(BaseFilterSet): q = django_filters.CharFilter( method="search", label="Search", ) content_types = ContentTypeMultipleChoiceFilter( choices=FeatureQuery("custom_fields").get_choices, ) class Meta: model = CustomField fields = ["id", "content_types", "name", "required", "filter_logic", "weight"] def search(self, queryset, name, value): if not value.strip(): return queryset return queryset.filter(Q(name__icontains=value) | Q(label__icontains=value) | Q(description__icontains=value))
class ComputedField(BaseModel, ChangeLoggedModel): """ Read-only rendered fields driven by a Jinja2 template that are applied to objects within a ContentType. """ content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, limit_choices_to=FeatureQuery("custom_fields"), ) slug = models.SlugField(max_length=100, unique=True, help_text="Internal field name") label = models.CharField( max_length=100, help_text="Name of the field as displayed to users") description = models.CharField(max_length=200, blank=True) template = models.TextField( max_length=500, help_text="Jinja2 template code for field value") fallback_value = models.CharField( max_length=500, help_text= "Fallback value to be used for the field in the case of a template rendering error." ) weight = models.PositiveSmallIntegerField(default=100) objects = ComputedFieldManager() class Meta: ordering = ["weight", "slug"] unique_together = ("content_type", "label") def __str__(self): return self.label def get_absolute_url(self): return reverse("extras:computedfield", args=[self.slug]) def render(self, context): try: return render_jinja2(self.template, context) except TemplateError as e: logger.warning("Failed to render computed field %s: %s", self.slug, e) return self.fallback_value
class Status(BaseModel, ChangeLoggedModel, CustomFieldModel, RelationshipModel): """Model for database-backend enum choice objects.""" content_types = models.ManyToManyField( to=ContentType, related_name="statuses", verbose_name="Content type(s)", limit_choices_to=FeatureQuery("statuses"), help_text="The content type(s) to which this status applies.", ) name = models.CharField(max_length=50, unique=True) color = ColorField(default=ColorChoices.COLOR_GREY) slug = models.SlugField(max_length=50, unique=True) description = models.CharField( max_length=200, blank=True, ) objects = StatusQuerySet.as_manager() csv_headers = ["name", "slug", "color", "content_types", "description"] clone_fields = ["color", "content_types"] class Meta: ordering = ["name"] verbose_name_plural = "statuses" def __str__(self): return self.name def get_absolute_url(self): return reverse("extras:status", args=[self.slug]) def to_csv(self): labels = ",".join(f"{ct.app_label}.{ct.model}" for ct in self.content_types.all()) return ( self.name, self.slug, self.color, f'"{labels}"', # Wrap labels in double quotes for CSV self.description, )
class ComputedFieldSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name="extras-api:computedfield-detail") content_type = ContentTypeField( queryset=ContentType.objects.filter(FeatureQuery("custom_fields").get_query()).order_by("app_label", "model"), ) class Meta: model = ComputedField fields = ( "id", "url", "slug", "label", "description", "content_type", "template", "fallback_value", "weight", )
class DynamicGroupSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField( view_name="extras-api:dynamicgroup-detail") content_type = ContentTypeField(queryset=ContentType.objects.filter( FeatureQuery("dynamic_groups").get_query()).order_by( "app_label", "model"), ) class Meta: model = DynamicGroup fields = [ "id", "url", "name", "slug", "description", "content_type", "filter", ] extra_kwargs = {"filter": {"read_only": False}}
class CustomLinkSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField(view_name="extras-api:customlink-detail") content_type = ContentTypeField( queryset=ContentType.objects.filter(FeatureQuery("custom_links").get_query()).order_by("app_label", "model"), ) class Meta: model = CustomLink fields = ( "id", "url", "target_url", "name", "content_type", "text", "weight", "group_name", "button_class", "new_window", )
class WebhookFilterSet(BaseFilterSet): q = SearchFilter(filter_predicates={ "name": "icontains", "payload_url": "icontains", "additional_headers": "icontains", "body_template": "icontains", }, ) content_types = ContentTypeMultipleChoiceFilter( choices=FeatureQuery("webhooks").get_choices, ) class Meta: model = Webhook fields = [ "name", "payload_url", "enabled", "content_types", "type_create", "type_update", "type_delete", ]
class StatusFilterSet(NautobotFilterSet): """API filter for filtering custom status object fields.""" q = SearchFilter(filter_predicates={ "name": "icontains", "slug": "icontains", "content_types__model": "icontains", }, ) content_types = ContentTypeMultipleChoiceFilter( choices=FeatureQuery("statuses").get_choices, ) class Meta: model = Status fields = [ "id", "content_types", "color", "name", "slug", "created", "last_updated", ]
class StatusSerializer(CustomFieldModelSerializer): """Serializer for `Status` objects.""" url = serializers.HyperlinkedIdentityField(view_name="extras-api:status-detail") content_types = ContentTypeField( queryset=ContentType.objects.filter(FeatureQuery("statuses").get_query()), many=True, ) class Meta: model = Status fields = [ "id", "url", "content_types", "name", "slug", "color", "custom_fields", "created", "last_updated", ]
def handle(self, *args, **kwargs): """Run through all objects and ensure they are associated with the correct custom fields.""" content_types = ContentType.objects.filter( FeatureQuery("custom_fields").get_query()) for content_type in content_types: self.stdout.write( self.style.SUCCESS(f"Processing ContentType {content_type}")) model = content_type.model_class() custom_fields_for_content_type = content_type.custom_fields.all() custom_field_names_for_content_type = [ cf.name for cf in custom_fields_for_content_type ] with transaction.atomic(): for obj in model.objects.all(): obj_changed = False # Provision CustomFields that are not associated with the object for custom_field in custom_fields_for_content_type: if not obj._custom_field_data[custom_field.name]: self.stdout.write( f"Adding missing CustomField {custom_field.name} to {obj}" ) obj._custom_field_data[ custom_field.name] = custom_field.default obj_changed = True # Remove any custom fields that are not associated with the content type for field_name in set(obj._custom_field_data) - set( custom_field_names_for_content_type): self.stdout.write( f"Removing invalid CustomField {field_name} from {obj}" ) del obj._custom_field_data[field_name] obj_changed = True if obj_changed: try: obj.validated_save() except ValidationError: self.stderr.write( self.style.ERROR(f"Failed saving {obj}"))
class ConfigContextSerializer(ValidatedModelSerializer): url = serializers.HyperlinkedIdentityField( view_name="extras-api:configcontext-detail") owner_content_type = ContentTypeField( queryset=ContentType.objects.filter( FeatureQuery("config_context_owners").get_query()), required=False, allow_null=True, default=None, ) owner = serializers.SerializerMethodField(read_only=True) schema = NestedConfigContextSchemaSerializer(required=False, allow_null=True) regions = SerializedPKRelatedField( queryset=Region.objects.all(), serializer=NestedRegionSerializer, required=False, many=True, ) sites = SerializedPKRelatedField( queryset=Site.objects.all(), serializer=NestedSiteSerializer, required=False, many=True, ) roles = SerializedPKRelatedField( queryset=DeviceRole.objects.all(), serializer=NestedDeviceRoleSerializer, required=False, many=True, ) device_types = SerializedPKRelatedField( queryset=DeviceType.objects.all(), serializer=NestedDeviceRoleSerializer, required=False, many=True, ) platforms = SerializedPKRelatedField( queryset=Platform.objects.all(), serializer=NestedPlatformSerializer, required=False, many=True, ) cluster_groups = SerializedPKRelatedField( queryset=ClusterGroup.objects.all(), serializer=NestedClusterGroupSerializer, required=False, many=True, ) clusters = SerializedPKRelatedField( queryset=Cluster.objects.all(), serializer=NestedClusterSerializer, required=False, many=True, ) tenant_groups = SerializedPKRelatedField( queryset=TenantGroup.objects.all(), serializer=NestedTenantGroupSerializer, required=False, many=True, ) tenants = SerializedPKRelatedField( queryset=Tenant.objects.all(), serializer=NestedTenantSerializer, required=False, many=True, ) tags = serializers.SlugRelatedField(queryset=Tag.objects.all(), slug_field="slug", required=False, many=True) class Meta: model = ConfigContext fields = [ "id", "url", "name", "owner_content_type", "owner_object_id", "owner", "weight", "description", "schema", "is_active", "regions", "sites", "roles", "device_types", "platforms", "cluster_groups", "clusters", "tenant_groups", "tenants", "tags", "data", "created", "last_updated", ] @swagger_serializer_method(serializer_or_field=serializers.DictField) def get_owner(self, obj): if obj.owner is None: return None serializer = get_serializer_for_model(obj.owner, prefix="Nested") context = {"request": self.context["request"]} return serializer(obj.owner, context=context).data
class Relationship(BaseModel, ChangeLoggedModel): name = models.CharField(max_length=100, unique=True, help_text="Internal relationship name") slug = models.SlugField(max_length=100, unique=True) description = models.CharField(max_length=200, blank=True) type = models.CharField( max_length=50, choices=RelationshipTypeChoices, default=RelationshipTypeChoices.TYPE_MANY_TO_MANY, ) # # Source # source_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, related_name="source_relationships", verbose_name="Source Object", limit_choices_to=FeatureQuery("relationships"), help_text="The source object to which this relationship applies.", ) source_label = models.CharField( max_length=50, blank=True, verbose_name="Source Label", help_text="Name of the relationship as displayed on the source object.", ) source_hidden = models.BooleanField( default=False, verbose_name="Hide for source object", help_text="Hide this relationship on the source object.", ) source_filter = models.JSONField( encoder=DjangoJSONEncoder, blank=True, null=True, help_text= "Queryset filter matching the applicable source objects of the selected type", ) # # Destination # destination_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, related_name="destination_relationships", verbose_name="Destination Object", limit_choices_to=FeatureQuery("relationships"), help_text="The destination object to which this relationship applies.", ) destination_label = models.CharField( max_length=50, blank=True, verbose_name="Destination Label", help_text="Name of the relationship as displayed on the source object.", ) destination_hidden = models.BooleanField( default=False, verbose_name="Hide for destination object", help_text="Hide this relationship on the destination object.", ) destination_filter = models.JSONField( encoder=DjangoJSONEncoder, blank=True, null=True, help_text= "Queryset filter matching the applicable destination objects of the selected type", ) objects = RelationshipManager() class Meta: ordering = ["name"] def __str__(self): return self.name.replace("_", " ").capitalize() def get_label(self, side): """Return the label for a given side, source or destination. If the label is not returned, return the verbose_name_plural of the other object """ if side not in VALID_SIDES: raise ValueError( f"side value can only be: {','.join(VALID_SIDES)}") if getattr(self, f"{side}_label"): return getattr(self, f"{side}_label") if side == RelationshipSideChoices.SIDE_SOURCE: destination_model = self.destination_type.model_class() if self.type in ( RelationshipTypeChoices.TYPE_MANY_TO_MANY, RelationshipTypeChoices.TYPE_ONE_TO_MANY, ): return destination_model._meta.verbose_name_plural else: return destination_model._meta.verbose_name elif side == RelationshipSideChoices.SIDE_DESTINATION: source_model = self.source_type.model_class() if self.type == RelationshipTypeChoices.TYPE_MANY_TO_MANY: return source_model._meta.verbose_name_plural else: return source_model._meta.verbose_name return None def has_many(self, side): """Return True if the given side of the relationship can support multiple objects.""" if side not in VALID_SIDES: raise ValueError( f"side value can only be: {','.join(VALID_SIDES)}") if self.type == RelationshipTypeChoices.TYPE_MANY_TO_MANY: return True if self.type == RelationshipTypeChoices.TYPE_ONE_TO_ONE: return False if side == RelationshipSideChoices.SIDE_SOURCE and self.type == RelationshipTypeChoices.TYPE_ONE_TO_MANY: return False if side == RelationshipSideChoices.SIDE_DESTINATION and self.type == RelationshipTypeChoices.TYPE_ONE_TO_MANY: return True return None def to_form_field(self, side): """ Return a form field suitable for setting a Relationship's value for an object. """ if side not in VALID_SIDES: raise ValueError( f"side value can only be: {','.join(VALID_SIDES)}") peer_side = RelationshipSideChoices.OPPOSITE[side] object_type = getattr(self, f"{peer_side}_type") filters = getattr(self, f"{peer_side}_filter") or {} queryset = object_type.model_class().objects.all() field_class = None if self.has_many(peer_side): field_class = DynamicModelMultipleChoiceField else: field_class = DynamicModelChoiceField field = field_class(queryset=queryset, query_params=filters) field.model = self field.required = False field.label = self.get_label(side) if self.description: field.help_text = self.description return field 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." )