mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
@ -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_features
|
||||
from extras.utils import extras_functionality
|
||||
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_features(['webhooks', 'custom_fields', 'export_templates', 'custom_links', 'graphs'])
|
||||
@extras_functionality(['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,
|
||||
|
@ -14,7 +14,7 @@ from extras.constants import *
|
||||
from extras.models import (
|
||||
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
|
||||
)
|
||||
from extras.utils import FeatureQuerySet
|
||||
from extras.utils import FunctionalityQueryset
|
||||
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(FeatureQuerySet('graphs').get_queryset()),
|
||||
queryset=ContentType.objects.filter(FunctionalityQueryset('graphs').get_queryset()),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@ -69,7 +69,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
|
||||
|
||||
class ExportTemplateSerializer(ValidatedModelSerializer):
|
||||
content_type = ContentTypeField(
|
||||
queryset=ContentType.objects.filter(FeatureQuerySet('export_templates').get_queryset()),
|
||||
queryset=ContentType.objects.filter(Q(FunctionalityQueryset('export_templates').get_queryset())),
|
||||
)
|
||||
template_language = ChoiceField(
|
||||
choices=TemplateLanguageChoices,
|
||||
|
@ -22,7 +22,7 @@ from utilities.utils import deepmerge, render_jinja2
|
||||
from .choices import *
|
||||
from .constants import *
|
||||
from .querysets import ConfigContextQuerySet
|
||||
from .utils import FeatureQuerySet
|
||||
from .utils import FunctionalityQueryset
|
||||
|
||||
|
||||
__all__ = (
|
||||
@ -59,7 +59,7 @@ class Webhook(models.Model):
|
||||
to=ContentType,
|
||||
related_name='webhooks',
|
||||
verbose_name='Object types',
|
||||
limit_choices_to=FeatureQuerySet('webhooks'),
|
||||
limit_choices_to=FunctionalityQueryset('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=FeatureQuerySet('custom_fields'),
|
||||
limit_choices_to=FunctionalityQueryset('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=FeatureQuerySet('custom_links')
|
||||
limit_choices_to=FunctionalityQueryset('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=FeatureQuerySet('graphs')
|
||||
limit_choices_to=FunctionalityQueryset('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=FeatureQuerySet('export_templates')
|
||||
limit_choices_to=FunctionalityQueryset('export_templates')
|
||||
)
|
||||
name = models.CharField(
|
||||
max_length=100
|
||||
|
@ -8,6 +8,7 @@ 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
|
||||
@ -34,7 +35,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(FeatureQuerySet('graphs').get_queryset())
|
||||
content_types = ContentType.objects.filter(GRAPH_MODELS)
|
||||
graph_type_choices = {
|
||||
"{}.{}".format(ct.app_label, ct.model): str(ct) for ct in content_types
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ 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
|
||||
@ -17,7 +18,7 @@ class GraphTestCase(TestCase):
|
||||
def setUpTestData(cls):
|
||||
|
||||
# Get the first three available types
|
||||
content_types = ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset())[:3]
|
||||
content_types = ContentType.objects.filter(GRAPH_MODELS)[:3]
|
||||
|
||||
graphs = (
|
||||
Graph(name='Graph 1', type=content_types[0], template_language=TemplateLanguageChoices.LANGUAGE_DJANGO, source='http://example.com/1'),
|
||||
@ -31,7 +32,7 @@ class GraphTestCase(TestCase):
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 2)
|
||||
|
||||
def test_type(self):
|
||||
content_type = ContentType.objects.filter(FeatureQuerySet('graphs').get_queryset()).first()
|
||||
content_type = ContentType.objects.filter(GRAPH_MODELS).first()
|
||||
params = {'type': content_type.pk}
|
||||
self.assertEqual(self.filterset(params, self.queryset).qs.count(), 1)
|
||||
|
||||
|
@ -22,61 +22,55 @@ def is_taggable(obj):
|
||||
|
||||
class Registry:
|
||||
"""
|
||||
The registry is a place to hook into for data storage across components
|
||||
Singleton object used to store important data
|
||||
"""
|
||||
instance = None
|
||||
|
||||
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()
|
||||
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
|
||||
|
||||
|
||||
#
|
||||
# Dynamic feature registration
|
||||
#
|
||||
|
||||
class FeatureQuerySet:
|
||||
class FunctionalityQueryset:
|
||||
"""
|
||||
Helper class that delays evaluation of the registry contents for the functionaility store
|
||||
until it has been populated.
|
||||
"""
|
||||
|
||||
def __init__(self, feature):
|
||||
self.feature = feature
|
||||
def __init__(self, functionality):
|
||||
self.functionality = functionality
|
||||
|
||||
def __call__(self):
|
||||
return self.get_queryset()
|
||||
|
||||
def get_queryset(self):
|
||||
"""
|
||||
Given an extras feature, return a Q object for content type lookup
|
||||
Given an extras functionality, return a Q object for content type lookup
|
||||
"""
|
||||
query = Q()
|
||||
#registry = Registry()
|
||||
for app_label, models in registry.model_feature_store[self.feature].items():
|
||||
registry = Registry()
|
||||
for app_label, models in registry.model_functionality_store[self.functionality].items():
|
||||
query |= Q(app_label=app_label, model__in=models)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
registry.add_store('model_feature_store', {f: collections.defaultdict(list) for f in EXTRAS_FUNCTIONALITIES})
|
||||
|
||||
|
||||
def extras_features(features):
|
||||
def extras_functionality(functionalities):
|
||||
"""
|
||||
Decorator used to register extras provided features to a model
|
||||
Decorator used to register extras provided functionalities to a model
|
||||
"""
|
||||
def wrapper(model_class):
|
||||
if isinstance(features, list) and features:
|
||||
#registry = Registry()
|
||||
model_class._extras_feature = []
|
||||
for feature in features:
|
||||
if feature in EXTRAS_FUNCTIONALITIES:
|
||||
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)
|
||||
app_label, model_name = model_class._meta.label_lower.split('.')
|
||||
registry.model_feature_store[feature][app_label].append(model_name)
|
||||
registry.model_functionality_store[functionality][app_label].append(model_name)
|
||||
return model_class
|
||||
return wrapper
|
||||
|
Reference in New Issue
Block a user