Feature: Add customisable icons in topology (#193)
* [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
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
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}")
|
||||
Reference in New Issue
Block a user