| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899 |
- from django.contrib.contenttypes.fields import GenericForeignKey
- from django.core.exceptions import ValidationError
- from django.core.validators import MaxValueValidator, MinValueValidator
- from django.db import models
- from django.utils.translation import gettext_lazy as _
- from mptt.models import MPTTModel, TreeForeignKey
- from dcim.choices import *
- from dcim.constants import *
- from dcim.models.base import PortMappingBase
- from dcim.models.mixins import InterfaceValidationMixin
- from netbox.models import ChangeLoggedModel
- from utilities.fields import ColorField, NaturalOrderingField
- from utilities.mptt import TreeManager
- from utilities.ordering import naturalize_interface
- from utilities.tracking import TrackingModelMixin
- from wireless.choices import WirelessRoleChoices
- from .device_components import (
- ConsolePort, ConsoleServerPort, DeviceBay, FrontPort, Interface, InventoryItem, ModuleBay, PowerOutlet, PowerPort,
- RearPort,
- )
- __all__ = (
- 'ConsolePortTemplate',
- 'ConsoleServerPortTemplate',
- 'DeviceBayTemplate',
- 'FrontPortTemplate',
- 'InterfaceTemplate',
- 'InventoryItemTemplate',
- 'ModuleBayTemplate',
- 'PortTemplateMapping',
- 'PowerOutletTemplate',
- 'PowerPortTemplate',
- 'RearPortTemplate',
- )
- class ComponentTemplateModel(ChangeLoggedModel, TrackingModelMixin):
- device_type = models.ForeignKey(
- to='dcim.DeviceType',
- on_delete=models.CASCADE,
- related_name='%(class)ss'
- )
- name = models.CharField(
- verbose_name=_('name'),
- max_length=64,
- help_text=_(
- "{module} is accepted as a substitution for the module bay position when attached to a module type."
- ),
- db_collation="natural_sort"
- )
- label = models.CharField(
- verbose_name=_('label'),
- max_length=64,
- blank=True,
- help_text=_('Physical label')
- )
- description = models.CharField(
- verbose_name=_('description'),
- max_length=200,
- blank=True
- )
- class Meta:
- abstract = True
- ordering = ('device_type', 'name')
- constraints = (
- models.UniqueConstraint(
- fields=('device_type', 'name'),
- name='%(app_label)s_%(class)s_unique_device_type_name'
- ),
- )
- def __str__(self):
- if self.label:
- return f"{self.name} ({self.label})"
- return self.name
- def instantiate(self, device):
- """
- Instantiate a new component on the specified Device.
- """
- raise NotImplementedError()
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- # Cache the original DeviceType ID for reference under clean()
- self._original_device_type = self.__dict__.get('device_type_id')
- def to_objectchange(self, action):
- objectchange = super().to_objectchange(action)
- objectchange.related_object = self.device_type
- return objectchange
- def clean(self):
- super().clean()
- if not self._state.adding and self._original_device_type != self.device_type_id:
- raise ValidationError({
- "device_type": _("Component templates cannot be moved to a different device type.")
- })
- class ModularComponentTemplateModel(ComponentTemplateModel):
- """
- A ComponentTemplateModel which supports optional assignment to a ModuleType.
- """
- device_type = models.ForeignKey(
- to='dcim.DeviceType',
- on_delete=models.CASCADE,
- related_name='%(class)ss',
- blank=True,
- null=True
- )
- module_type = models.ForeignKey(
- to='dcim.ModuleType',
- on_delete=models.CASCADE,
- related_name='%(class)ss',
- blank=True,
- null=True
- )
- class Meta:
- abstract = True
- ordering = ('device_type', 'module_type', 'name')
- constraints = (
- models.UniqueConstraint(
- fields=('device_type', 'name'),
- name='%(app_label)s_%(class)s_unique_device_type_name'
- ),
- models.UniqueConstraint(
- fields=('module_type', 'name'),
- name='%(app_label)s_%(class)s_unique_module_type_name'
- ),
- )
- def to_objectchange(self, action):
- objectchange = super().to_objectchange(action)
- if self.device_type is not None:
- objectchange.related_object = self.device_type
- elif self.module_type is not None:
- objectchange.related_object = self.module_type
- return objectchange
- def clean(self):
- super().clean()
- # A component template must belong to a DeviceType *or* to a ModuleType
- if self.device_type and self.module_type:
- raise ValidationError(
- _("A component template cannot be associated with both a device type and a module type.")
- )
- if not self.device_type and not self.module_type:
- raise ValidationError(
- _("A component template must be associated with either a device type or a module type.")
- )
- def _get_module_tree(self, module):
- modules = []
- while module:
- modules.append(module)
- if module.module_bay:
- module = module.module_bay.module
- else:
- module = None
- modules.reverse()
- return modules
- def resolve_name(self, module):
- if MODULE_TOKEN not in self.name:
- return self.name
- if module:
- modules = self._get_module_tree(module)
- token_count = self.name.count(MODULE_TOKEN)
- name = self.name
- if token_count == 1:
- # Single token: substitute with full path (e.g., "1/1" for depth 2)
- full_path = '/'.join([m.module_bay.position for m in modules])
- name = name.replace(MODULE_TOKEN, full_path, 1)
- else:
- # Multiple tokens: substitute level-by-level (existing behavior)
- for m in modules:
- name = name.replace(MODULE_TOKEN, m.module_bay.position, 1)
- return name
- return self.name
- def resolve_label(self, module):
- if MODULE_TOKEN not in self.label:
- return self.label
- if module:
- modules = self._get_module_tree(module)
- token_count = self.label.count(MODULE_TOKEN)
- label = self.label
- if token_count == 1:
- # Single token: substitute with full path (e.g., "1/1" for depth 2)
- full_path = '/'.join([m.module_bay.position for m in modules])
- label = label.replace(MODULE_TOKEN, full_path, 1)
- else:
- # Multiple tokens: substitute level-by-level (existing behavior)
- for m in modules:
- label = label.replace(MODULE_TOKEN, m.module_bay.position, 1)
- return label
- return self.label
- def resolve_position(self, position, module):
- """
- Resolve {module} placeholder in position field.
- This is used by ModuleBayTemplate to resolve positions like "{module}/1"
- to actual values like "A/1" when the parent module is installed in bay "A".
- Fixes Issue #20467.
- """
- if not position or MODULE_TOKEN not in position:
- return position
- if module:
- modules = self._get_module_tree(module)
- token_count = position.count(MODULE_TOKEN)
- if token_count == 1:
- # Single token: substitute with full path
- full_path = '/'.join([m.module_bay.position for m in modules])
- position = position.replace(MODULE_TOKEN, full_path, 1)
- else:
- # Multiple tokens: substitute level-by-level
- for m in modules:
- position = position.replace(MODULE_TOKEN, m.module_bay.position, 1)
- return position
- return position
- class ConsolePortTemplate(ModularComponentTemplateModel):
- """
- A template for a ConsolePort to be created for a new Device.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=ConsolePortTypeChoices,
- blank=True,
- null=True
- )
- component_model = ConsolePort
- class Meta(ModularComponentTemplateModel.Meta):
- verbose_name = _('console port template')
- verbose_name_plural = _('console port templates')
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.resolve_label(kwargs.get('module')),
- type=self.type,
- **kwargs
- )
- def to_yaml(self):
- return {
- 'name': self.name,
- 'type': self.type,
- 'label': self.label,
- 'description': self.description,
- }
- class ConsoleServerPortTemplate(ModularComponentTemplateModel):
- """
- A template for a ConsoleServerPort to be created for a new Device.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=ConsolePortTypeChoices,
- blank=True,
- null=True
- )
- component_model = ConsoleServerPort
- class Meta(ModularComponentTemplateModel.Meta):
- verbose_name = _('console server port template')
- verbose_name_plural = _('console server port templates')
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.resolve_label(kwargs.get('module')),
- type=self.type,
- **kwargs
- )
- instantiate.do_not_call_in_templates = True
- def to_yaml(self):
- return {
- 'name': self.name,
- 'type': self.type,
- 'label': self.label,
- 'description': self.description,
- }
- class PowerPortTemplate(ModularComponentTemplateModel):
- """
- A template for a PowerPort to be created for a new Device.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=PowerPortTypeChoices,
- blank=True,
- null=True
- )
- maximum_draw = models.PositiveIntegerField(
- verbose_name=_('maximum draw'),
- blank=True,
- null=True,
- validators=[MinValueValidator(1)],
- help_text=_('Maximum power draw (watts)')
- )
- allocated_draw = models.PositiveIntegerField(
- verbose_name=_('allocated draw'),
- blank=True,
- null=True,
- validators=[MinValueValidator(1)],
- help_text=_('Allocated power draw (watts)')
- )
- component_model = PowerPort
- class Meta(ModularComponentTemplateModel.Meta):
- verbose_name = _('power port template')
- verbose_name_plural = _('power port templates')
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.resolve_label(kwargs.get('module')),
- type=self.type,
- maximum_draw=self.maximum_draw,
- allocated_draw=self.allocated_draw,
- **kwargs
- )
- instantiate.do_not_call_in_templates = True
- def clean(self):
- super().clean()
- if self.maximum_draw is not None and self.allocated_draw is not None:
- if self.allocated_draw > self.maximum_draw:
- raise ValidationError({
- 'allocated_draw': _(
- "Allocated draw cannot exceed the maximum draw ({maximum_draw}W)."
- ).format(maximum_draw=self.maximum_draw)
- })
- def to_yaml(self):
- return {
- 'name': self.name,
- 'type': self.type,
- 'maximum_draw': self.maximum_draw,
- 'allocated_draw': self.allocated_draw,
- 'label': self.label,
- 'description': self.description,
- }
- class PowerOutletTemplate(ModularComponentTemplateModel):
- """
- A template for a PowerOutlet to be created for a new Device.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=PowerOutletTypeChoices,
- blank=True,
- null=True
- )
- color = ColorField(
- verbose_name=_('color'),
- blank=True
- )
- power_port = models.ForeignKey(
- to='dcim.PowerPortTemplate',
- on_delete=models.SET_NULL,
- blank=True,
- null=True,
- related_name='poweroutlet_templates'
- )
- feed_leg = models.CharField(
- verbose_name=_('feed leg'),
- max_length=50,
- choices=PowerOutletFeedLegChoices,
- blank=True,
- null=True,
- help_text=_('Phase (for three-phase feeds)')
- )
- component_model = PowerOutlet
- class Meta(ModularComponentTemplateModel.Meta):
- verbose_name = _('power outlet template')
- verbose_name_plural = _('power outlet templates')
- def clean(self):
- super().clean()
- # Validate power port assignment
- if self.power_port:
- if self.device_type and self.power_port.device_type != self.device_type:
- raise ValidationError(
- _("Parent power port ({power_port}) must belong to the same device type").format(
- power_port=self.power_port
- )
- )
- if self.module_type and self.power_port.module_type != self.module_type:
- raise ValidationError(
- _("Parent power port ({power_port}) must belong to the same module type").format(
- power_port=self.power_port
- )
- )
- def instantiate(self, **kwargs):
- if self.power_port:
- power_port_name = self.power_port.resolve_name(kwargs.get('module'))
- power_port = PowerPort.objects.get(name=power_port_name, **kwargs)
- else:
- power_port = None
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.resolve_label(kwargs.get('module')),
- type=self.type,
- color=self.color,
- power_port=power_port,
- feed_leg=self.feed_leg,
- **kwargs
- )
- instantiate.do_not_call_in_templates = True
- def to_yaml(self):
- return {
- 'name': self.name,
- 'type': self.type,
- 'color': self.color,
- 'power_port': self.power_port.name if self.power_port else None,
- 'feed_leg': self.feed_leg,
- 'label': self.label,
- 'description': self.description,
- }
- class InterfaceTemplate(InterfaceValidationMixin, ModularComponentTemplateModel):
- """
- A template for a physical data interface on a new Device.
- """
- # Override ComponentTemplateModel._name to specify naturalize_interface function
- _name = NaturalOrderingField(
- target_field='name',
- naturalize_function=naturalize_interface,
- max_length=100,
- blank=True
- )
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=InterfaceTypeChoices
- )
- enabled = models.BooleanField(
- verbose_name=_('enabled'),
- default=True
- )
- mgmt_only = models.BooleanField(
- default=False,
- verbose_name=_('management only')
- )
- bridge = models.ForeignKey(
- to='self',
- on_delete=models.SET_NULL,
- related_name='bridge_interfaces',
- null=True,
- blank=True,
- verbose_name=_('bridge interface')
- )
- poe_mode = models.CharField(
- max_length=50,
- choices=InterfacePoEModeChoices,
- blank=True,
- null=True,
- verbose_name=_('PoE mode')
- )
- poe_type = models.CharField(
- max_length=50,
- choices=InterfacePoETypeChoices,
- blank=True,
- null=True,
- verbose_name=_('PoE type')
- )
- rf_role = models.CharField(
- max_length=30,
- choices=WirelessRoleChoices,
- blank=True,
- null=True,
- verbose_name=_('wireless role')
- )
- component_model = Interface
- class Meta(ModularComponentTemplateModel.Meta):
- verbose_name = _('interface template')
- verbose_name_plural = _('interface templates')
- def clean(self):
- super().clean()
- if self.bridge:
- if self.device_type and self.device_type != self.bridge.device_type:
- raise ValidationError({
- 'bridge': _(
- "Bridge interface ({bridge}) must belong to the same device type"
- ).format(bridge=self.bridge)
- })
- if self.module_type and self.module_type != self.bridge.module_type:
- raise ValidationError({
- 'bridge': _(
- "Bridge interface ({bridge}) must belong to the same module type"
- ).format(bridge=self.bridge)
- })
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.resolve_label(kwargs.get('module')),
- type=self.type,
- enabled=self.enabled,
- mgmt_only=self.mgmt_only,
- poe_mode=self.poe_mode,
- poe_type=self.poe_type,
- rf_role=self.rf_role,
- **kwargs
- )
- instantiate.do_not_call_in_templates = True
- def to_yaml(self):
- return {
- 'name': self.name,
- 'type': self.type,
- 'enabled': self.enabled,
- 'mgmt_only': self.mgmt_only,
- 'label': self.label,
- 'description': self.description,
- 'bridge': self.bridge.name if self.bridge else None,
- 'poe_mode': self.poe_mode,
- 'poe_type': self.poe_type,
- 'rf_role': self.rf_role,
- }
- class PortTemplateMapping(PortMappingBase):
- """
- Maps a FrontPortTemplate & position to a RearPortTemplate & position.
- """
- device_type = models.ForeignKey(
- to='dcim.DeviceType',
- on_delete=models.CASCADE,
- related_name='port_mappings',
- blank=True,
- null=True,
- )
- module_type = models.ForeignKey(
- to='dcim.ModuleType',
- on_delete=models.CASCADE,
- related_name='port_mappings',
- blank=True,
- null=True,
- )
- front_port = models.ForeignKey(
- to='dcim.FrontPortTemplate',
- on_delete=models.CASCADE,
- related_name='mappings',
- )
- rear_port = models.ForeignKey(
- to='dcim.RearPortTemplate',
- on_delete=models.CASCADE,
- related_name='mappings',
- )
- def clean(self):
- super().clean()
- # Validate rear port assignment
- if self.front_port.device_type_id != self.rear_port.device_type_id:
- raise ValidationError({
- "rear_port": _("Rear port ({rear_port}) must belong to the same device type").format(
- rear_port=self.rear_port
- )
- })
- def save(self, *args, **kwargs):
- # Associate the mapping with the parent DeviceType/ModuleType
- self.device_type = self.front_port.device_type
- self.module_type = self.front_port.module_type
- super().save(*args, **kwargs)
- class FrontPortTemplate(ModularComponentTemplateModel):
- """
- Template for a pass-through port on the front of a new Device.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=PortTypeChoices
- )
- color = ColorField(
- verbose_name=_('color'),
- blank=True
- )
- positions = models.PositiveSmallIntegerField(
- verbose_name=_('positions'),
- default=1,
- validators=[
- MinValueValidator(PORT_POSITION_MIN),
- MaxValueValidator(PORT_POSITION_MAX)
- ],
- )
- component_model = FrontPort
- class Meta(ModularComponentTemplateModel.Meta):
- constraints = (
- models.UniqueConstraint(
- fields=('device_type', 'name'),
- name='%(app_label)s_%(class)s_unique_device_type_name'
- ),
- models.UniqueConstraint(
- fields=('module_type', 'name'),
- name='%(app_label)s_%(class)s_unique_module_type_name'
- ),
- )
- verbose_name = _('front port template')
- verbose_name_plural = _('front port templates')
- def clean(self):
- super().clean()
- # Check that positions is greater than or equal to the number of associated RearPortTemplates
- if not self._state.adding:
- mapping_count = self.mappings.count()
- if self.positions < mapping_count:
- raise ValidationError({
- "positions": _(
- "The number of positions cannot be less than the number of mapped rear port templates ({count})"
- ).format(count=mapping_count)
- })
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.resolve_label(kwargs.get('module')),
- type=self.type,
- color=self.color,
- positions=self.positions,
- **kwargs
- )
- instantiate.do_not_call_in_templates = True
- def to_yaml(self):
- return {
- 'name': self.name,
- 'type': self.type,
- 'color': self.color,
- 'positions': self.positions,
- 'label': self.label,
- 'description': self.description,
- }
- class RearPortTemplate(ModularComponentTemplateModel):
- """
- Template for a pass-through port on the rear of a new Device.
- """
- type = models.CharField(
- verbose_name=_('type'),
- max_length=50,
- choices=PortTypeChoices
- )
- color = ColorField(
- verbose_name=_('color'),
- blank=True
- )
- positions = models.PositiveSmallIntegerField(
- verbose_name=_('positions'),
- default=1,
- validators=[
- MinValueValidator(PORT_POSITION_MIN),
- MaxValueValidator(PORT_POSITION_MAX)
- ],
- )
- component_model = RearPort
- class Meta(ModularComponentTemplateModel.Meta):
- verbose_name = _('rear port template')
- verbose_name_plural = _('rear port templates')
- def clean(self):
- super().clean()
- # Check that positions is greater than or equal to the number of associated FrontPortTemplates
- if not self._state.adding:
- mapping_count = self.mappings.count()
- if self.positions < mapping_count:
- raise ValidationError({
- "positions": _(
- "The number of positions cannot be less than the number of mapped front port templates "
- "({count})"
- ).format(count=mapping_count)
- })
- def instantiate(self, **kwargs):
- return self.component_model(
- name=self.resolve_name(kwargs.get('module')),
- label=self.resolve_label(kwargs.get('module')),
- type=self.type,
- color=self.color,
- positions=self.positions,
- **kwargs
- )
- instantiate.do_not_call_in_templates = True
- def to_yaml(self):
- return {
- 'name': self.name,
- 'type': self.type,
- 'color': self.color,
- 'positions': self.positions,
- 'label': self.label,
- 'description': self.description,
- }
- class ModuleBayTemplate(ModularComponentTemplateModel):
- """
- A template for a ModuleBay to be created for a new parent Device.
- """
- position = models.CharField(
- verbose_name=_('position'),
- max_length=30,
- blank=True,
- help_text=_('Identifier to reference when renaming installed components')
- )
- component_model = ModuleBay
- class Meta(ModularComponentTemplateModel.Meta):
- verbose_name = _('module bay template')
- verbose_name_plural = _('module bay templates')
- def instantiate(self, **kwargs):
- module = kwargs.get('module')
- return self.component_model(
- name=self.resolve_name(module),
- label=self.resolve_label(module),
- position=self.resolve_position(self.position, module),
- **kwargs
- )
- instantiate.do_not_call_in_templates = True
- def to_yaml(self):
- return {
- 'name': self.name,
- 'label': self.label,
- 'position': self.position,
- 'description': self.description,
- }
- class DeviceBayTemplate(ComponentTemplateModel):
- """
- A template for a DeviceBay to be created for a new parent Device.
- """
- component_model = DeviceBay
- class Meta(ComponentTemplateModel.Meta):
- verbose_name = _('device bay template')
- verbose_name_plural = _('device bay templates')
- def instantiate(self, device):
- return self.component_model(
- device=device,
- name=self.name,
- label=self.label
- )
- instantiate.do_not_call_in_templates = True
- def clean(self):
- if self.device_type and self.device_type.subdevice_role != SubdeviceRoleChoices.ROLE_PARENT:
- raise ValidationError(
- _(
- 'Subdevice role of device type ({device_type}) must be set to "parent" to allow device bays.'
- ).format(device_type=self.device_type)
- )
- def to_yaml(self):
- return {
- 'name': self.name,
- 'label': self.label,
- 'description': self.description,
- }
- class InventoryItemTemplate(MPTTModel, ComponentTemplateModel):
- """
- A template for an InventoryItem to be created for a new parent Device.
- """
- parent = TreeForeignKey(
- to='self',
- on_delete=models.CASCADE,
- related_name='child_items',
- blank=True,
- null=True,
- db_index=True
- )
- component_type = models.ForeignKey(
- to='contenttypes.ContentType',
- on_delete=models.PROTECT,
- related_name='+',
- blank=True,
- null=True
- )
- component_id = models.PositiveBigIntegerField(
- blank=True,
- null=True
- )
- component = GenericForeignKey(
- ct_field='component_type',
- fk_field='component_id'
- )
- role = models.ForeignKey(
- to='dcim.InventoryItemRole',
- on_delete=models.PROTECT,
- related_name='inventory_item_templates',
- blank=True,
- null=True
- )
- manufacturer = models.ForeignKey(
- to='dcim.Manufacturer',
- on_delete=models.PROTECT,
- related_name='inventory_item_templates',
- blank=True,
- null=True
- )
- part_id = models.CharField(
- max_length=50,
- verbose_name=_('part ID'),
- blank=True,
- help_text=_('Manufacturer-assigned part identifier')
- )
- objects = TreeManager()
- component_model = InventoryItem
- class Meta:
- ordering = ('device_type__id', 'parent__id', 'name')
- indexes = (
- models.Index(fields=('component_type', 'component_id')),
- )
- constraints = (
- models.UniqueConstraint(
- fields=('device_type', 'parent', 'name'),
- name='%(app_label)s_%(class)s_unique_device_type_parent_name'
- ),
- )
- verbose_name = _('inventory item template')
- verbose_name_plural = _('inventory item templates')
- def instantiate(self, **kwargs):
- parent = InventoryItem.objects.get(name=self.parent.name, **kwargs) if self.parent else None
- if self.component:
- model = self.component.component_model
- component = model.objects.get(name=self.component.name, **kwargs)
- else:
- component = None
- return self.component_model(
- parent=parent,
- name=self.name,
- label=self.label,
- component=component,
- role=self.role,
- manufacturer=self.manufacturer,
- part_id=self.part_id,
- **kwargs
- )
- instantiate.do_not_call_in_templates = True
|