implemented registry for extras model functionality

This commit is contained in:
John Anderson 2020-03-12 04:07:54 -04:00
parent 8af4cf87b5
commit 235d99021b
5 changed files with 249 additions and 173 deletions

View File

@ -21,6 +21,7 @@ from dcim.constants import *
from dcim.fields import ASNField from dcim.fields import ASNField
from dcim.elevations import RackElevationSVG from dcim.elevations import RackElevationSVG
from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem from extras.models import ConfigContextModel, CustomFieldModel, ObjectChange, TaggedItem
from extras.utils import extras_functionality
from utilities.fields import ColorField, NaturalOrderingField from utilities.fields import ColorField, NaturalOrderingField
from utilities.models import ChangeLoggedModel from utilities.models import ChangeLoggedModel
from utilities.utils import serialize_object, to_meters from utilities.utils import serialize_object, to_meters
@ -1220,6 +1221,7 @@ class Platform(ChangeLoggedModel):
) )
@extras_functionality(['webhooks', 'custom_fields', 'export_templates', 'custom_links', 'graphs'])
class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel): class Device(ChangeLoggedModel, ConfigContextModel, CustomFieldModel):
""" """
A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType, A Device represents a piece of physical hardware mounted within a Rack. Each Device is assigned a DeviceType,

View File

@ -1,5 +1,6 @@
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from drf_yasg.utils import swagger_serializer_method from drf_yasg.utils import swagger_serializer_method
from rest_framework import serializers from rest_framework import serializers
@ -13,6 +14,7 @@ from extras.constants import *
from extras.models import ( from extras.models import (
ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag, ConfigContext, ExportTemplate, Graph, ImageAttachment, ObjectChange, ReportResult, Tag,
) )
from extras.utils import FunctionalityQueryset
from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer from tenancy.api.nested_serializers import NestedTenantSerializer, NestedTenantGroupSerializer
from tenancy.models import Tenant, TenantGroup from tenancy.models import Tenant, TenantGroup
from users.api.nested_serializers import NestedUserSerializer from users.api.nested_serializers import NestedUserSerializer
@ -31,7 +33,7 @@ from .nested_serializers import *
class GraphSerializer(ValidatedModelSerializer): class GraphSerializer(ValidatedModelSerializer):
type = ContentTypeField( type = ContentTypeField(
queryset=ContentType.objects.filter(GRAPH_MODELS), queryset=ContentType.objects.filter(FunctionalityQueryset('graphs').get_queryset()),
) )
class Meta: class Meta:
@ -67,7 +69,7 @@ class RenderedGraphSerializer(serializers.ModelSerializer):
class ExportTemplateSerializer(ValidatedModelSerializer): class ExportTemplateSerializer(ValidatedModelSerializer):
content_type = ContentTypeField( content_type = ContentTypeField(
queryset=ContentType.objects.filter(EXPORTTEMPLATE_MODELS), queryset=ContentType.objects.filter(Q(FunctionalityQueryset('export_templates').get_queryset())),
) )
template_language = ChoiceField( template_language = ChoiceField(
choices=TemplateLanguageChoices, choices=TemplateLanguageChoices,

View File

@ -2,127 +2,127 @@ from django.db.models import Q
# Models which support custom fields # Models which support custom fields
CUSTOMFIELD_MODELS = Q( #CUSTOMFIELD_MODELS = Q(
Q(app_label='circuits', model__in=[ # Q(app_label='circuits', model__in=[
'circuit', # 'circuit',
'provider', # 'provider',
]) | # ]) |
Q(app_label='dcim', model__in=[ # Q(app_label='dcim', model__in=[
'device', # 'device',
'devicetype', # 'devicetype',
'powerfeed', # 'powerfeed',
'rack', # 'rack',
'site', # 'site',
]) | # ]) |
Q(app_label='ipam', model__in=[ # Q(app_label='ipam', model__in=[
'aggregate', # 'aggregate',
'ipaddress', # 'ipaddress',
'prefix', # 'prefix',
'service', # 'service',
'vlan', # 'vlan',
'vrf', # 'vrf',
]) | # ]) |
Q(app_label='secrets', model__in=[ # Q(app_label='secrets', model__in=[
'secret', # 'secret',
]) | # ]) |
Q(app_label='tenancy', model__in=[ # Q(app_label='tenancy', model__in=[
'tenant', # 'tenant',
]) | # ]) |
Q(app_label='virtualization', model__in=[ # Q(app_label='virtualization', model__in=[
'cluster', # 'cluster',
'virtualmachine', # 'virtualmachine',
]) # ])
) #)
#
# Custom links ## Custom links
CUSTOMLINK_MODELS = Q( #CUSTOMLINK_MODELS = Q(
Q(app_label='circuits', model__in=[ # Q(app_label='circuits', model__in=[
'circuit', # 'circuit',
'provider', # 'provider',
]) | # ]) |
Q(app_label='dcim', model__in=[ # Q(app_label='dcim', model__in=[
'cable', # 'cable',
'device', # 'device',
'devicetype', # 'devicetype',
'powerpanel', # 'powerpanel',
'powerfeed', # 'powerfeed',
'rack', # 'rack',
'site', # 'site',
]) | # ]) |
Q(app_label='ipam', model__in=[ # Q(app_label='ipam', model__in=[
'aggregate', # 'aggregate',
'ipaddress', # 'ipaddress',
'prefix', # 'prefix',
'service', # 'service',
'vlan', # 'vlan',
'vrf', # 'vrf',
]) | # ]) |
Q(app_label='secrets', model__in=[ # Q(app_label='secrets', model__in=[
'secret', # 'secret',
]) | # ]) |
Q(app_label='tenancy', model__in=[ # Q(app_label='tenancy', model__in=[
'tenant', # 'tenant',
]) | # ]) |
Q(app_label='virtualization', model__in=[ # Q(app_label='virtualization', model__in=[
'cluster', # 'cluster',
'virtualmachine', # 'virtualmachine',
]) # ])
) #)
#
# Models which can have Graphs associated with them ## Models which can have Graphs associated with them
GRAPH_MODELS = Q( #GRAPH_MODELS = Q(
Q(app_label='circuits', model__in=[ # Q(app_label='circuits', model__in=[
'provider', # 'provider',
]) | # ]) |
Q(app_label='dcim', model__in=[ # Q(app_label='dcim', model__in=[
'device', # 'device',
'interface', # 'interface',
'site', # 'site',
]) # ])
) #)
#
# Models which support export templates ## Models which support export templates
EXPORTTEMPLATE_MODELS = Q( #EXPORTTEMPLATE_MODELS = Q(
Q(app_label='circuits', model__in=[ # Q(app_label='circuits', model__in=[
'circuit', # 'circuit',
'provider', # 'provider',
]) | # ]) |
Q(app_label='dcim', model__in=[ # Q(app_label='dcim', model__in=[
'cable', # 'cable',
'consoleport', # 'consoleport',
'device', # 'device',
'devicetype', # 'devicetype',
'interface', # 'interface',
'inventoryitem', # 'inventoryitem',
'manufacturer', # 'manufacturer',
'powerpanel', # 'powerpanel',
'powerport', # 'powerport',
'powerfeed', # 'powerfeed',
'rack', # 'rack',
'rackgroup', # 'rackgroup',
'region', # 'region',
'site', # 'site',
'virtualchassis', # 'virtualchassis',
]) | # ]) |
Q(app_label='ipam', model__in=[ # Q(app_label='ipam', model__in=[
'aggregate', # 'aggregate',
'ipaddress', # 'ipaddress',
'prefix', # 'prefix',
'service', # 'service',
'vlan', # 'vlan',
'vrf', # 'vrf',
]) | # ]) |
Q(app_label='secrets', model__in=[ # Q(app_label='secrets', model__in=[
'secret', # 'secret',
]) | # ]) |
Q(app_label='tenancy', model__in=[ # Q(app_label='tenancy', model__in=[
'tenant', # 'tenant',
]) | # ]) |
Q(app_label='virtualization', model__in=[ # Q(app_label='virtualization', model__in=[
'cluster', # 'cluster',
'virtualmachine', # 'virtualmachine',
]) # ])
) #)
# Report logging levels # Report logging levels
LOG_DEFAULT = 0 LOG_DEFAULT = 0
@ -141,48 +141,58 @@ LOG_LEVEL_CODES = {
HTTP_CONTENT_TYPE_JSON = 'application/json' HTTP_CONTENT_TYPE_JSON = 'application/json'
# Models which support registered webhooks # Models which support registered webhooks
WEBHOOK_MODELS = Q( #WEBHOOK_MODELS = Q(
Q(app_label='circuits', model__in=[ # Q(app_label='circuits', model__in=[
'circuit', # 'circuit',
'provider', # 'provider',
]) | # ]) |
Q(app_label='dcim', model__in=[ # Q(app_label='dcim', model__in=[
'cable', # 'cable',
'consoleport', # 'consoleport',
'consoleserverport', # 'consoleserverport',
'device', # 'device',
'devicebay', # 'devicebay',
'devicetype', # 'devicetype',
'frontport', # 'frontport',
'interface', # 'interface',
'inventoryitem', # 'inventoryitem',
'manufacturer', # 'manufacturer',
'poweroutlet', # 'poweroutlet',
'powerpanel', # 'powerpanel',
'powerport', # 'powerport',
'powerfeed', # 'powerfeed',
'rack', # 'rack',
'rearport', # 'rearport',
'region', # 'region',
'site', # 'site',
'virtualchassis', # 'virtualchassis',
]) | # ]) |
Q(app_label='ipam', model__in=[ # Q(app_label='ipam', model__in=[
'aggregate', # 'aggregate',
'ipaddress', # 'ipaddress',
'prefix', # 'prefix',
'service', # 'service',
'vlan', # 'vlan',
'vrf', # 'vrf',
]) | # ]) |
Q(app_label='secrets', model__in=[ # Q(app_label='secrets', model__in=[
'secret', # 'secret',
]) | # ]) |
Q(app_label='tenancy', model__in=[ # Q(app_label='tenancy', model__in=[
'tenant', # 'tenant',
]) | # ]) |
Q(app_label='virtualization', model__in=[ # Q(app_label='virtualization', model__in=[
'cluster', # 'cluster',
'virtualmachine', # 'virtualmachine',
]) # ])
) #)
# Registerable extras functionalities
EXTRAS_FUNCTIONALITIES = [
'custom_fields',
'custom_links',
'graphs',
'export_templates',
'webhooks'
]

View File

@ -22,6 +22,7 @@ from utilities.utils import deepmerge, render_jinja2
from .choices import * from .choices import *
from .constants import * from .constants import *
from .querysets import ConfigContextQuerySet from .querysets import ConfigContextQuerySet
from .utils import FunctionalityQueryset
__all__ = ( __all__ = (
@ -58,7 +59,7 @@ class Webhook(models.Model):
to=ContentType, to=ContentType,
related_name='webhooks', related_name='webhooks',
verbose_name='Object types', verbose_name='Object types',
limit_choices_to=WEBHOOK_MODELS, limit_choices_to=FunctionalityQueryset('webhooks'),
help_text="The object(s) to which this Webhook applies." help_text="The object(s) to which this Webhook applies."
) )
name = models.CharField( name = models.CharField(
@ -223,7 +224,7 @@ class CustomField(models.Model):
to=ContentType, to=ContentType,
related_name='custom_fields', related_name='custom_fields',
verbose_name='Object(s)', verbose_name='Object(s)',
limit_choices_to=CUSTOMFIELD_MODELS, limit_choices_to=FunctionalityQueryset('custom_fields'),
help_text='The object(s) to which this field applies.' help_text='The object(s) to which this field applies.'
) )
type = models.CharField( type = models.CharField(
@ -470,7 +471,7 @@ class CustomLink(models.Model):
content_type = models.ForeignKey( content_type = models.ForeignKey(
to=ContentType, to=ContentType,
on_delete=models.CASCADE, on_delete=models.CASCADE,
limit_choices_to=CUSTOMLINK_MODELS limit_choices_to=FunctionalityQueryset('custom_links')
) )
name = models.CharField( name = models.CharField(
max_length=100, max_length=100,
@ -518,7 +519,7 @@ class Graph(models.Model):
type = models.ForeignKey( type = models.ForeignKey(
to=ContentType, to=ContentType,
on_delete=models.CASCADE, on_delete=models.CASCADE,
limit_choices_to=GRAPH_MODELS limit_choices_to=FunctionalityQueryset('graphs')
) )
weight = models.PositiveSmallIntegerField( weight = models.PositiveSmallIntegerField(
default=1000 default=1000
@ -581,7 +582,7 @@ class ExportTemplate(models.Model):
content_type = models.ForeignKey( content_type = models.ForeignKey(
to=ContentType, to=ContentType,
on_delete=models.CASCADE, on_delete=models.CASCADE,
limit_choices_to=EXPORTTEMPLATE_MODELS limit_choices_to=FunctionalityQueryset('export_templates')
) )
name = models.CharField( name = models.CharField(
max_length=100 max_length=100

View File

@ -1,6 +1,11 @@
import collections
from django.db.models import Q
from taggit.managers import _TaggableManager from taggit.managers import _TaggableManager
from utilities.querysets import DummyQuerySet from utilities.querysets import DummyQuerySet
from extras.constants import EXTRAS_FUNCTIONALITIES
def is_taggable(obj): def is_taggable(obj):
""" """
@ -13,3 +18,59 @@ def is_taggable(obj):
if isinstance(obj.tags, DummyQuerySet): if isinstance(obj.tags, DummyQuerySet):
return True return True
return False return False
class Registry:
"""
Singleton object used to store important data
"""
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
class FunctionalityQueryset:
"""
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 __call__(self):
return self.get_queryset()
def get_queryset(self):
"""
Given an extras functionality, return a Q object for content type lookup
"""
query = Q()
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
def extras_functionality(functionalities):
"""
Decorator used to register extras provided functionalities 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)
app_label, model_name = model_class._meta.label_lower.split('.')
registry.model_functionality_store[functionality][app_label].append(model_name)
return model_class
return wrapper