1
0
mirror of https://github.com/netbox-community/netbox.git synced 2024-05-10 07:54:54 +00:00

Designate feature mixin classes & employ class_prepared signal to register features

This commit is contained in:
jeremystretch
2022-01-19 14:46:50 -05:00
parent bf6345aa90
commit c7825e391c
3 changed files with 198 additions and 125 deletions

View File

@ -57,21 +57,24 @@ class FeatureQuery:
return query return query
def register_features(model, features):
if 'model_features' not in registry:
registry['model_features'] = {
f: collections.defaultdict(list) for f in EXTRAS_FEATURES
}
for feature in features:
if feature not in EXTRAS_FEATURES:
raise ValueError(f"{feature} is not a valid extras feature!")
app_label, model_name = model._meta.label_lower.split('.')
registry['model_features'][feature][app_label].append(model_name)
def extras_features(*features): def extras_features(*features):
""" """
Decorator used to register extras provided features to a model Decorator used to register extras provided features to a model
""" """
def wrapper(model_class): def wrapper(model_class):
# Initialize the model_features store if not already defined # Initialize the model_features store if not already defined
if 'model_features' not in registry: register_features(model_class, features)
registry['model_features'] = {
f: collections.defaultdict(list) for f in EXTRAS_FEATURES
}
for feature in features:
if feature in EXTRAS_FEATURES:
app_label, model_name = model_class._meta.label_lower.split('.')
registry['model_features'][feature][app_label].append(model_name)
else:
raise ValueError('{} is not a valid extras feature!'.format(feature))
return model_class return model_class
return wrapper return wrapper

View File

@ -0,0 +1,129 @@
from django.contrib.contenttypes.fields import GenericRelation
from django.core.validators import ValidationError
from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from utilities.mptt import TreeManager
from utilities.querysets import RestrictedQuerySet
from netbox.models.features import *
__all__ = (
'BigIDModel',
'ChangeLoggedModel',
'NestedGroupModel',
'OrganizationalModel',
'PrimaryModel',
)
#
# Base model classes
#
class BigIDModel(models.Model):
"""
Abstract base model for all data objects. Ensures the use of a 64-bit PK.
"""
id = models.BigAutoField(
primary_key=True
)
class Meta:
abstract = True
class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, BigIDModel):
"""
Base model for all objects which support change logging.
"""
objects = RestrictedQuerySet.as_manager()
class Meta:
abstract = True
class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
"""
Primary models represent real objects within the infrastructure being modeled.
"""
journal_entries = GenericRelation(
to='extras.JournalEntry',
object_id_field='assigned_object_id',
content_type_field='assigned_object_type'
)
objects = RestrictedQuerySet.as_manager()
class Meta:
abstract = True
class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel, MPTTModel):
"""
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
recursively using MPTT. Within each parent, each child instance must have a unique name.
"""
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
name = models.CharField(
max_length=100
)
description = models.CharField(
max_length=200,
blank=True
)
objects = TreeManager()
class Meta:
abstract = True
class MPTTMeta:
order_insertion_by = ('name',)
def __str__(self):
return self.name
def clean(self):
super().clean()
# An MPTT model cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({
"parent": "Cannot assign self as parent."
})
class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
"""
Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
any real information about the infrastructure being modeled (for example, functional device roles). Organizational
models provide the following standard attributes:
- Unique name
- Unique slug (automatically derived from name)
- Optional description
"""
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
description = models.CharField(
max_length=200,
blank=True
)
objects = RestrictedQuerySet.as_manager()
class Meta:
abstract = True
ordering = ('name',)

View File

@ -1,34 +1,37 @@
import logging import logging
from django.contrib.contenttypes.fields import GenericRelation from django.db.models.signals import class_prepared
from django.dispatch import receiver
from django.core.serializers.json import DjangoJSONEncoder from django.core.serializers.json import DjangoJSONEncoder
from django.core.validators import ValidationError from django.core.validators import ValidationError
from django.db import models from django.db import models
from mptt.models import MPTTModel, TreeForeignKey
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from extras.choices import ObjectChangeActionChoices from extras.choices import ObjectChangeActionChoices
from extras.utils import register_features
from netbox.signals import post_clean from netbox.signals import post_clean
from utilities.mptt import TreeManager
from utilities.querysets import RestrictedQuerySet
from utilities.utils import serialize_object from utilities.utils import serialize_object
__all__ = ( __all__ = (
'BigIDModel', 'ChangeLoggingMixin',
'ChangeLoggedModel', 'CustomFieldsMixin',
'NestedGroupModel', 'CustomLinksMixin',
'OrganizationalModel', 'CustomValidationMixin',
'PrimaryModel', 'ExportTemplatesMixin',
'JobResultsMixin',
'TagsMixin',
'WebhooksMixin',
) )
# #
# Mixins # Feature mixins
# #
class ChangeLoggingMixin(models.Model): class ChangeLoggingMixin(models.Model):
""" """
Provides change logging support. Provides change logging support for a model. Adds the `created` and `last_updated` fields.
""" """
created = models.DateField( created = models.DateField(
auto_now_add=True, auto_now_add=True,
@ -74,7 +77,7 @@ class ChangeLoggingMixin(models.Model):
class CustomFieldsMixin(models.Model): class CustomFieldsMixin(models.Model):
""" """
Provides support for custom fields. Enables support for custom fields.
""" """
custom_field_data = models.JSONField( custom_field_data = models.JSONField(
encoder=DjangoJSONEncoder, encoder=DjangoJSONEncoder,
@ -128,6 +131,14 @@ class CustomFieldsMixin(models.Model):
raise ValidationError(f"Missing required custom field '{cf.name}'.") raise ValidationError(f"Missing required custom field '{cf.name}'.")
class CustomLinksMixin(models.Model):
"""
Enables support for custom links.
"""
class Meta:
abstract = True
class CustomValidationMixin(models.Model): class CustomValidationMixin(models.Model):
""" """
Enables user-configured validation rules for built-in models by extending the clean() method. Enables user-configured validation rules for built-in models by extending the clean() method.
@ -142,9 +153,25 @@ class CustomValidationMixin(models.Model):
post_clean.send(sender=self.__class__, instance=self) post_clean.send(sender=self.__class__, instance=self)
class ExportTemplatesMixin(models.Model):
"""
Enables support for export templates.
"""
class Meta:
abstract = True
class JobResultsMixin(models.Model):
"""
Enable the assignment of JobResults to a model.
"""
class Meta:
abstract = True
class TagsMixin(models.Model): class TagsMixin(models.Model):
""" """
Enable the assignment of Tags. Enable the assignment of Tags to a model.
""" """
tags = TaggableManager( tags = TaggableManager(
through='extras.TaggedItem' through='extras.TaggedItem'
@ -154,113 +181,27 @@ class TagsMixin(models.Model):
abstract = True abstract = True
# class WebhooksMixin(models.Model):
# Base model classes
class BigIDModel(models.Model):
""" """
Abstract base model for all data objects. Ensures the use of a 64-bit PK. Enables support for webhooks.
""" """
id = models.BigAutoField(
primary_key=True
)
class Meta: class Meta:
abstract = True abstract = True
class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, BigIDModel): FEATURES_MAP = (
""" ('custom_fields', CustomFieldsMixin),
Base model for all objects which support change logging. ('custom_links', CustomLinksMixin),
""" ('export_templates', ExportTemplatesMixin),
objects = RestrictedQuerySet.as_manager() ('job_results', JobResultsMixin),
('tags', TagsMixin),
class Meta: ('webhooks', WebhooksMixin),
abstract = True )
class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel): @receiver(class_prepared)
""" def _register_features(sender, **kwargs):
Primary models represent real objects within the infrastructure being modeled. features = {
""" feature for feature, cls in FEATURES_MAP if issubclass(sender, cls)
journal_entries = GenericRelation( }
to='extras.JournalEntry', register_features(sender, features)
object_id_field='assigned_object_id',
content_type_field='assigned_object_type'
)
objects = RestrictedQuerySet.as_manager()
class Meta:
abstract = True
class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel, MPTTModel):
"""
Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
recursively using MPTT. Within each parent, each child instance must have a unique name.
"""
parent = TreeForeignKey(
to='self',
on_delete=models.CASCADE,
related_name='children',
blank=True,
null=True,
db_index=True
)
name = models.CharField(
max_length=100
)
description = models.CharField(
max_length=200,
blank=True
)
objects = TreeManager()
class Meta:
abstract = True
class MPTTMeta:
order_insertion_by = ('name',)
def __str__(self):
return self.name
def clean(self):
super().clean()
# An MPTT model cannot be its own parent
if self.pk and self.parent_id == self.pk:
raise ValidationError({
"parent": "Cannot assign self as parent."
})
class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
"""
Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
any real information about the infrastructure being modeled (for example, functional device roles). Organizational
models provide the following standard attributes:
- Unique name
- Unique slug (automatically derived from name)
- Optional description
"""
name = models.CharField(
max_length=100,
unique=True
)
slug = models.SlugField(
max_length=100,
unique=True
)
description = models.CharField(
max_length=200,
blank=True
)
objects = RestrictedQuerySet.as_manager()
class Meta:
abstract = True
ordering = ('name',)