Skip to content

Commit

Permalink
Merge pull request #361 from Kircheneer/u/Kircheneer-backport-cf-iden…
Browse files Browse the repository at this point in the history
…tifiers

Backport of #350
  • Loading branch information
jdrew82 authored Feb 21, 2024
2 parents 7a705fe + c4a7c59 commit 273587f
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 9 deletions.
3 changes: 3 additions & 0 deletions docs/dev/jobs.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,9 @@ As you can see when looking at the [source code](https://github.com/nautobot/nau

The above example shows the simplest field type (an attribute on the model), however, to build a production implementation you will need to understand how to identify different variants of fields by following the [modeling docs](../user/modeling.md).

!!! warn
Currently, only normal fields, forwards foreign key fields and custom fields may be used in identifiers. Anything else is unsupported and will likely fail in unintuitive ways.

### Step 2.1 - Creating the Nautobot Adapter

Having created all your models, creating the Nautobot side adapter is very straight-forward:
Expand Down
46 changes: 37 additions & 9 deletions nautobot_ssot/contrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,11 @@ class CustomFieldAnnotation:
For usage with `typing.Annotated`.
This exists to map model fields to their corresponding custom fields. This solves the problem of Python object
attributes not being able to include spaces, while custom field names/labels may.
TODO: With Nautobot 2.0, the custom fields `key` field needs to be a valid Python identifier. This will probably
simplify this a lot.
This exists to map model fields to their corresponding custom fields. This serves to explicitly differentiate
normal fields from custom fields.
Example:
Given a boolean custom field "Is Global" on the Provider model:
Given a boolean custom field with name "Is Global" and key "is_global" on the Provider model:
```python
class ProviderModel(NautobotModel):
Expand All @@ -92,7 +89,7 @@ class ProviderModel(NautobotModel):
is_global: Annotated[bool, CustomFieldAnnotation(name="Is Global")
```
This then maps the model field 'is_global' to the custom field 'Is Global'.
This then maps the model field 'is_global' to the custom field with the name 'Is Global'.
"""

name: str
Expand Down Expand Up @@ -466,9 +463,40 @@ def _check_field(cls, name):
def get_from_db(self):
"""Get the ORM object for this diffsync object from the database using the identifiers.
TODO: Currently I don't think this works for custom fields, therefore those can't be identifiers.
Note that this method currently supports the following things in identifiers:
- Normal model fields
- Foreign key fields (i.e. ones with the `__` syntax separating fields)
- Nautobot custom fields
TODO - Currently unsupported are:
- to-many-relationships, i.e. reverse foreign keys or many-to-many relationships
- probably also generic relationships, this is untested and hard to test in the current Nautobot version (2.1)
"""
return self.diffsync.get_from_orm_cache(self.get_identifiers(), self._model)
parameters = {}
custom_field_lookup = {}
type_hints = get_type_hints(self, include_extras=True)
is_custom_field = False
for key, value in self.get_identifiers().items():
metadata_for_this_field = getattr(type_hints[key], "__metadata__", [])
for metadata in metadata_for_this_field:
if isinstance(metadata, CustomFieldAnnotation):
custom_field_lookup[metadata.name] = value
is_custom_field = True
if not is_custom_field:
parameters[key] = value
for key, value in custom_field_lookup.items():
parameters[f"_custom_field_data__{key}"] = value
try:
return self.diffsync.get_from_orm_cache(parameters, self._model)
except self._model.DoesNotExist as error:
raise ValueError(
f"No such {self._model._meta.verbose_name} instance with lookup parameters {parameters}."
) from error
except self._model.MultipleObjectsReturned as error:
raise ValueError(
f"Multiple {self._model._meta.verbose_name} instances with lookup parameters {parameters}."
) from error

def update(self, attrs):
"""Update the ORM object corresponding to this diffsync object."""
Expand Down
38 changes: 38 additions & 0 deletions nautobot_ssot/tests/test_contrib.py
Original file line number Diff line number Diff line change
Expand Up @@ -849,3 +849,41 @@ def test_caching(self):
tenant_group_queries = [query["sql"] for query in ctx.captured_queries if query_filter in query["sql"]]
# One query per tenant to re-populate the cache and another query per tenant during `clean`.
self.assertEqual(6, len(tenant_group_queries))


class BaseModelIdentifierTest(TestCase):
"""Test cases for testing various things as identifiers for models."""

@classmethod
def setUpTestData(cls):
custom_field_label = "Preferred ice cream flavour"
cls.custom_field = extras_models.CustomField.objects.create(
label=custom_field_label, description="The preferred flavour of ice cream for the reps for this provider"
)
cls.custom_field.content_types.add(ContentType.objects.get_for_model(circuits_models.Provider))
provider_name = "Link Inc."
provider_flavour = "Vanilla"
cls.provider = circuits_models.Provider.objects.create(
name=provider_name, _custom_field_data={cls.custom_field.name: provider_flavour}
)

def test_custom_field_in_identifiers(self):
"""Test the basic case where a custom field is part of the identifiers of a diffsync model."""
custom_field_name = self.custom_field.name

class _ProviderTestModel(NautobotModel):
_model = circuits_models.Provider
_modelname = "provider"
_identifiers = ("name", "flavour")
_attributes = ()

name: str
flavour: Annotated[str, CustomFieldAnnotation(name=custom_field_name)]

diffsync_provider = _ProviderTestModel(
name=self.provider.name,
flavour=self.provider._custom_field_data[self.custom_field.name], # pylint: disable=protected-access
)
diffsync_provider.diffsync = NautobotAdapter(job=None)

self.assertEqual(self.provider, diffsync_provider.get_from_db())

0 comments on commit 273587f

Please sign in to comment.