* [up] add config for custom images, better js * [up] use bundler to bundle js * [fix] remove domcontentloaded since defer is used * [fix] formatting with black * [fix] images drf api * [up] add support for custom icons on additional roles: power panel, power feed, circuit * [up] optimize topology generation by using `_id` when trying to access pk of related object * [fix] kebab case instead of slugification * [fix] use empty queryset in api views & change coord save logic
94 lines
2.7 KiB
Python
94 lines
2.7 KiB
Python
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
from dcim.models import DeviceRole
|
|
from django.conf import settings
|
|
from django.contrib.contenttypes.models import ContentType
|
|
from django.db import models
|
|
from django.templatetags.static import static
|
|
from netbox.models.features import (
|
|
ChangeLoggingMixin,
|
|
ExportTemplatesMixin,
|
|
WebhooksMixin,
|
|
)
|
|
|
|
from netbox_topology_views.utils import (
|
|
CONF_IMAGE_DIR,
|
|
IMAGE_DIR,
|
|
Role,
|
|
find_image_url,
|
|
get_model_role,
|
|
image_static_url,
|
|
)
|
|
|
|
|
|
class RoleImage(ChangeLoggingMixin, ExportTemplatesMixin, WebhooksMixin):
|
|
class Meta:
|
|
indexes = [
|
|
models.Index(fields=["content_type", "object_id"]),
|
|
]
|
|
|
|
objects: "models.Manager[RoleImage]"
|
|
|
|
image = models.CharField("Path within the netbox static directory", max_length=255)
|
|
|
|
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE)
|
|
object_id = models.PositiveIntegerField(null=True, blank=True)
|
|
|
|
__role: Optional[Role] = None
|
|
|
|
@property
|
|
def role(self) -> Role:
|
|
if self.__role:
|
|
return self.__role
|
|
|
|
model_class = self.content_type.model_class()
|
|
|
|
if not model_class:
|
|
raise ValueError(f"Invalid content type: {self.content_type}")
|
|
|
|
if model_class == DeviceRole:
|
|
device_role: DeviceRole = DeviceRole.objects.get(pk=self.object_id)
|
|
self.__role = Role(slug=device_role.slug, name=device_role.name)
|
|
return self.__role
|
|
|
|
self.__role = get_model_role(model_class)
|
|
return self.__role
|
|
|
|
def __str__(self):
|
|
return f"{self.role} - {self.image}"
|
|
|
|
def get_image(self) -> Path:
|
|
"""Get Icon
|
|
|
|
returns the model's image's absolute path in the filesystem
|
|
raises ValueError if the file cannot be found
|
|
"""
|
|
path = Path(settings.STATIC_ROOT) / self.image
|
|
|
|
if not path.exists():
|
|
raise ValueError(f"{self.role} path '{path}' does not exists")
|
|
|
|
return path
|
|
|
|
def get_default_image(self, dir: Path = CONF_IMAGE_DIR):
|
|
"""Get default image
|
|
|
|
will attempt to find image in given directory with any file extension,
|
|
otherwise will try to find a `role-unknown` image
|
|
|
|
fallback is `STATIC_ROOT/netbox_topology_views/img/role-unknown.png`
|
|
"""
|
|
if url := find_image_url(self.role.slug, dir):
|
|
return url
|
|
|
|
# fallback to default role unknown image
|
|
return image_static_url(IMAGE_DIR / "role-unknown.png")
|
|
|
|
def get_image_url(self, dir: Path = CONF_IMAGE_DIR) -> str:
|
|
try:
|
|
self.get_image()
|
|
except ValueError:
|
|
return self.get_default_image(dir)
|
|
return static(f"/{self.image}")
|