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:
@ -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
|
||||||
|
129
netbox/netbox/models/__init__.py
Normal file
129
netbox/netbox/models/__init__.py
Normal 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',)
|
@ -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',)
|
|
Reference in New Issue
Block a user