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

234 lines
6.0 KiB
Python

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.validators import ValidationError
from django.db import models
from taggit.managers import TaggableManager
from extras.choices import ObjectChangeActionChoices
from extras.utils import register_features
from netbox.signals import post_clean
from utilities.utils import serialize_object
__all__ = (
'ChangeLoggingMixin',
'CustomFieldsMixin',
'CustomLinksMixin',
'CustomValidationMixin',
'ExportTemplatesMixin',
'JobResultsMixin',
'JournalingMixin',
'TagsMixin',
'WebhooksMixin',
)
#
# Feature mixins
#
class ChangeLoggingMixin(models.Model):
"""
Provides change logging support for a model. Adds the `created` and `last_updated` fields.
"""
created = models.DateTimeField(
auto_now_add=True,
blank=True,
null=True
)
last_updated = models.DateTimeField(
auto_now=True,
blank=True,
null=True
)
class Meta:
abstract = True
def snapshot(self):
"""
Save a snapshot of the object's current state in preparation for modification.
"""
self._prechange_snapshot = serialize_object(self)
def to_objectchange(self, action):
"""
Return a new ObjectChange representing a change made to this object. This will typically be called automatically
by ChangeLoggingMiddleware.
"""
from extras.models import ObjectChange
objectchange = ObjectChange(
changed_object=self,
object_repr=str(self)[:200],
action=action
)
if hasattr(self, '_prechange_snapshot'):
objectchange.prechange_data = self._prechange_snapshot
if action in (ObjectChangeActionChoices.ACTION_CREATE, ObjectChangeActionChoices.ACTION_UPDATE):
objectchange.postchange_data = serialize_object(self)
return objectchange
class CustomFieldsMixin(models.Model):
"""
Enables support for custom fields.
"""
custom_field_data = models.JSONField(
encoder=DjangoJSONEncoder,
blank=True,
default=dict
)
class Meta:
abstract = True
@property
def cf(self):
"""
A pass-through convenience alias for accessing `custom_field_data` (read-only).
```python
>>> tenant = Tenant.objects.first()
>>> tenant.cf
{'cust_id': 'CYB01'}
```
"""
return self.custom_field_data
def get_custom_fields(self):
"""
Return a dictionary of custom fields for a single object in the form `{field: value}`.
```python
>>> tenant = Tenant.objects.first()
>>> tenant.get_custom_fields()
{<CustomField: Customer ID>: 'CYB01'}
```
"""
from extras.models import CustomField
data = {}
for field in CustomField.objects.get_for_model(self):
value = self.custom_field_data.get(field.name)
data[field] = field.deserialize(value)
return data
def clean(self):
super().clean()
from extras.models import CustomField
custom_fields = {
cf.name: cf for cf in CustomField.objects.get_for_model(self)
}
# Validate all field values
for field_name, value in self.custom_field_data.items():
if field_name not in custom_fields:
raise ValidationError(f"Unknown field name '{field_name}' in custom field data.")
try:
custom_fields[field_name].validate(value)
except ValidationError as e:
raise ValidationError(f"Invalid value for custom field '{field_name}': {e.message}")
# Check for missing required values
for cf in custom_fields.values():
if cf.required and cf.name not in self.custom_field_data:
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):
"""
Enables user-configured validation rules for models.
"""
class Meta:
abstract = True
def clean(self):
super().clean()
# Send the post_clean signal
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):
"""
Enables support for job results.
"""
class Meta:
abstract = True
class JournalingMixin(models.Model):
"""
Enables support for object journaling. Adds a generic relation (`journal_entries`)
to NetBox's JournalEntry model.
"""
journal_entries = GenericRelation(
to='extras.JournalEntry',
object_id_field='assigned_object_id',
content_type_field='assigned_object_type'
)
class Meta:
abstract = True
class TagsMixin(models.Model):
"""
Enables support for tag assignment. Assigned tags can be managed via the `tags` attribute,
which is a `TaggableManager` instance.
"""
tags = TaggableManager(
through='extras.TaggedItem'
)
class Meta:
abstract = True
class WebhooksMixin(models.Model):
"""
Enables support for webhooks.
"""
class Meta:
abstract = True
FEATURES_MAP = (
('custom_fields', CustomFieldsMixin),
('custom_links', CustomLinksMixin),
('export_templates', ExportTemplatesMixin),
('job_results', JobResultsMixin),
('journaling', JournalingMixin),
('tags', TagsMixin),
('webhooks', WebhooksMixin),
)
@receiver(class_prepared)
def _register_features(sender, **kwargs):
features = {
feature for feature, cls in FEATURES_MAP if issubclass(sender, cls)
}
register_features(sender, features)