Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor admin panel code #361

Merged
merged 7 commits into from
Apr 30, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
645 changes: 0 additions & 645 deletions src/meshapi/admin.py

This file was deleted.

9 changes: 9 additions & 0 deletions src/meshapi/admin/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from .admin import *
from .building import *
from .device import *
from .inlines import *
from .install import *
from .link import *
from .member import *
from .node import *
from .sector import *
50 changes: 50 additions & 0 deletions src/meshapi/admin/admin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from django.contrib import admin

admin.site.site_header = "MeshDB Admin"
admin.site.site_title = "MeshDB Admin Portal"
admin.site.index_title = "Welcome to MeshDB Admin Portal"

device_fieldsets = [
(
"Details",
{
"fields": [
"status",
"name",
"ssid",
"ip_address",
"node",
]
},
),
(
"Location",
{
"fields": [
"latitude",
"longitude",
"altitude",
]
},
),
(
"Dates",
{
"fields": [
"install_date",
"abandon_date",
]
},
),
(
"Misc",
{
"fields": [
"model",
"type",
"uisp_id",
"notes",
]
},
),
]
127 changes: 127 additions & 0 deletions src/meshapi/admin/building.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
from django.contrib import admin
from django.contrib.admin.options import forms
from django.utils.safestring import mark_safe

from meshapi.admin.inlines import InstallInline
from meshapi.models import Building
from meshapi.widgets import PanoramaViewer


class BoroughFilter(admin.SimpleListFilter):
title = "Borough"
parameter_name = "borough"

def lookups(self, request, model_admin):
return (
("bronx", ("The Bronx")),
("manhattan", ("Manhattan")),
("brooklyn", ("Brooklyn")),
("queens", ("Queens")),
("staten_island", ("Staten Island")),
)

def queryset(self, request, queryset):
if self.value() == "bronx":
return queryset.filter(city="Bronx")
elif self.value() == "manhattan":
return queryset.filter(city="New York")
elif self.value() == "brooklyn":
return queryset.filter(city="Brooklyn")
elif self.value() == "queens":
return queryset.filter(city="Queens")
elif self.value() == "staten_island":
return queryset.filter(city="Staten Island")
return queryset


class BuildingAdminForm(forms.ModelForm):
class Meta:
model = Building
fields = "__all__"
widgets = {
"panoramas": PanoramaViewer(schema={"type": "array", "items": {"type": "string"}}),
"bin": forms.NumberInput(attrs={"style": "width:21ch"}),
}


@admin.register(Building)
class BuildingAdmin(admin.ModelAdmin):
form = BuildingAdminForm
list_display = ["__str__", "street_address", "primary_node"]
search_fields = [
# Sometimes they have an actual name
"nodes__name__icontains",
# Address info
"street_address__icontains",
"city__icontains",
"state__icontains",
"zip_code__iexact",
"bin__iexact",
# Search by NN
"nodes__network_number__iexact",
"installs__install_number__iexact",
# Search by Member info
"installs__member__name__icontains",
"installs__member__primary_email_address__icontains",
"installs__member__phone_number__iexact",
"installs__member__slack_handle__iexact",
# Notes
"notes__icontains",
]
list_filter = [
BoroughFilter,
("primary_node", admin.EmptyFieldListFilter),
]
fieldsets = [
(
"Address Information",
{
"fields": [
"street_address",
"city",
"state",
"zip_code",
]
},
),
(
"NYC Information",
{
"fields": [
"bin",
"latitude",
"longitude",
"altitude",
]
},
),
(
"Misc",
{
"fields": [
"notes",
"panoramas",
]
},
),
(
"Nodes",
{
"fields": [
"primary_node",
"nodes",
]
},
),
]
inlines = [InstallInline]
filter_horizontal = ("nodes",)

# This is probably a bad idea because you'll have to load a million panos
# and OOM your computer
# Need to find a way to "thumbnail-ize" them on the server side, probably.
@mark_safe
def thumb(self, obj):
return f"<img src='{obj.get_thumb()}' width='50' height='50' />"

thumb.__name__ = "Thumbnail"
42 changes: 42 additions & 0 deletions src/meshapi/admin/device.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
from django.contrib import admin
from django.contrib.admin.options import forms

from meshapi.admin.admin import device_fieldsets
from meshapi.admin.inlines import DeviceLinkInline
from meshapi.models import Device
from meshapi.widgets import DeviceIPAddressWidget


class DeviceAdminForm(forms.ModelForm):
class Meta:
model = Device
fields = "__all__"
widgets = {
"ip_address": DeviceIPAddressWidget(),
}


@admin.register(Device)
class DeviceAdmin(admin.ModelAdmin):
form = DeviceAdminForm
search_fields = ["name__icontains", "model__icontains", "ssid__icontains", "notes__icontains"]
list_display = [
"__str__",
"ssid",
"name",
"model",
]
list_filter = [
"status",
"install_date",
"model",
]
fieldsets = device_fieldsets
inlines = [DeviceLinkInline]

def get_queryset(self, request):
# Get the base queryset
queryset = super().get_queryset(request)
# Filter out sectors
queryset = queryset.exclude(sector__isnull=False)
return queryset
117 changes: 117 additions & 0 deletions src/meshapi/admin/inlines.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
from django.contrib import admin
from django.db.models import Q
from nonrelated_inlines.admin import NonrelatedTabularInline

from meshapi.models import Building, Device, Install, Link, Sector


# Inline with the typical rules we want + Formatting
class BetterInline(admin.TabularInline):
extra = 0
can_delete = False
template = "admin/install_tabular.html"

def has_add_permission(self, request, obj) -> bool:
return False

class Media:
css = {
"all": ("admin/install_tabular.css",),
}


class BetterNonrelatedInline(NonrelatedTabularInline):
extra = 0
can_delete = False
template = "admin/install_tabular.html"

def has_add_permission(self, request, obj) -> bool:
return False

class Media:
css = {
"all": ("admin/install_tabular.css",),
}


class NonrelatedBuildingInline(BetterNonrelatedInline):
model = Building
fields = ["primary_node", "bin", "street_address", "city", "zip_code"]
readonly_fields = fields

add_button = True

# Hack to get the NN
network_number = None

def get_form_queryset(self, obj):
self.network_number = obj.pk
return self.model.objects.filter(nodes=obj)

def save_new_instance(self, parent, instance):
pass


class BuildingMembershipInline(admin.TabularInline):
model = Building.nodes.through
extra = 0
autocomplete_fields = ["building_id"]
classes = ["collapse"]
verbose_name = "Building"
verbose_name_plural = "Edit Related Buildings"


class DeviceInline(BetterInline):
model = Device
fields = ["status", "type", "model"]
readonly_fields = fields

def get_queryset(self, request):
# Get the base queryset
queryset = super().get_queryset(request)
# Filter out sectors
queryset = queryset.exclude(sector__isnull=False)
return queryset


class NodeLinkInline(BetterNonrelatedInline):
model = Link
fields = ["status", "type", "from_device", "to_device"]
readonly_fields = fields

def get_form_queryset(self, obj):
from_device_q = Q(from_device__in=obj.devices.all())
to_device_q = Q(to_device__in=obj.devices.all())
all_links = from_device_q | to_device_q
return self.model.objects.filter(all_links)

def save_new_instance(self, parent, instance):
pass


class DeviceLinkInline(BetterNonrelatedInline):
model = Link
fields = ["status", "type", "from_device", "to_device"]
readonly_fields = fields

def get_form_queryset(self, obj):
from_device_q = Q(from_device=obj)
to_device_q = Q(to_device=obj)
all_links = from_device_q | to_device_q
return self.model.objects.filter(all_links)

def save_new_instance(self, parent, instance):
pass


class SectorInline(BetterInline):
model = Sector
fields = ["status", "type", "model"]
readonly_fields = fields


# This controls the list of installs reverse FK'd to Buildings and Members
class InstallInline(BetterInline):
model = Install
fields = ["status", "node", "member", "building", "unit"]
readonly_fields = fields
Loading
Loading