mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2026-02-13 10:36:25 +01:00
Improve menu app
This commit is contained in:
@@ -1,174 +1,132 @@
|
||||
import logging
|
||||
from allianceauth.hooks import get_hooks
|
||||
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.template.loader import render_to_string
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
from allianceauth.menu.constants import DEFAULT_FOLDER_ICON_CLASSES
|
||||
|
||||
from .constants import DEFAULT_MENU_ITEM_ORDER, MenuItemType
|
||||
from .core.menu_item_hooks import MenuItemHookCustom
|
||||
from .managers import MenuItemManager
|
||||
|
||||
|
||||
class MenuItem(models.Model):
|
||||
# Auto Generated model from an auth_hook
|
||||
hook_function = models.CharField(
|
||||
max_length=500, default=None, null=True, blank=True)
|
||||
"""An item in the sidebar menu.
|
||||
|
||||
Some of these objects are generated from `MenuItemHook` objects.
|
||||
To avoid confusion we are using the same same field names.user defined
|
||||
"""
|
||||
|
||||
# User Made Model
|
||||
icon_classes = models.CharField(
|
||||
max_length=150, default=None, null=True, blank=True, help_text="Font Awesome classes to show as icon on menu")
|
||||
text = models.CharField(
|
||||
max_length=150, default=None, null=True, blank=True, help_text="Text to show on menu")
|
||||
url = models.CharField(max_length=2048, default=None,
|
||||
null=True, blank=True)
|
||||
|
||||
# Put it under a header?
|
||||
max_length=150,
|
||||
db_index=True,
|
||||
verbose_name=_("text"),
|
||||
help_text=_("Text to show on menu"),
|
||||
)
|
||||
order = models.IntegerField(
|
||||
default=DEFAULT_MENU_ITEM_ORDER,
|
||||
db_index=True,
|
||||
verbose_name=_("order"),
|
||||
help_text=_("Order of the menu. Lowest First"),
|
||||
)
|
||||
parent = models.ForeignKey(
|
||||
'self', on_delete=models.SET_NULL, null=True, blank=True, help_text="Parent Header. (Optional)")
|
||||
"self",
|
||||
on_delete=models.SET_NULL,
|
||||
default=None,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="children",
|
||||
verbose_name=_("folder"),
|
||||
help_text=_("Folder this item is in (optional)"),
|
||||
)
|
||||
is_hidden = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name=_("is hidden"),
|
||||
help_text=_(
|
||||
"Hide this menu item."
|
||||
"If this item is a folder all items under it will be hidden too"
|
||||
),
|
||||
)
|
||||
|
||||
# Put it where? lowest first
|
||||
rank = models.IntegerField(default=1000, help_text="Order of the menu. Lowest First.")
|
||||
# app related properties
|
||||
hook_hash = models.CharField(
|
||||
max_length=64, default=None, null=True, unique=True, editable=False
|
||||
) # hash of a menu item hook. Must be nullable for unique comparison.
|
||||
|
||||
# Hide it fully? Hiding a parent will hide all it's children
|
||||
hide = models.BooleanField(default=False, help_text="Hide this menu item. If this item is a header all items under it will be hidden too.")
|
||||
# user defined properties
|
||||
classes = models.CharField(
|
||||
max_length=150,
|
||||
default="",
|
||||
blank=True,
|
||||
verbose_name=_("icon classes"),
|
||||
help_text=_(
|
||||
"Font Awesome classes to show as icon on menu, "
|
||||
"e.g. <code>fa-solid fa-house</code>"
|
||||
),
|
||||
)
|
||||
url = models.TextField(
|
||||
default="",
|
||||
verbose_name=_("url"),
|
||||
help_text=_("External URL this menu items will link to"),
|
||||
)
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=['rank', ]),
|
||||
]
|
||||
objects = MenuItemManager()
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.text
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.hook_hash:
|
||||
self.hook_hash = None # empty strings can create problems
|
||||
return super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def classes(self): # Helper function to make this model closer to the hook functions
|
||||
return self.icon_classes
|
||||
def item_type(self) -> MenuItemType:
|
||||
"""Return the type of this menu item."""
|
||||
if self.hook_hash:
|
||||
return MenuItemType.APP
|
||||
|
||||
@staticmethod
|
||||
def hook_to_name(mh):
|
||||
return f"{mh.__class__.__module__}.{mh.__class__.__name__}"
|
||||
if not self.url:
|
||||
return MenuItemType.FOLDER
|
||||
|
||||
@staticmethod
|
||||
def sync_hook_models():
|
||||
# TODO define aa way for hooks to predefine a "parent" to create a sub menu from modules
|
||||
menu_hooks = get_hooks('menu_item_hook')
|
||||
hook_functions = []
|
||||
for hook in menu_hooks:
|
||||
mh = hook()
|
||||
cls = MenuItem.hook_to_name(mh)
|
||||
try:
|
||||
# if it exists update the text only
|
||||
# Users can adjust ranks so lets not change it if they have.
|
||||
mi = MenuItem.objects.get(hook_function=cls)
|
||||
mi.text = getattr(mh, "text", mh.__class__.__name__)
|
||||
mi.save()
|
||||
except MenuItem.DoesNotExist:
|
||||
# This is a new hook, Make the database model.
|
||||
MenuItem.objects.create(
|
||||
hook_function=cls,
|
||||
rank=getattr(mh, "order", 500),
|
||||
text=getattr(mh, "text", mh.__class__.__name__)
|
||||
)
|
||||
hook_functions.append(cls)
|
||||
return MenuItemType.LINK
|
||||
|
||||
# Get rid of any legacy hooks from modules removed
|
||||
MenuItem.objects.filter(hook_function__isnull=False).exclude(
|
||||
hook_function__in=hook_functions).delete()
|
||||
@property
|
||||
def is_app_item(self) -> bool:
|
||||
"""Return True if this is an app item, else False."""
|
||||
return self.item_type is MenuItemType.APP
|
||||
|
||||
@classmethod
|
||||
def filter_items(cls, menu_item: dict):
|
||||
"""
|
||||
filter any items with no valid children from a menu
|
||||
"""
|
||||
count_items = len(menu_item['items'])
|
||||
if count_items: # if we have children confirm we can see them
|
||||
for i in menu_item['items']:
|
||||
if len(i['render']) == 0:
|
||||
count_items -= 1
|
||||
if count_items == 0: # no children left dont render header
|
||||
return False
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
@property
|
||||
def is_child(self) -> bool:
|
||||
"""Return True if this item is a child, else False."""
|
||||
return bool(self.parent_id)
|
||||
|
||||
@classmethod
|
||||
def render_menu(cls, request):
|
||||
"""
|
||||
Return the sorted side menu items with any items the user can't see removed.
|
||||
"""
|
||||
# Override all the items to the bs5 theme
|
||||
template = "menu/menu-item-bs5.html"
|
||||
# TODO discuss permissions for user defined links
|
||||
@property
|
||||
def is_folder(self) -> bool:
|
||||
"""Return True if this item is a folder, else False."""
|
||||
return self.item_type is MenuItemType.FOLDER
|
||||
|
||||
# Turn all the hooks into functions
|
||||
menu_hooks = get_hooks('menu_item_hook')
|
||||
items = {}
|
||||
for fn in menu_hooks:
|
||||
f = fn()
|
||||
items[cls.hook_to_name(f)] = f
|
||||
@property
|
||||
def is_link_item(self) -> bool:
|
||||
"""Return True if this item is a link item, else False."""
|
||||
return self.item_type is MenuItemType.LINK
|
||||
|
||||
menu_items = MenuItem.objects.all().order_by("rank")
|
||||
@property
|
||||
def is_user_defined(self) -> bool:
|
||||
"""Return True if this item is user defined."""
|
||||
return self.item_type is not MenuItemType.APP
|
||||
|
||||
menu = {}
|
||||
for mi in menu_items:
|
||||
if mi.hide:
|
||||
# hidden item, skip it completely
|
||||
continue
|
||||
try:
|
||||
_cnt = 0
|
||||
_render = None
|
||||
if mi.hook_function:
|
||||
# This is a module hook, so we need to render it as the developer intended
|
||||
# TODO add a new attribute for apps that want to override it in the new theme
|
||||
items[mi.hook_function].template = template
|
||||
_render = items[mi.hook_function].render(request)
|
||||
_cnt = items[mi.hook_function].count
|
||||
else:
|
||||
# This is a user defined menu item so we render it with defaults.
|
||||
_render = render_to_string(template,
|
||||
{'item': mi},
|
||||
request=request)
|
||||
def to_hook_obj(self) -> MenuItemHookCustom:
|
||||
"""Convert to hook object for rendering."""
|
||||
if self.is_app_item:
|
||||
raise ValueError("The related hook objects should be used for app items.")
|
||||
|
||||
parent = mi.id
|
||||
if mi.parent_id: # Set it if present
|
||||
parent = mi.parent_id
|
||||
hook_obj = MenuItemHookCustom(
|
||||
text=self.text, classes=self.classes, url_name="", order=self.order
|
||||
)
|
||||
hook_obj.navactive = []
|
||||
if self.is_folder and not self.classes:
|
||||
hook_obj.classes = DEFAULT_FOLDER_ICON_CLASSES
|
||||
|
||||
if parent not in menu: # this will cause the menu headers to be out of order
|
||||
menu[parent] = {"items": [],
|
||||
"count": 0,
|
||||
"render": None,
|
||||
"text": "None",
|
||||
"rank": 9999,
|
||||
}
|
||||
_mi = {
|
||||
"count": _cnt,
|
||||
"render": _render,
|
||||
"text": mi.text,
|
||||
"rank": mi.rank,
|
||||
"classes": (mi.icon_classes if mi.icon_classes != "" else "fa-solid fa-folder"),
|
||||
"hide": mi.hide
|
||||
}
|
||||
|
||||
if parent != mi.id:
|
||||
# this is a sub item
|
||||
menu[parent]["items"].append(_mi)
|
||||
if _cnt:
|
||||
#add its count to the header count
|
||||
menu[parent]["count"] += _cnt
|
||||
else:
|
||||
if len(menu[parent]["items"]):
|
||||
# this is a top folder dont update the count.
|
||||
del(_mi["count"])
|
||||
menu[parent].update(_mi)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
|
||||
# reset to list
|
||||
menu = list(menu.values())
|
||||
|
||||
# sort the menu list as the parents may be out of order.
|
||||
menu.sort(key=lambda i: i['rank'])
|
||||
|
||||
# ensure no empty groups
|
||||
menu = filter(cls.filter_items, menu)
|
||||
|
||||
return menu
|
||||
hook_obj.url = self.url
|
||||
hook_obj.is_folder = self.is_folder
|
||||
hook_obj.html_id = f"id-folder-{self.id}" if self.is_folder else ""
|
||||
return hook_obj
|
||||
|
||||
Reference in New Issue
Block a user