Răsfoiți Sursa

Remove extras_features() decorator

jeremystretch 4 ani în urmă
părinte
comite
cdae0c2bef

+ 2 - 5
netbox/circuits/models/circuits.py

@@ -5,8 +5,8 @@ from django.urls import reverse
 
 
 from circuits.choices import *
 from circuits.choices import *
 from dcim.models import LinkTermination
 from dcim.models import LinkTermination
-from extras.utils import extras_features
 from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
 from netbox.models import ChangeLoggedModel, OrganizationalModel, PrimaryModel
+from netbox.models.features import WebhooksMixin
 
 
 __all__ = (
 __all__ = (
     'Circuit',
     'Circuit',
@@ -15,7 +15,6 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class CircuitType(OrganizationalModel):
 class CircuitType(OrganizationalModel):
     """
     """
     Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
     Circuits can be organized by their functional role. For example, a user might wish to define CircuitTypes named
@@ -44,7 +43,6 @@ class CircuitType(OrganizationalModel):
         return reverse('circuits:circuittype', args=[self.pk])
         return reverse('circuits:circuittype', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Circuit(PrimaryModel):
 class Circuit(PrimaryModel):
     """
     """
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
     A communications circuit connects two points. Each Circuit belongs to a Provider; Providers may have multiple
@@ -138,8 +136,7 @@ class Circuit(PrimaryModel):
         return CircuitStatusChoices.colors.get(self.status, 'secondary')
         return CircuitStatusChoices.colors.get(self.status, 'secondary')
 
 
 
 
-@extras_features('webhooks')
-class CircuitTermination(ChangeLoggedModel, LinkTermination):
+class CircuitTermination(WebhooksMixin, ChangeLoggedModel, LinkTermination):
     circuit = models.ForeignKey(
     circuit = models.ForeignKey(
         to='circuits.Circuit',
         to='circuits.Circuit',
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,

+ 0 - 3
netbox/circuits/models/providers.py

@@ -3,7 +3,6 @@ from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 
 
 from dcim.fields import ASNField
 from dcim.fields import ASNField
-from extras.utils import extras_features
 from netbox.models import PrimaryModel
 from netbox.models import PrimaryModel
 
 
 __all__ = (
 __all__ = (
@@ -12,7 +11,6 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Provider(PrimaryModel):
 class Provider(PrimaryModel):
     """
     """
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
     Each Circuit belongs to a Provider. This is usually a telecommunications company or similar organization. This model
@@ -72,7 +70,6 @@ class Provider(PrimaryModel):
         return reverse('circuits:provider', args=[self.pk])
         return reverse('circuits:provider', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ProviderNetwork(PrimaryModel):
 class ProviderNetwork(PrimaryModel):
     """
     """
     This represents a provider network which exists outside of NetBox, the details of which are unknown or
     This represents a provider network which exists outside of NetBox, the details of which are unknown or

+ 0 - 2
netbox/dcim/models/cables.py

@@ -11,7 +11,6 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.fields import PathField
 from dcim.fields import PathField
 from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
 from dcim.utils import decompile_path_node, object_to_path_node, path_node_to_object
-from extras.utils import extras_features
 from netbox.models import BigIDModel, PrimaryModel
 from netbox.models import BigIDModel, PrimaryModel
 from utilities.fields import ColorField
 from utilities.fields import ColorField
 from utilities.utils import to_meters
 from utilities.utils import to_meters
@@ -29,7 +28,6 @@ __all__ = (
 # Cables
 # Cables
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Cable(PrimaryModel):
 class Cable(PrimaryModel):
     """
     """
     A physical connection between two endpoints.
     A physical connection between two endpoints.

+ 3 - 13
netbox/dcim/models/device_component_templates.py

@@ -1,4 +1,4 @@
-from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
+from django.contrib.contenttypes.fields import GenericForeignKey
 from django.contrib.contenttypes.models import ContentType
 from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
 from django.core.validators import MaxValueValidator, MinValueValidator
 from django.core.validators import MaxValueValidator, MinValueValidator
@@ -7,8 +7,8 @@ from mptt.models import MPTTModel, TreeForeignKey
 
 
 from dcim.choices import *
 from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
-from extras.utils import extras_features
 from netbox.models import ChangeLoggedModel
 from netbox.models import ChangeLoggedModel
+from netbox.models.features import WebhooksMixin
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.mptt import TreeManager
 from utilities.mptt import TreeManager
 from utilities.ordering import naturalize_interface
 from utilities.ordering import naturalize_interface
@@ -32,7 +32,7 @@ __all__ = (
 )
 )
 
 
 
 
-class ComponentTemplateModel(ChangeLoggedModel):
+class ComponentTemplateModel(WebhooksMixin, ChangeLoggedModel):
     device_type = models.ForeignKey(
     device_type = models.ForeignKey(
         to='dcim.DeviceType',
         to='dcim.DeviceType',
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
@@ -135,7 +135,6 @@ class ModularComponentTemplateModel(ComponentTemplateModel):
         return self.name
         return self.name
 
 
 
 
-@extras_features('webhooks')
 class ConsolePortTemplate(ModularComponentTemplateModel):
 class ConsolePortTemplate(ModularComponentTemplateModel):
     """
     """
     A template for a ConsolePort to be created for a new Device.
     A template for a ConsolePort to be created for a new Device.
@@ -164,7 +163,6 @@ class ConsolePortTemplate(ModularComponentTemplateModel):
         )
         )
 
 
 
 
-@extras_features('webhooks')
 class ConsoleServerPortTemplate(ModularComponentTemplateModel):
 class ConsoleServerPortTemplate(ModularComponentTemplateModel):
     """
     """
     A template for a ConsoleServerPort to be created for a new Device.
     A template for a ConsoleServerPort to be created for a new Device.
@@ -193,7 +191,6 @@ class ConsoleServerPortTemplate(ModularComponentTemplateModel):
         )
         )
 
 
 
 
-@extras_features('webhooks')
 class PowerPortTemplate(ModularComponentTemplateModel):
 class PowerPortTemplate(ModularComponentTemplateModel):
     """
     """
     A template for a PowerPort to be created for a new Device.
     A template for a PowerPort to be created for a new Device.
@@ -245,7 +242,6 @@ class PowerPortTemplate(ModularComponentTemplateModel):
                 })
                 })
 
 
 
 
-@extras_features('webhooks')
 class PowerOutletTemplate(ModularComponentTemplateModel):
 class PowerOutletTemplate(ModularComponentTemplateModel):
     """
     """
     A template for a PowerOutlet to be created for a new Device.
     A template for a PowerOutlet to be created for a new Device.
@@ -307,7 +303,6 @@ class PowerOutletTemplate(ModularComponentTemplateModel):
         )
         )
 
 
 
 
-@extras_features('webhooks')
 class InterfaceTemplate(ModularComponentTemplateModel):
 class InterfaceTemplate(ModularComponentTemplateModel):
     """
     """
     A template for a physical data interface on a new Device.
     A template for a physical data interface on a new Device.
@@ -347,7 +342,6 @@ class InterfaceTemplate(ModularComponentTemplateModel):
         )
         )
 
 
 
 
-@extras_features('webhooks')
 class FrontPortTemplate(ModularComponentTemplateModel):
 class FrontPortTemplate(ModularComponentTemplateModel):
     """
     """
     Template for a pass-through port on the front of a new Device.
     Template for a pass-through port on the front of a new Device.
@@ -420,7 +414,6 @@ class FrontPortTemplate(ModularComponentTemplateModel):
         )
         )
 
 
 
 
-@extras_features('webhooks')
 class RearPortTemplate(ModularComponentTemplateModel):
 class RearPortTemplate(ModularComponentTemplateModel):
     """
     """
     Template for a pass-through port on the rear of a new Device.
     Template for a pass-through port on the rear of a new Device.
@@ -460,7 +453,6 @@ class RearPortTemplate(ModularComponentTemplateModel):
         )
         )
 
 
 
 
-@extras_features('webhooks')
 class ModuleBayTemplate(ComponentTemplateModel):
 class ModuleBayTemplate(ComponentTemplateModel):
     """
     """
     A template for a ModuleBay to be created for a new parent Device.
     A template for a ModuleBay to be created for a new parent Device.
@@ -486,7 +478,6 @@ class ModuleBayTemplate(ComponentTemplateModel):
         )
         )
 
 
 
 
-@extras_features('webhooks')
 class DeviceBayTemplate(ComponentTemplateModel):
 class DeviceBayTemplate(ComponentTemplateModel):
     """
     """
     A template for a DeviceBay to be created for a new parent Device.
     A template for a DeviceBay to be created for a new parent Device.
@@ -511,7 +502,6 @@ class DeviceBayTemplate(ComponentTemplateModel):
             )
             )
 
 
 
 
-@extras_features('webhooks')
 class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
 class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
     """
     """
     A template for an InventoryItem to be created for a new parent Device.
     A template for an InventoryItem to be created for a new parent Device.

+ 0 - 12
netbox/dcim/models/device_components.py

@@ -11,7 +11,6 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.fields import MACAddressField, WWNField
 from dcim.fields import MACAddressField, WWNField
 from dcim.svg import CableTraceSVG
 from dcim.svg import CableTraceSVG
-from extras.utils import extras_features
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
 from utilities.choices import ColorChoices
 from utilities.choices import ColorChoices
 from utilities.fields import ColorField, NaturalOrderingField
 from utilities.fields import ColorField, NaturalOrderingField
@@ -254,7 +253,6 @@ class PathEndpoint(models.Model):
 # Console components
 # Console components
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ConsolePort(ModularComponentModel, LinkTermination, PathEndpoint):
 class ConsolePort(ModularComponentModel, LinkTermination, PathEndpoint):
     """
     """
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
     A physical console port within a Device. ConsolePorts connect to ConsoleServerPorts.
@@ -282,7 +280,6 @@ class ConsolePort(ModularComponentModel, LinkTermination, PathEndpoint):
         return reverse('dcim:consoleport', kwargs={'pk': self.pk})
         return reverse('dcim:consoleport', kwargs={'pk': self.pk})
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ConsoleServerPort(ModularComponentModel, LinkTermination, PathEndpoint):
 class ConsoleServerPort(ModularComponentModel, LinkTermination, PathEndpoint):
     """
     """
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
     A physical port within a Device (typically a designated console server) which provides access to ConsolePorts.
@@ -314,7 +311,6 @@ class ConsoleServerPort(ModularComponentModel, LinkTermination, PathEndpoint):
 # Power components
 # Power components
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
 class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
     """
     """
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
     A physical power supply (intake) port within a Device. PowerPorts connect to PowerOutlets.
@@ -407,7 +403,6 @@ class PowerPort(ModularComponentModel, LinkTermination, PathEndpoint):
         }
         }
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class PowerOutlet(ModularComponentModel, LinkTermination, PathEndpoint):
 class PowerOutlet(ModularComponentModel, LinkTermination, PathEndpoint):
     """
     """
     A physical power outlet (output) within a Device which provides power to a PowerPort.
     A physical power outlet (output) within a Device which provides power to a PowerPort.
@@ -522,7 +517,6 @@ class BaseInterface(models.Model):
         return self.fhrp_group_assignments.count()
         return self.fhrp_group_assignments.count()
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpoint):
 class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpoint):
     """
     """
     A network interface within a Device. A physical Interface can connect to exactly one other Interface.
     A network interface within a Device. A physical Interface can connect to exactly one other Interface.
@@ -793,7 +787,6 @@ class Interface(ModularComponentModel, BaseInterface, LinkTermination, PathEndpo
 # Pass-through ports
 # Pass-through ports
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class FrontPort(ModularComponentModel, LinkTermination):
 class FrontPort(ModularComponentModel, LinkTermination):
     """
     """
     A pass-through port on the front of a Device.
     A pass-through port on the front of a Device.
@@ -847,7 +840,6 @@ class FrontPort(ModularComponentModel, LinkTermination):
             })
             })
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class RearPort(ModularComponentModel, LinkTermination):
 class RearPort(ModularComponentModel, LinkTermination):
     """
     """
     A pass-through port on the rear of a Device.
     A pass-through port on the rear of a Device.
@@ -891,7 +883,6 @@ class RearPort(ModularComponentModel, LinkTermination):
 # Bays
 # Bays
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ModuleBay(ComponentModel):
 class ModuleBay(ComponentModel):
     """
     """
     An empty space within a Device which can house a child device
     An empty space within a Device which can house a child device
@@ -912,7 +903,6 @@ class ModuleBay(ComponentModel):
         return reverse('dcim:modulebay', kwargs={'pk': self.pk})
         return reverse('dcim:modulebay', kwargs={'pk': self.pk})
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class DeviceBay(ComponentModel):
 class DeviceBay(ComponentModel):
     """
     """
     An empty space within a Device which can house a child device
     An empty space within a Device which can house a child device
@@ -963,7 +953,6 @@ class DeviceBay(ComponentModel):
 #
 #
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class InventoryItemRole(OrganizationalModel):
 class InventoryItemRole(OrganizationalModel):
     """
     """
     Inventory items may optionally be assigned a functional role.
     Inventory items may optionally be assigned a functional role.
@@ -994,7 +983,6 @@ class InventoryItemRole(OrganizationalModel):
         return reverse('dcim:inventoryitemrole', args=[self.pk])
         return reverse('dcim:inventoryitemrole', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class InventoryItem(MPTTModel, ComponentModel):
 class InventoryItem(MPTTModel, ComponentModel):
     """
     """
     An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.
     An InventoryItem represents a serialized piece of hardware within a Device, such as a line card or power supply.

+ 0 - 9
netbox/dcim/models/devices.py

@@ -13,7 +13,6 @@ from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from extras.models import ConfigContextModel
 from extras.models import ConfigContextModel
 from extras.querysets import ConfigContextModelQuerySet
 from extras.querysets import ConfigContextModelQuerySet
-from extras.utils import extras_features
 from netbox.config import ConfigItem
 from netbox.config import ConfigItem
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
 from utilities.choices import ColorChoices
 from utilities.choices import ColorChoices
@@ -37,7 +36,6 @@ __all__ = (
 # Device Types
 # Device Types
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Manufacturer(OrganizationalModel):
 class Manufacturer(OrganizationalModel):
     """
     """
     A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
     A Manufacturer represents a company which produces hardware devices; for example, Juniper or Dell.
@@ -70,7 +68,6 @@ class Manufacturer(OrganizationalModel):
         return reverse('dcim:manufacturer', args=[self.pk])
         return reverse('dcim:manufacturer', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class DeviceType(PrimaryModel):
 class DeviceType(PrimaryModel):
     """
     """
     A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
     A DeviceType represents a particular make (Manufacturer) and model of device. It specifies rack height and depth, as
@@ -353,7 +350,6 @@ class DeviceType(PrimaryModel):
         return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
         return self.subdevice_role == SubdeviceRoleChoices.ROLE_CHILD
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ModuleType(PrimaryModel):
 class ModuleType(PrimaryModel):
     """
     """
     A ModuleType represents a hardware element that can be installed within a device and which houses additional
     A ModuleType represents a hardware element that can be installed within a device and which houses additional
@@ -487,7 +483,6 @@ class ModuleType(PrimaryModel):
 # Devices
 # Devices
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class DeviceRole(OrganizationalModel):
 class DeviceRole(OrganizationalModel):
     """
     """
     Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
     Devices are organized by functional role; for example, "Core Switch" or "File Server". Each DeviceRole is assigned a
@@ -525,7 +520,6 @@ class DeviceRole(OrganizationalModel):
         return reverse('dcim:devicerole', args=[self.pk])
         return reverse('dcim:devicerole', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Platform(OrganizationalModel):
 class Platform(OrganizationalModel):
     """
     """
     Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
     Platform refers to the software or firmware running on a Device. For example, "Cisco IOS-XR" or "Juniper Junos".
@@ -575,7 +569,6 @@ class Platform(OrganizationalModel):
         return reverse('dcim:platform', args=[self.pk])
         return reverse('dcim:platform', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Device(PrimaryModel, ConfigContextModel):
 class Device(PrimaryModel, ConfigContextModel):
     """
     """
     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,
@@ -1012,7 +1005,6 @@ class Device(PrimaryModel, ConfigContextModel):
         return DeviceStatusChoices.colors.get(self.status, 'secondary')
         return DeviceStatusChoices.colors.get(self.status, 'secondary')
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Module(PrimaryModel, ConfigContextModel):
 class Module(PrimaryModel, ConfigContextModel):
     """
     """
     A Module represents a field-installable component within a Device which may itself hold multiple device components
     A Module represents a field-installable component within a Device which may itself hold multiple device components
@@ -1095,7 +1087,6 @@ class Module(PrimaryModel, ConfigContextModel):
 # Virtual chassis
 # Virtual chassis
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VirtualChassis(PrimaryModel):
 class VirtualChassis(PrimaryModel):
     """
     """
     A collection of Devices which operate with a shared control plane (e.g. a switch stack).
     A collection of Devices which operate with a shared control plane (e.g. a switch stack).

+ 0 - 3
netbox/dcim/models/power.py

@@ -6,7 +6,6 @@ from django.urls import reverse
 
 
 from dcim.choices import *
 from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
-from extras.utils import extras_features
 from netbox.models import PrimaryModel
 from netbox.models import PrimaryModel
 from utilities.validators import ExclusionValidator
 from utilities.validators import ExclusionValidator
 from .device_components import LinkTermination, PathEndpoint
 from .device_components import LinkTermination, PathEndpoint
@@ -21,7 +20,6 @@ __all__ = (
 # Power
 # Power
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class PowerPanel(PrimaryModel):
 class PowerPanel(PrimaryModel):
     """
     """
     A distribution point for electrical power; e.g. a data center RPP.
     A distribution point for electrical power; e.g. a data center RPP.
@@ -68,7 +66,6 @@ class PowerPanel(PrimaryModel):
             )
             )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class PowerFeed(PrimaryModel, PathEndpoint, LinkTermination):
 class PowerFeed(PrimaryModel, PathEndpoint, LinkTermination):
     """
     """
     An electrical circuit delivered from a PowerPanel.
     An electrical circuit delivered from a PowerPanel.

+ 0 - 4
netbox/dcim/models/racks.py

@@ -13,7 +13,6 @@ from django.urls import reverse
 from dcim.choices import *
 from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
 from dcim.svg import RackElevationSVG
 from dcim.svg import RackElevationSVG
-from extras.utils import extras_features
 from netbox.config import get_config
 from netbox.config import get_config
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
 from utilities.choices import ColorChoices
 from utilities.choices import ColorChoices
@@ -34,7 +33,6 @@ __all__ = (
 # Racks
 # Racks
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class RackRole(OrganizationalModel):
 class RackRole(OrganizationalModel):
     """
     """
     Racks can be organized by functional role, similar to Devices.
     Racks can be organized by functional role, similar to Devices.
@@ -65,7 +63,6 @@ class RackRole(OrganizationalModel):
         return reverse('dcim:rackrole', args=[self.pk])
         return reverse('dcim:rackrole', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Rack(PrimaryModel):
 class Rack(PrimaryModel):
     """
     """
     Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
     Devices are housed within Racks. Each rack has a defined height measured in rack units, and a front and rear face.
@@ -438,7 +435,6 @@ class Rack(PrimaryModel):
         return int(allocated_draw_total / available_power_total * 100)
         return int(allocated_draw_total / available_power_total * 100)
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class RackReservation(PrimaryModel):
 class RackReservation(PrimaryModel):
     """
     """
     One or more reserved units within a Rack.
     One or more reserved units within a Rack.

+ 0 - 6
netbox/dcim/models/sites.py

@@ -7,8 +7,6 @@ from timezone_field import TimeZoneField
 
 
 from dcim.choices import *
 from dcim.choices import *
 from dcim.constants import *
 from dcim.constants import *
-from dcim.fields import ASNField
-from extras.utils import extras_features
 from netbox.models import NestedGroupModel, PrimaryModel
 from netbox.models import NestedGroupModel, PrimaryModel
 from utilities.fields import NaturalOrderingField
 from utilities.fields import NaturalOrderingField
 
 
@@ -24,7 +22,6 @@ __all__ = (
 # Regions
 # Regions
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Region(NestedGroupModel):
 class Region(NestedGroupModel):
     """
     """
     A region represents a geographic collection of sites. For example, you might create regions representing countries,
     A region represents a geographic collection of sites. For example, you might create regions representing countries,
@@ -111,7 +108,6 @@ class Region(NestedGroupModel):
 # Site groups
 # Site groups
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class SiteGroup(NestedGroupModel):
 class SiteGroup(NestedGroupModel):
     """
     """
     A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
     A site group is an arbitrary grouping of sites. For example, you might have corporate sites and customer sites; and
@@ -198,7 +194,6 @@ class SiteGroup(NestedGroupModel):
 # Sites
 # Sites
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Site(PrimaryModel):
 class Site(PrimaryModel):
     """
     """
     A Site represents a geographic location within a network; typically a building or campus. The optional facility
     A Site represents a geographic location within a network; typically a building or campus. The optional facility
@@ -322,7 +317,6 @@ class Site(PrimaryModel):
 # Locations
 # Locations
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Location(NestedGroupModel):
 class Location(NestedGroupModel):
     """
     """
     A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a
     A Location represents a subgroup of Racks and/or Devices within a Site. A Location may represent a building within a

+ 1 - 0
netbox/extras/constants.py

@@ -7,6 +7,7 @@ EXTRAS_FEATURES = [
     'custom_links',
     'custom_links',
     'export_templates',
     'export_templates',
     'job_results',
     'job_results',
+    'journaling',
     'tags',
     'tags',
     'webhooks'
     'webhooks'
 ]
 ]

+ 2 - 3
netbox/extras/models/configcontexts.py

@@ -5,8 +5,8 @@ from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 
 
 from extras.querysets import ConfigContextQuerySet
 from extras.querysets import ConfigContextQuerySet
-from extras.utils import extras_features
 from netbox.models import ChangeLoggedModel
 from netbox.models import ChangeLoggedModel
+from netbox.models.features import WebhooksMixin
 from utilities.utils import deepmerge
 from utilities.utils import deepmerge
 
 
 
 
@@ -20,8 +20,7 @@ __all__ = (
 # Config contexts
 # Config contexts
 #
 #
 
 
-@extras_features('webhooks')
-class ConfigContext(ChangeLoggedModel):
+class ConfigContext(WebhooksMixin, ChangeLoggedModel):
     """
     """
     A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
     A ConfigContext represents a set of arbitrary data available to any Device or VirtualMachine matching its assigned
     qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B
     qualifiers (region, site, etc.). For example, the data stored in a ConfigContext assigned to site A and tenant B

+ 3 - 3
netbox/extras/models/customfields.py

@@ -12,8 +12,9 @@ from django.utils.html import escape
 from django.utils.safestring import mark_safe
 from django.utils.safestring import mark_safe
 
 
 from extras.choices import *
 from extras.choices import *
-from extras.utils import FeatureQuery, extras_features
+from extras.utils import FeatureQuery
 from netbox.models import ChangeLoggedModel
 from netbox.models import ChangeLoggedModel
+from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
 from utilities import filters
 from utilities import filters
 from utilities.forms import (
 from utilities.forms import (
     CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
     CSVChoiceField, CSVMultipleChoiceField, DatePicker, DynamicModelChoiceField, DynamicModelMultipleChoiceField,
@@ -40,8 +41,7 @@ class CustomFieldManager(models.Manager.from_queryset(RestrictedQuerySet)):
         return self.get_queryset().filter(content_types=content_type)
         return self.get_queryset().filter(content_types=content_type)
 
 
 
 
-@extras_features('webhooks', 'export_templates')
-class CustomField(ChangeLoggedModel):
+class CustomField(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
     content_types = models.ManyToManyField(
     content_types = models.ManyToManyField(
         to=ContentType,
         to=ContentType,
         related_name='custom_fields',
         related_name='custom_fields',

+ 9 - 15
netbox/extras/models/models.py

@@ -17,8 +17,9 @@ from rest_framework.utils.encoders import JSONEncoder
 from extras.choices import *
 from extras.choices import *
 from extras.constants import *
 from extras.constants import *
 from extras.conditions import ConditionSet
 from extras.conditions import ConditionSet
-from extras.utils import extras_features, FeatureQuery, image_upload
+from extras.utils import FeatureQuery, image_upload
 from netbox.models import BigIDModel, ChangeLoggedModel
 from netbox.models import BigIDModel, ChangeLoggedModel
+from netbox.models.features import ExportTemplatesMixin, JobResultsMixin, WebhooksMixin
 from utilities.querysets import RestrictedQuerySet
 from utilities.querysets import RestrictedQuerySet
 from utilities.utils import render_jinja2
 from utilities.utils import render_jinja2
 
 
@@ -35,8 +36,7 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('webhooks', 'export_templates')
-class Webhook(ChangeLoggedModel):
+class Webhook(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
     """
     """
     A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
     A Webhook defines a request that will be sent to a remote application when an object is created, updated, and/or
     delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
     delete in NetBox. The request will contain a representation of the object, which the remote application can act on.
@@ -184,8 +184,7 @@ class Webhook(ChangeLoggedModel):
         return render_jinja2(self.payload_url, context)
         return render_jinja2(self.payload_url, context)
 
 
 
 
-@extras_features('webhooks', 'export_templates')
-class CustomLink(ChangeLoggedModel):
+class CustomLink(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
     """
     """
     A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
     A custom link to an external representation of a NetBox object. The link text and URL fields accept Jinja2 template
     code to be rendered with an object as context.
     code to be rendered with an object as context.
@@ -258,8 +257,7 @@ class CustomLink(ChangeLoggedModel):
         }
         }
 
 
 
 
-@extras_features('webhooks', 'export_templates')
-class ExportTemplate(ChangeLoggedModel):
+class ExportTemplate(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel):
     content_type = models.ForeignKey(
     content_type = models.ForeignKey(
         to=ContentType,
         to=ContentType,
         on_delete=models.CASCADE,
         on_delete=models.CASCADE,
@@ -345,8 +343,7 @@ class ExportTemplate(ChangeLoggedModel):
         return response
         return response
 
 
 
 
-@extras_features('webhooks')
-class ImageAttachment(ChangeLoggedModel):
+class ImageAttachment(WebhooksMixin, ChangeLoggedModel):
     """
     """
     An uploaded image which is associated with an object.
     An uploaded image which is associated with an object.
     """
     """
@@ -424,8 +421,7 @@ class ImageAttachment(ChangeLoggedModel):
         return super().to_objectchange(action, related_object=self.parent)
         return super().to_objectchange(action, related_object=self.parent)
 
 
 
 
-@extras_features('webhooks')
-class JournalEntry(ChangeLoggedModel):
+class JournalEntry(WebhooksMixin, ChangeLoggedModel):
     """
     """
     A historical remark concerning an object; collectively, these form an object's journal. The journal is used to
     A historical remark concerning an object; collectively, these form an object's journal. The journal is used to
     preserve historical context around an object, and complements NetBox's built-in change logging. For example, you
     preserve historical context around an object, and complements NetBox's built-in change logging. For example, you
@@ -603,8 +599,7 @@ class ConfigRevision(models.Model):
 # Custom scripts & reports
 # Custom scripts & reports
 #
 #
 
 
-@extras_features('job_results')
-class Script(models.Model):
+class Script(JobResultsMixin, models.Model):
     """
     """
     Dummy model used to generate permissions for custom scripts. Does not exist in the database.
     Dummy model used to generate permissions for custom scripts. Does not exist in the database.
     """
     """
@@ -616,8 +611,7 @@ class Script(models.Model):
 # Reports
 # Reports
 #
 #
 
 
-@extras_features('job_results')
-class Report(models.Model):
+class Report(JobResultsMixin, models.Model):
     """
     """
     Dummy model used to generate permissions for reports. Does not exist in the database.
     Dummy model used to generate permissions for reports. Does not exist in the database.
     """
     """

+ 2 - 3
netbox/extras/models/tags.py

@@ -3,8 +3,8 @@ from django.urls import reverse
 from django.utils.text import slugify
 from django.utils.text import slugify
 from taggit.models import TagBase, GenericTaggedItemBase
 from taggit.models import TagBase, GenericTaggedItemBase
 
 
-from extras.utils import extras_features
 from netbox.models import BigIDModel, ChangeLoggedModel
 from netbox.models import BigIDModel, ChangeLoggedModel
+from netbox.models.features import ExportTemplatesMixin, WebhooksMixin
 from utilities.choices import ColorChoices
 from utilities.choices import ColorChoices
 from utilities.fields import ColorField
 from utilities.fields import ColorField
 
 
@@ -13,8 +13,7 @@ from utilities.fields import ColorField
 # Tags
 # Tags
 #
 #
 
 
-@extras_features('webhooks', 'export_templates')
-class Tag(ChangeLoggedModel, TagBase):
+class Tag(ExportTemplatesMixin, WebhooksMixin, ChangeLoggedModel, TagBase):
     color = ColorField(
     color = ColorField(
         default=ColorChoices.COLOR_GREY
         default=ColorChoices.COLOR_GREY
     )
     )

+ 0 - 11
netbox/extras/utils.py

@@ -67,14 +67,3 @@ def register_features(model, features):
             raise ValueError(f"{feature} is not a valid extras feature!")
             raise ValueError(f"{feature} is not a valid extras feature!")
         app_label, model_name = model._meta.label_lower.split('.')
         app_label, model_name = model._meta.label_lower.split('.')
         registry['model_features'][feature][app_label].append(model_name)
         registry['model_features'][feature][app_label].append(model_name)
-
-
-def extras_features(*features):
-    """
-    Decorator used to register extras provided features to a model
-    """
-    def wrapper(model_class):
-        # Initialize the model_features store if not already defined
-        register_features(model_class, features)
-        return model_class
-    return wrapper

+ 2 - 4
netbox/ipam/models/fhrp.py

@@ -4,8 +4,8 @@ from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 
 
-from extras.utils import extras_features
 from netbox.models import ChangeLoggedModel, PrimaryModel
 from netbox.models import ChangeLoggedModel, PrimaryModel
+from netbox.models.features import WebhooksMixin
 from ipam.choices import *
 from ipam.choices import *
 from ipam.constants import *
 from ipam.constants import *
 
 
@@ -15,7 +15,6 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class FHRPGroup(PrimaryModel):
 class FHRPGroup(PrimaryModel):
     """
     """
     A grouping of next hope resolution protocol (FHRP) peers. (For instance, VRRP or HSRP.)
     A grouping of next hope resolution protocol (FHRP) peers. (For instance, VRRP or HSRP.)
@@ -70,8 +69,7 @@ class FHRPGroup(PrimaryModel):
         return reverse('ipam:fhrpgroup', args=[self.pk])
         return reverse('ipam:fhrpgroup', args=[self.pk])
 
 
 
 
-@extras_features('webhooks')
-class FHRPGroupAssignment(ChangeLoggedModel):
+class FHRPGroupAssignment(WebhooksMixin, ChangeLoggedModel):
     interface_type = models.ForeignKey(
     interface_type = models.ForeignKey(
         to=ContentType,
         to=ContentType,
         on_delete=models.CASCADE
         on_delete=models.CASCADE

+ 0 - 8
netbox/ipam/models/ip.py

@@ -9,7 +9,6 @@ from django.utils.functional import cached_property
 
 
 from dcim.fields import ASNField
 from dcim.fields import ASNField
 from dcim.models import Device
 from dcim.models import Device
-from extras.utils import extras_features
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
 from ipam.choices import *
 from ipam.choices import *
 from ipam.constants import *
 from ipam.constants import *
@@ -54,7 +53,6 @@ class GetAvailablePrefixesMixin:
         return available_prefixes.iter_cidrs()[0]
         return available_prefixes.iter_cidrs()[0]
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class RIR(OrganizationalModel):
 class RIR(OrganizationalModel):
     """
     """
     A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
     A Regional Internet Registry (RIR) is responsible for the allocation of a large portion of the global IP address
@@ -90,7 +88,6 @@ class RIR(OrganizationalModel):
         return reverse('ipam:rir', args=[self.pk])
         return reverse('ipam:rir', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ASN(PrimaryModel):
 class ASN(PrimaryModel):
     """
     """
     An autonomous system (AS) number is typically used to represent an independent routing domain. A site can have
     An autonomous system (AS) number is typically used to represent an independent routing domain. A site can have
@@ -150,7 +147,6 @@ class ASN(PrimaryModel):
             return self.asn
             return self.asn
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
 class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
     """
     """
     An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
     An aggregate exists at the root level of the IP address space hierarchy in NetBox. Aggregates are used to organize
@@ -253,7 +249,6 @@ class Aggregate(GetAvailablePrefixesMixin, PrimaryModel):
         return min(utilization, 100)
         return min(utilization, 100)
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Role(OrganizationalModel):
 class Role(OrganizationalModel):
     """
     """
     A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
     A Role represents the functional role of a Prefix or VLAN; for example, "Customer," "Infrastructure," or
@@ -285,7 +280,6 @@ class Role(OrganizationalModel):
         return reverse('ipam:role', args=[self.pk])
         return reverse('ipam:role', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
 class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
     """
     """
     A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
     A Prefix represents an IPv4 or IPv6 network, including mask length. Prefixes can optionally be assigned to Sites and
@@ -563,7 +557,6 @@ class Prefix(GetAvailablePrefixesMixin, PrimaryModel):
         return min(utilization, 100)
         return min(utilization, 100)
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class IPRange(PrimaryModel):
 class IPRange(PrimaryModel):
     """
     """
     A range of IP addresses, defined by start and end addresses.
     A range of IP addresses, defined by start and end addresses.
@@ -759,7 +752,6 @@ class IPRange(PrimaryModel):
         return int(float(child_count) / self.size * 100)
         return int(float(child_count) / self.size * 100)
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class IPAddress(PrimaryModel):
 class IPAddress(PrimaryModel):
     """
     """
     An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is
     An IPAddress represents an individual IPv4 or IPv6 address and its mask. The mask length should match what is

+ 0 - 3
netbox/ipam/models/services.py

@@ -4,7 +4,6 @@ from django.core.validators import MaxValueValidator, MinValueValidator
 from django.db import models
 from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 
 
-from extras.utils import extras_features
 from ipam.choices import *
 from ipam.choices import *
 from ipam.constants import *
 from ipam.constants import *
 from netbox.models import PrimaryModel
 from netbox.models import PrimaryModel
@@ -47,7 +46,6 @@ class ServiceBase(models.Model):
         return array_to_string(self.ports)
         return array_to_string(self.ports)
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ServiceTemplate(ServiceBase, PrimaryModel):
 class ServiceTemplate(ServiceBase, PrimaryModel):
     """
     """
     A template for a Service to be applied to a device or virtual machine.
     A template for a Service to be applied to a device or virtual machine.
@@ -64,7 +62,6 @@ class ServiceTemplate(ServiceBase, PrimaryModel):
         return reverse('ipam:servicetemplate', args=[self.pk])
         return reverse('ipam:servicetemplate', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Service(ServiceBase, PrimaryModel):
 class Service(ServiceBase, PrimaryModel):
     """
     """
     A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may
     A Service represents a layer-four service (e.g. HTTP or SSH) running on a Device or VirtualMachine. A Service may

+ 0 - 3
netbox/ipam/models/vlans.py

@@ -6,7 +6,6 @@ from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 
 
 from dcim.models import Interface
 from dcim.models import Interface
-from extras.utils import extras_features
 from ipam.choices import *
 from ipam.choices import *
 from ipam.constants import *
 from ipam.constants import *
 from ipam.querysets import VLANQuerySet
 from ipam.querysets import VLANQuerySet
@@ -20,7 +19,6 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VLANGroup(OrganizationalModel):
 class VLANGroup(OrganizationalModel):
     """
     """
     A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
     A VLAN group is an arbitrary collection of VLANs within which VLAN IDs and names must be unique.
@@ -118,7 +116,6 @@ class VLANGroup(OrganizationalModel):
         return None
         return None
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VLAN(PrimaryModel):
 class VLAN(PrimaryModel):
     """
     """
     A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned
     A VLAN is a distinct layer two forwarding domain identified by a 12-bit integer (1-4094). Each VLAN must be assigned

+ 0 - 3
netbox/ipam/models/vrfs.py

@@ -1,7 +1,6 @@
 from django.db import models
 from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 
 
-from extras.utils import extras_features
 from ipam.constants import *
 from ipam.constants import *
 from netbox.models import PrimaryModel
 from netbox.models import PrimaryModel
 
 
@@ -12,7 +11,6 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VRF(PrimaryModel):
 class VRF(PrimaryModel):
     """
     """
     A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
     A virtual routing and forwarding (VRF) table represents a discrete layer three forwarding domain (e.g. a routing
@@ -75,7 +73,6 @@ class VRF(PrimaryModel):
         return reverse('ipam:vrf', args=[self.pk])
         return reverse('ipam:vrf', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class RouteTarget(PrimaryModel):
 class RouteTarget(PrimaryModel):
     """
     """
     A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.
     A BGP extended community used to control the redistribution of routes among VRFs, as defined in RFC 4364.

+ 15 - 10
netbox/netbox/models/__init__.py

@@ -1,4 +1,3 @@
-from django.contrib.contenttypes.fields import GenericRelation
 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 mptt.models import MPTTModel, TreeForeignKey
@@ -20,6 +19,18 @@ __all__ = (
 # Base model classes
 # Base model classes
 #
 #
 
 
+class BaseModel(
+    CustomFieldsMixin,
+    CustomLinksMixin,
+    ExportTemplatesMixin,
+    JournalingMixin,
+    TagsMixin,
+    WebhooksMixin,
+):
+    class Meta:
+        abstract = True
+
+
 class BigIDModel(models.Model):
 class BigIDModel(models.Model):
     """
     """
     Abstract base model for all data objects. Ensures the use of a 64-bit PK.
     Abstract base model for all data objects. Ensures the use of a 64-bit PK.
@@ -42,23 +53,17 @@ class ChangeLoggedModel(ChangeLoggingMixin, CustomValidationMixin, BigIDModel):
         abstract = True
         abstract = True
 
 
 
 
-class PrimaryModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
+class PrimaryModel(BaseModel, ChangeLoggingMixin, CustomValidationMixin, BigIDModel):
     """
     """
     Primary models represent real objects within the infrastructure being modeled.
     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()
     objects = RestrictedQuerySet.as_manager()
 
 
     class Meta:
     class Meta:
         abstract = True
         abstract = True
 
 
 
 
-class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel, MPTTModel):
+class NestedGroupModel(BaseModel, ChangeLoggingMixin, CustomValidationMixin, BigIDModel, MPTTModel):
     """
     """
     Base model for objects which are used to form a hierarchy (regions, locations, etc.). These models nest
     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.
     recursively using MPTT. Within each parent, each child instance must have a unique name.
@@ -100,7 +105,7 @@ class NestedGroupModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMi
             })
             })
 
 
 
 
-class OrganizationalModel(ChangeLoggingMixin, CustomFieldsMixin, CustomValidationMixin, TagsMixin, BigIDModel):
+class OrganizationalModel(BaseModel, ChangeLoggingMixin, CustomValidationMixin, BigIDModel):
     """
     """
     Organizational models are those which are used solely to categorize and qualify other objects, and do not convey
     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
     any real information about the infrastructure being modeled (for example, functional device roles). Organizational

+ 17 - 0
netbox/netbox/models/features.py

@@ -1,5 +1,6 @@
 import logging
 import logging
 
 
+from django.contrib.contenttypes.fields import GenericRelation
 from django.db.models.signals import class_prepared
 from django.db.models.signals import class_prepared
 from django.dispatch import receiver
 from django.dispatch import receiver
 
 
@@ -20,6 +21,7 @@ __all__ = (
     'CustomValidationMixin',
     'CustomValidationMixin',
     'ExportTemplatesMixin',
     'ExportTemplatesMixin',
     'JobResultsMixin',
     'JobResultsMixin',
+    'JournalingMixin',
     'TagsMixin',
     'TagsMixin',
     'WebhooksMixin',
     'WebhooksMixin',
 )
 )
@@ -169,6 +171,20 @@ class JobResultsMixin(models.Model):
         abstract = True
         abstract = True
 
 
 
 
+class JournalingMixin(models.Model):
+    """
+    Enables support for JournalEntry assignment.
+    """
+    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):
 class TagsMixin(models.Model):
     """
     """
     Enable the assignment of Tags to a model.
     Enable the assignment of Tags to a model.
@@ -194,6 +210,7 @@ FEATURES_MAP = (
     ('custom_links', CustomLinksMixin),
     ('custom_links', CustomLinksMixin),
     ('export_templates', ExportTemplatesMixin),
     ('export_templates', ExportTemplatesMixin),
     ('job_results', JobResultsMixin),
     ('job_results', JobResultsMixin),
+    ('journaling', JournalingMixin),
     ('tags', TagsMixin),
     ('tags', TagsMixin),
     ('webhooks', WebhooksMixin),
     ('webhooks', WebhooksMixin),
 )
 )

+ 2 - 6
netbox/tenancy/models/contacts.py

@@ -4,8 +4,8 @@ from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 from mptt.models import TreeForeignKey
 from mptt.models import TreeForeignKey
 
 
-from extras.utils import extras_features
 from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel
 from netbox.models import ChangeLoggedModel, NestedGroupModel, OrganizationalModel, PrimaryModel
+from netbox.models.features import WebhooksMixin
 from tenancy.choices import *
 from tenancy.choices import *
 
 
 __all__ = (
 __all__ = (
@@ -16,7 +16,6 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ContactGroup(NestedGroupModel):
 class ContactGroup(NestedGroupModel):
     """
     """
     An arbitrary collection of Contacts.
     An arbitrary collection of Contacts.
@@ -50,7 +49,6 @@ class ContactGroup(NestedGroupModel):
         return reverse('tenancy:contactgroup', args=[self.pk])
         return reverse('tenancy:contactgroup', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ContactRole(OrganizationalModel):
 class ContactRole(OrganizationalModel):
     """
     """
     Functional role for a Contact assigned to an object.
     Functional role for a Contact assigned to an object.
@@ -78,7 +76,6 @@ class ContactRole(OrganizationalModel):
         return reverse('tenancy:contactrole', args=[self.pk])
         return reverse('tenancy:contactrole', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Contact(PrimaryModel):
 class Contact(PrimaryModel):
     """
     """
     Contact information for a particular object(s) in NetBox.
     Contact information for a particular object(s) in NetBox.
@@ -129,8 +126,7 @@ class Contact(PrimaryModel):
         return reverse('tenancy:contact', args=[self.pk])
         return reverse('tenancy:contact', args=[self.pk])
 
 
 
 
-@extras_features('webhooks')
-class ContactAssignment(ChangeLoggedModel):
+class ContactAssignment(WebhooksMixin, ChangeLoggedModel):
     content_type = models.ForeignKey(
     content_type = models.ForeignKey(
         to=ContentType,
         to=ContentType,
         on_delete=models.CASCADE
         on_delete=models.CASCADE

+ 0 - 3
netbox/tenancy/models/tenants.py

@@ -3,7 +3,6 @@ from django.db import models
 from django.urls import reverse
 from django.urls import reverse
 from mptt.models import TreeForeignKey
 from mptt.models import TreeForeignKey
 
 
-from extras.utils import extras_features
 from netbox.models import NestedGroupModel, PrimaryModel
 from netbox.models import NestedGroupModel, PrimaryModel
 
 
 __all__ = (
 __all__ = (
@@ -12,7 +11,6 @@ __all__ = (
 )
 )
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class TenantGroup(NestedGroupModel):
 class TenantGroup(NestedGroupModel):
     """
     """
     An arbitrary collection of Tenants.
     An arbitrary collection of Tenants.
@@ -45,7 +43,6 @@ class TenantGroup(NestedGroupModel):
         return reverse('tenancy:tenantgroup', args=[self.pk])
         return reverse('tenancy:tenantgroup', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Tenant(PrimaryModel):
 class Tenant(PrimaryModel):
     """
     """
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal
     A Tenant represents an organization served by the NetBox owner. This is typically a customer or an internal

+ 0 - 7
netbox/virtualization/models.py

@@ -7,7 +7,6 @@ from django.urls import reverse
 from dcim.models import BaseInterface, Device
 from dcim.models import BaseInterface, Device
 from extras.models import ConfigContextModel
 from extras.models import ConfigContextModel
 from extras.querysets import ConfigContextModelQuerySet
 from extras.querysets import ConfigContextModelQuerySet
-from extras.utils import extras_features
 from netbox.config import get_config
 from netbox.config import get_config
 from netbox.models import OrganizationalModel, PrimaryModel
 from netbox.models import OrganizationalModel, PrimaryModel
 from utilities.fields import NaturalOrderingField
 from utilities.fields import NaturalOrderingField
@@ -15,7 +14,6 @@ from utilities.ordering import naturalize_interface
 from utilities.query_functions import CollateAsChar
 from utilities.query_functions import CollateAsChar
 from .choices import *
 from .choices import *
 
 
-
 __all__ = (
 __all__ = (
     'Cluster',
     'Cluster',
     'ClusterGroup',
     'ClusterGroup',
@@ -29,7 +27,6 @@ __all__ = (
 # Cluster types
 # Cluster types
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ClusterType(OrganizationalModel):
 class ClusterType(OrganizationalModel):
     """
     """
     A type of Cluster.
     A type of Cluster.
@@ -61,7 +58,6 @@ class ClusterType(OrganizationalModel):
 # Cluster groups
 # Cluster groups
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class ClusterGroup(OrganizationalModel):
 class ClusterGroup(OrganizationalModel):
     """
     """
     An organizational group of Clusters.
     An organizational group of Clusters.
@@ -104,7 +100,6 @@ class ClusterGroup(OrganizationalModel):
 # Clusters
 # Clusters
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class Cluster(PrimaryModel):
 class Cluster(PrimaryModel):
     """
     """
     A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
     A cluster of VirtualMachines. Each Cluster may optionally be associated with one or more Devices.
@@ -188,7 +183,6 @@ class Cluster(PrimaryModel):
 # Virtual machines
 # Virtual machines
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VirtualMachine(PrimaryModel, ConfigContextModel):
 class VirtualMachine(PrimaryModel, ConfigContextModel):
     """
     """
     A virtual machine which runs inside a Cluster.
     A virtual machine which runs inside a Cluster.
@@ -351,7 +345,6 @@ class VirtualMachine(PrimaryModel, ConfigContextModel):
 # Interfaces
 # Interfaces
 #
 #
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class VMInterface(PrimaryModel, BaseInterface):
 class VMInterface(PrimaryModel, BaseInterface):
     virtual_machine = models.ForeignKey(
     virtual_machine = models.ForeignKey(
         to='virtualization.VirtualMachine',
         to='virtualization.VirtualMachine',

+ 0 - 4
netbox/wireless/models.py

@@ -5,7 +5,6 @@ from mptt.models import MPTTModel, TreeForeignKey
 
 
 from dcim.choices import LinkStatusChoices
 from dcim.choices import LinkStatusChoices
 from dcim.constants import WIRELESS_IFACE_TYPES
 from dcim.constants import WIRELESS_IFACE_TYPES
-from extras.utils import extras_features
 from netbox.models import BigIDModel, NestedGroupModel, PrimaryModel
 from netbox.models import BigIDModel, NestedGroupModel, PrimaryModel
 from .choices import *
 from .choices import *
 from .constants import *
 from .constants import *
@@ -41,7 +40,6 @@ class WirelessAuthenticationBase(models.Model):
         abstract = True
         abstract = True
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class WirelessLANGroup(NestedGroupModel):
 class WirelessLANGroup(NestedGroupModel):
     """
     """
     A nested grouping of WirelessLANs
     A nested grouping of WirelessLANs
@@ -81,7 +79,6 @@ class WirelessLANGroup(NestedGroupModel):
         return reverse('wireless:wirelesslangroup', args=[self.pk])
         return reverse('wireless:wirelesslangroup', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
 class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
     """
     """
     A wireless network formed among an arbitrary number of access point and clients.
     A wireless network formed among an arbitrary number of access point and clients.
@@ -120,7 +117,6 @@ class WirelessLAN(WirelessAuthenticationBase, PrimaryModel):
         return reverse('wireless:wirelesslan', args=[self.pk])
         return reverse('wireless:wirelesslan', args=[self.pk])
 
 
 
 
-@extras_features('custom_fields', 'custom_links', 'export_templates', 'tags', 'webhooks')
 class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
 class WirelessLink(WirelessAuthenticationBase, PrimaryModel):
     """
     """
     A point-to-point connection between two wireless Interfaces.
     A point-to-point connection between two wireless Interfaces.