Refactored comparison.py

- Switched to using attrs instead of dataclasses
This commit is contained in:
Victor Golovanenko 2022-01-21 11:42:37 +03:00
parent eb01474779
commit 87d12f05f8
Signed by: drygdryg
GPG Key ID: 50E0F568281DB835
3 changed files with 67 additions and 193 deletions

View File

@ -1,228 +1,102 @@
from dataclasses import dataclass, fields import attr
from attrs import fields
from django.conf import settings from django.conf import settings
config = settings.PLUGINS_CONFIG["netbox_interface_sync"] config = settings.PLUGINS_CONFIG["netbox_interface_sync"]
COMPARE_DESCRIPTIONS: bool = config["compare_description"]
@dataclass(frozen=True) @attr.s(frozen=True, auto_attribs=True)
class ParentComparison: class BaseComparison:
"""Common fields of a device component""" """Common fields of a device component"""
id: int # Do not compare IDs
name: str id: int = attr.ib(eq=False, hash=False, metadata={'printable': False})
label: str # Compare names case-insensitively and spaces-insensitively
description: str name: str = attr.ib(eq=lambda name: name.lower().replace(" ", ""), metadata={'printable': False})
label: str = attr.ib(hash=False)
# Compare descriptions if it is set by the configuration
description: str = attr.ib(eq=COMPARE_DESCRIPTIONS, hash=False)
# Do not compare `is_template` properties
is_template: bool = attr.ib(kw_only=True, default=False, eq=False, hash=False, metadata={'printable': False})
_non_printable_fields = ('id', 'name', 'is_template') @property
def fields_display(self) -> str:
def __eq__(self, other): """Generate human-readable list of printable fields to display in the comparison table"""
# Ignore some fields when comparing; ignore component name case and whitespaces
eq = (
self.name.lower().replace(" ", "") == other.name.lower().replace(" ", "")
) and (self.label == other.label)
if config["compare_description"]:
eq = eq and (self.description == other.description)
return eq
def __hash__(self):
# Ignore some fields when hashing; ignore component name case and whitespaces
return hash(self.name.lower().replace(" ", ""))
@staticmethod
def __field_name_caption__(field_name: str):
field_captions = {
'type_display': 'Type',
'rear_port_position': 'Position'
}
return field_captions.get(field_name) or field_name.replace('_', ' ').capitalize()
def __str__(self):
fields_to_display = [] fields_to_display = []
for field in fields(self): for field in fields(self.__class__):
if field.name in self._non_printable_fields: if not field.metadata.get('printable', True):
continue continue
field_value = getattr(self, field.name) field_value = getattr(self, field.name)
if not field_value: if not field_value:
continue continue
field_name_display = self.__field_name_caption__(field.name) field_caption = field.metadata.get('displayed_caption') or field.name.replace('_', ' ').capitalize()
fields_to_display.append(f'{field_name_display}: {field_value}') fields_to_display.append(f'{field_caption}: {field_value}')
return '\n'.join(fields_to_display) return '\n'.join(fields_to_display)
@dataclass(frozen=True) @attr.s(frozen=True, auto_attribs=True)
class ParentTypedComparison(ParentComparison): class BaseTypedComparison(BaseComparison):
"""Common fields of a device typed component""" """Common fields of a device typed component"""
type: str type: str = attr.ib(hash=False, metadata={'printable': False})
type_display: str type_display: str = attr.ib(eq=False, hash=False, metadata={'displayed_caption': 'Type'})
_non_printable_fields = ParentComparison._non_printable_fields + ('type',)
def __eq__(self, other):
eq = (
(self.name.lower().replace(" ", "") == other.name.lower().replace(" ", ""))
and (self.label == other.label)
and (self.type == other.type)
)
if config["compare_description"]:
eq = eq and (self.description == other.description)
return eq
def __hash__(self):
return hash((self.name.lower().replace(" ", ""), self.type))
@dataclass(frozen=True) @attr.s(frozen=True, auto_attribs=True)
class InterfaceComparison(ParentTypedComparison): class DeviceBayComparison(BaseComparison):
"""A unified way to represent the device bay and device bay template"""
pass
@attr.s(frozen=True, auto_attribs=True)
class InterfaceComparison(BaseTypedComparison):
"""A unified way to represent the interface and interface template""" """A unified way to represent the interface and interface template"""
mgmt_only: bool mgmt_only: bool = attr.ib(hash=False)
is_template: bool = False
def __eq__(self, other):
eq = (
(self.name.lower().replace(" ", "") == other.name.lower().replace(" ", ""))
and (self.label == other.label)
and (self.type == other.type)
and (self.mgmt_only == other.mgmt_only)
)
if config["compare_description"]:
eq = eq and (self.description == other.description)
return eq
def __hash__(self):
return hash((self.name.lower().replace(" ", ""), self.type))
@dataclass(frozen=True) @attr.s(frozen=True, auto_attribs=True)
class FrontPortComparison(ParentTypedComparison): class FrontPortComparison(BaseTypedComparison):
"""A unified way to represent the front port and front port template""" """A unified way to represent the front port and front port template"""
color: str color: str = attr.ib(hash=False)
# rear_port_id: int # rear_port_id: int
rear_port_position: int rear_port_position: int = attr.ib(hash=False, metadata={'displayed_caption': 'Position'})
is_template: bool = False
def __eq__(self, other):
eq = (
(self.name.lower().replace(" ", "") == other.name.lower().replace(" ", ""))
and (self.label == other.label)
and (self.type == other.type)
and (self.color == other.color)
and (self.rear_port_position == other.rear_port_position)
)
if config["compare_description"]:
eq = eq and (self.description == other.description)
return eq
def __hash__(self):
return hash((self.name.lower().replace(" ", ""), self.type))
@dataclass(frozen=True) @attr.s(frozen=True, auto_attribs=True)
class RearPortComparison(ParentTypedComparison): class RearPortComparison(BaseTypedComparison):
"""A unified way to represent the rear port and rear port template""" """A unified way to represent the rear port and rear port template"""
color: str color: str = attr.ib(hash=False)
positions: int positions: int = attr.ib(hash=False)
is_template: bool = False
def __eq__(self, other):
eq = (
(self.name.lower().replace(" ", "") == other.name.lower().replace(" ", ""))
and (self.label == other.label)
and (self.type == other.type)
and (self.color == other.color)
and (self.positions == other.positions)
)
if config["compare_description"]:
eq = eq and (self.description == other.description)
return eq
def __hash__(self):
return hash((self.name.lower().replace(" ", ""), self.type))
@dataclass(frozen=True, eq=False) @attr.s(frozen=True, auto_attribs=True)
class ConsolePortComparison(ParentTypedComparison): class ConsolePortComparison(BaseTypedComparison):
"""A unified way to represent the consoleport and consoleport template""" """A unified way to represent the consoleport and consoleport template"""
pass
is_template: bool = False
@dataclass(frozen=True, eq=False) @attr.s(frozen=True, auto_attribs=True)
class ConsoleServerPortComparison(ParentTypedComparison): class ConsoleServerPortComparison(BaseTypedComparison):
"""A unified way to represent the consoleserverport and consoleserverport template""" """A unified way to represent the consoleserverport and consoleserverport template"""
pass
is_template: bool = False
@dataclass(frozen=True) @attr.s(frozen=True, auto_attribs=True)
class PowerPortComparison(ParentTypedComparison): class PowerPortComparison(BaseTypedComparison):
"""A unified way to represent the power port and power port template""" """A unified way to represent the power port and power port template"""
maximum_draw: str maximum_draw: str = attr.ib(hash=False)
allocated_draw: str allocated_draw: str = attr.ib(hash=False)
is_template: bool = False
def __eq__(self, other):
eq = (
(self.name.lower().replace(" ", "") == other.name.lower().replace(" ", ""))
and (self.label == other.label)
and (self.type == other.type)
and (self.maximum_draw == other.maximum_draw)
and (self.allocated_draw == other.allocated_draw)
)
if config["compare_description"]:
eq = eq and (self.description == other.description)
return eq
def __hash__(self):
return hash((self.name.lower().replace(" ", ""), self.type))
@dataclass(frozen=True) @attr.s(frozen=True, auto_attribs=True)
class PowerOutletComparison(ParentTypedComparison): class PowerOutletComparison(BaseTypedComparison):
"""A unified way to represent the power outlet and power outlet template""" """A unified way to represent the power outlet and power outlet template"""
power_port_name: str = "" power_port_name: str = attr.ib(hash=False)
feed_leg: str = "" feed_leg: str = attr.ib(hash=False)
is_template: bool = False
def __eq__(self, other):
eq = (
(self.name.lower().replace(" ", "") == other.name.lower().replace(" ", ""))
and (self.label == other.label)
and (self.type == other.type)
and (self.power_port_name == other.power_port_name)
and (self.feed_leg == other.feed_leg)
)
if config["compare_description"]:
eq = eq and (self.description == other.description)
return eq
def __hash__(self):
return hash(
(self.name.lower().replace(" ", ""), self.type, self.power_port_name)
)
@dataclass(frozen=True, eq=False)
class DeviceBayComparison(ParentComparison):
"""A unified way to represent the device bay and device bay template"""
is_template: bool = False

View File

@ -26,16 +26,16 @@
</style> </style>
<script> <script>
function toggle(event) { function toggle(event) {
event = event || window.event; event = event || window.event;
var src = event.target || event.srcElement || event; const src = event.target || event.srcElement || event;
checkboxes = document.getElementsByName(src.id); const checkboxes = document.getElementsByName(src.id);
for(var checkbox of checkboxes) checkbox.checked = src.checked; for(const checkbox of checkboxes) checkbox.checked = src.checked;
} }
function uncheck(event) { function uncheck(event) {
event = event || window.event; event = event || window.event;
var src = event.target || event.srcElement || event; const src = event.target || event.srcElement || event;
if (src.checked == false) { if (src.checked === false) {
document.getElementById(src.name).checked = false; document.getElementById(src.name).checked = false;
} }
} }
@ -99,7 +99,7 @@ function uncheck(event) {
{{ component_template.name }} {{ component_template.name }}
{% endif %} {% endif %}
</th> </th>
<td style="white-space:pre" {% if not component %}class="table-danger"{% endif %}>{{ component_template }}</td> <td style="white-space:pre" {% if not component %}class="table-danger"{% endif %}>{{ component_template.fields_display }}</td>
<td {% if not component %}class="table-danger"{% endif %}> <td {% if not component %}class="table-danger"{% endif %}>
{% if not component %} {% if not component %}
<label> <label>
@ -122,7 +122,7 @@ function uncheck(event) {
{{ component.name }} {{ component.name }}
{% endif %} {% endif %}
</th> </th>
<td style="white-space:pre" {% if not component_template %}class="table-success"{% endif %}>{{ component }}</td> <td style="white-space:pre" {% if not component_template %}class="table-success"{% endif %}>{{ component.fields_display }}</td>
<td {% if not component_template %}class="table-success"{% endif %}> <td {% if not component_template %}class="table-success"{% endif %}>
{% if not component_template %} {% if not component_template %}
<label> <label>

View File

@ -12,7 +12,7 @@ setup(
author='Victor Golovanenko', author='Victor Golovanenko',
author_email='drygdryg2014@yandex.com', author_email='drygdryg2014@yandex.com',
license='GPL-3.0', license='GPL-3.0',
install_requires=[], install_requires=['attrs>=21.1.0'],
packages=["netbox_interface_sync"], packages=["netbox_interface_sync"],
package_data={"netbox_interface_sync": ["templates/netbox_interface_sync/*.html"]}, package_data={"netbox_interface_sync": ["templates/netbox_interface_sync/*.html"]},
zip_safe=False zip_safe=False