Beispiel #1
0
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",
        ]
Beispiel #2
0
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"]
Beispiel #3
0
    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
Beispiel #4
0
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",
        ]
Beispiel #5
0
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"]
Beispiel #6
0
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()
Beispiel #7
0
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)
        )
Beispiel #8
0
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
Beispiel #9
0
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",
        ]
Beispiel #10
0
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)
Beispiel #11
0
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."
            })
Beispiel #12
0
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
Beispiel #13
0
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",
        ]
Beispiel #14
0
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")
Beispiel #15
0
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"
        ]
Beispiel #16
0
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
Beispiel #17
0
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})
Beispiel #18
0
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
Beispiel #19
0
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))
Beispiel #20
0
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
Beispiel #21
0
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,
        )
Beispiel #22
0
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",
        )
Beispiel #23
0
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}}
Beispiel #24
0
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",
        )
Beispiel #25
0
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",
        ]
Beispiel #26
0
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",
        ]
Beispiel #27
0
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",
        ]
Beispiel #28
0
    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}"))
Beispiel #29
0
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."
                )