From c189895f6c17bd64de21e419a2f6678342d62477 Mon Sep 17 00:00:00 2001 From: John Anderson Date: Thu, 12 Mar 2020 18:12:12 -0400 Subject: [PATCH] refactor extras registry --- netbox/dcim/models/__init__.py | 4 +-- netbox/extras/api/serializers.py | 6 ++-- netbox/extras/models.py | 12 +++---- netbox/extras/tests/test_api.py | 3 +- netbox/extras/tests/test_filters.py | 5 ++- netbox/extras/utils.py | 54 ++++++++++++++++------------- 6 files changed, 44 insertions(+), 40 deletions(-) diff --git a/netbox/dcim/models/__init__.py b/netbox/dcim/models/__init__.py index f702f8dff..40f022fa4 100644 --- a/netbox/dcim/models/__init__.py +++ b/netbox/dcim/models/__init__.py @@ -21,7 +21,7 @@ from dcim.constants import * from dcim.fields import ASNField from dcim.elevations import RackElevationSVG from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem -from extras.utils import extras_functionality +from extras.utils import extras_features from utilities.fields import ColorField, NaturalOrderingField from utilities.models import ChangeLoggedModel from utilities.utils import serialize_object, to_meters @@ -1221,7 +1221,7 @@ class Platform(ChangeLoggedModel): ) -@extras_functionality(['webhooks', 'custom_fields', 'export_templates', 'custom_links', 'graphs']) +@extras_features(['webhooks', 'custom_fields', 'export_templates', 'custom_links', 'graphs']) class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): """ A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, diff --git a/netbox/extras/api/serializers.py b/netbox/extras/api/serializers.py index 9830cdd51..774e183ec 100644 --- a/netbox/extras/api/serializers.py +++ b/netbox/extras/api/serializers.py @@ -14,7 +14,7 @@ from extras.constants import * from extras.models import ( ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ) -from extras.utils import FunctionalityQueryset +from extras.utils import FeatureQuerySet from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.models import Tenant, TenantGroup from users.api.nested_serializers import NestedUserSerializer @@ -33,7 +33,7 @@ from .nested_serializers import * class GraphSerializer(ValidatedModelSerializer): type = ContentTypeField( - queryset=ContentType.objects.filter(FunctionalityQueryset('graphs').get_queryset()), + queryset=ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset()), ) class Meta: @@ -69,7 +69,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer): content_type = ContentTypeField( - queryset=ContentType.objects.filter(Q(FunctionalityQueryset('export_templates').get_queryset())), + queryset=ContentType.objects.filter(FeatureQuerySet('export_templates').get_queryset()), ) template_language = ChoiceField( choices=TemplateLanguageChoices, diff --git a/netbox/extras/models.py b/netbox/extras/models.py index 47c05667c..21809c35b 100644 --- a/netbox/extras/models.py +++ b/netbox/extras/models.py @@ -22,7 +22,7 @@ from utilities.utils import deepmerge, render_jinja2 from .choices import * from .constants import * from .querysets import ConfigContextQuerySet -from .utils import FunctionalityQueryset +from .utils import FeatureQuerySet __all__ = ( @@ -59,7 +59,7 @@ class Webhook(models.Model): to=ContentType, related_name='webhooks', verbose_name='Object types', - limit_choices_to=FunctionalityQueryset('webhooks'), + limit_choices_to=FeatureQuerySet('webhooks'), help_text="The object(s) to which this Webhook applies." ) name = models.CharField( @@ -224,7 +224,7 @@ class CustomField(models.Model): to=ContentType, related_name='custom_fields', verbose_name='Object(s)', - limit_choices_to=FunctionalityQueryset('custom_fields'), + limit_choices_to=FeatureQuerySet('custom_fields'), help_text='The object(s) to which this field applies.' ) type = models.CharField( @@ -471,7 +471,7 @@ class CustomLink(models.Model): content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, - limit_choices_to=FunctionalityQueryset('custom_links') + limit_choices_to=FeatureQuerySet('custom_links') ) name = models.CharField( max_length=100, @@ -519,7 +519,7 @@ class Graph(models.Model): type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, - limit_choices_to=FunctionalityQueryset('graphs') + limit_choices_to=FeatureQuerySet('graphs') ) weight = models.PositiveSmallIntegerField( default=1000 @@ -582,7 +582,7 @@ class ExportTemplate(models.Model): content_type = models.ForeignKey( to=ContentType, on_delete=models.CASCADE, - limit_choices_to=FunctionalityQueryset('export_templates') + limit_choices_to=FeatureQuerySet('export_templates') ) name = models.CharField( max_length=100 diff --git a/netbox/extras/tests/test_api.py b/netbox/extras/tests/test_api.py index 6871b2654..b52091f62 100644 --- a/netbox/extras/tests/test_api.py +++ b/netbox/extras/tests/test_api.py @@ -8,7 +8,6 @@ from rest_framework import status from dcim.models import Device, DeviceRole, DeviceType, Manufacturer, Platform, Rack, RackGroup, RackRole, Region, Site from extras.api.views import ScriptViewSet from extras.choices import * -from extras.constants import GRAPH_MODELS from extras.models import ConfigContext, Graph, ExportTemplate, Tag from extras.scripts import BooleanVar, IntegerVar, Script, StringVar from tenancy.models import Tenant, TenantGroup @@ -35,7 +34,7 @@ class AppTest(APITestCase): self.assertEqual(choices_to_dict(response.data.get('export-template:template_language')), TemplateLanguageChoices.as_dict()) # Graph - content_types = ContentType.objects.filter(GRAPH_MODELS) + content_types = ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset()) graph_type_choices = { "{}.{}".format(ct.app_label, ct.model): str(ct) for ct in content_types } diff --git a/netbox/extras/tests/test_filters.py b/netbox/extras/tests/test_filters.py index 126414cfd..9b02293eb 100644 --- a/netbox/extras/tests/test_filters.py +++ b/netbox/extras/tests/test_filters.py @@ -3,7 +3,6 @@ from django.test import TestCase from dcim.models import DeviceRole, Platform, Region, Site from extras.choices import * -from extras.constants import GRAPH_MODELS from extras.filters import * from extras.models import ConfigContext, ExportTemplate, Graph from tenancy.models import Tenant, TenantGroup @@ -18,7 +17,7 @@ class GraphTestCase(TestCase): def setUpTestData(cls): # Get the first three available types - content_types = ContentType.objects.filter(GRAPH_MODELS)[:3] + content_types = ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset())[:3] graphs = ( Graph(name='Graph 1', type=content_types[0], template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, source='http://example.com/1'), @@ -32,7 +31,7 @@ class GraphTestCase(TestCase): self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2) def test_type(self): - content_type = ContentType.objects.filter(GRAPH_MODELS).first() + content_type = ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset()).first() params = {'type': content_type.pk} self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1) diff --git a/netbox/extras/utils.py b/netbox/extras/utils.py index d6e55e6f6..31a312333 100644 --- a/netbox/extras/utils.py +++ b/netbox/extras/utils.py @@ -22,55 +22,61 @@ def is_taggable(obj): class Registry: """ - Singleton object used to store important data + The registry is a place to hook into for data storage across components """ - instance = None - def __new__(cls): - if cls.instance is not None: - return cls.instance - else: - cls.instance = super().__new__(cls) - cls.model_functionality_store = {f: collections.defaultdict(list) for f in EXTRAS_FUNCTIONALITIES} - return cls.instance + def add_store(self, store_name, initial_value=None): + """ + Given the name of some new data parameter and an optional initial value, setup the registry store + """ + if not hasattr(Registry, store_name): + setattr(Registry, store_name, initial_value) + +registry = Registry() -class FunctionalityQueryset: +# +# Dynamic feature registration +# + +class FeatureQuerySet: """ Helper class that delays evaluation of the registry contents for the functionaility store until it has been populated. """ - def __init__(self, functionality): - self.functionality = functionality + def __init__(self, feature): + self.feature = feature def __call__(self): return self.get_queryset() def get_queryset(self): """ - Given an extras functionality, return a Q object for content type lookup + Given an extras feature, return a Q object for content type lookup """ query = Q() - registry = Registry() - for app_label, models in registry.model_functionality_store[self.functionality].items(): + #registry = Registry() + for app_label, models in registry.model_feature_store[self.feature].items(): query |= Q(app_label=app_label, model__in=models) return query -def extras_functionality(functionalities): +registry.add_store('model_feature_store', {f: collections.defaultdict(list) for f in EXTRAS_FUNCTIONALITIES}) + + +def extras_features(features): """ - Decorator used to register extras provided functionalities to a model + Decorator used to register extras provided features to a model """ def wrapper(model_class): - if isinstance(functionalities, list) and functionalities: - registry = Registry() - model_class._extras_functionality = [] - for functionality in functionalities: - if functionality in EXTRAS_FUNCTIONALITIES: - model_class._extras_functionality.append(functionality) + if isinstance(features, list) and features: + #registry = Registry() + model_class._extras_feature = [] + for feature in features: + if feature in EXTRAS_FUNCTIONALITIES: app_label, model_name = model_class._meta.label_lower.split('.') - registry.model_functionality_store[functionality][app_label].append(model_name) + registry.model_feature_store[feature][app_label].append(model_name) return model_class return wrapper