~homeworkprod/byceps

d260c9e27f7b9e02ef4d8a45ecde7ee4522e10fa — Jochen Kupperschmidt a month ago 38756fa
Add admin UI to view site navigation menus, items

No support for nested items, yet, though.
M byceps/blueprints/admin/core/templates/layout/admin/_header_nav_site.html => byceps/blueprints/admin/core/templates/layout/admin/_header_nav_site.html +6 -0
@@ 27,6 27,12 @@
      required_permission='snippet.view',
      icon='site')
    .add_item(
      url_for('site_navigation_admin.index_for_site', site_id=site.id),
      _('Navigation'),
      id='site_navigation_admin',
      required_permission='site.view',
      icon='site')
    .add_item(
      url_for('shop_storefront_admin.view', storefront_id=site.storefront_id) if site.storefront_id else '',
      _('Storefront'),
      id='shop_storefront_admin',

A byceps/blueprints/admin/site/navigation/__init__.py => byceps/blueprints/admin/site/navigation/__init__.py +0 -0
A byceps/blueprints/admin/site/navigation/templates/admin/site/navigation/index_for_site.html => byceps/blueprints/admin/site/navigation/templates/admin/site/navigation/index_for_site.html +40 -0
@@ 0,0 1,40 @@
{% extends 'layout/admin/site/navigation.html' %}
{% from 'macros/misc.html' import render_tag %}
{% set page_title = _('Menus') %}

{% block body %}

  <div class="row row--space-between">
    <h1>{{ page_title }}</h1>
  </div>

  <div class="box">
    {%- if menus %}
    <table class="index wide">
      <thead>
        <tr>
          <th>{{ _('Name') }}</th>
          <th>{{ _('Language') }}</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        {%- for menu in menus|sort(attribute='name,language_code') %}
        <tr>
          <td><a href="{{ url_for('.view', menu_id=menu.id) }}"><strong>{{ menu.name }}</strong></a></td>
          <td>{{ menu.language_code }}</td>
          <td>
            {%- if menu.hidden -%}
            {{ render_tag(_('hidden'), class='color-disabled', icon='hidden') }}
            {%- endif -%}
          </td>
        </tr>
        {%- endfor %}
      </tbody>
    </table>
    {%- else %}
    <div class="dimmed-box centered">{{ _('No menus exist.') }}</div>
    {%- endif %}
  </div>

{%- endblock %}

A byceps/blueprints/admin/site/navigation/templates/admin/site/navigation/view.html => byceps/blueprints/admin/site/navigation/templates/admin/site/navigation/view.html +62 -0
@@ 0,0 1,62 @@
{% extends 'layout/admin/site/navigation.html' %}
{% from 'macros/misc.html' import render_tag %}
{% set page_title = [_('Menu'), menu.name] %}

{% block body %}

  <div class="row row--space-between">
    <div>
      <h1>
        {{- menu.name }} ({{ menu.language_code }})
        {%- if menu.hidden %}
        {{ render_tag(_('hidden'), class='color-disabled', icon='hidden') }}
        {%- endif -%}
      </h1>
    </div>
  </div>

  <div class="box">

    <div class="data-label">{{ _('Language') }}</div>
    <div class="data-value">{{ menu.language_code }}</div>

  </div>

  <h2>{{ _('Items') }}</h2>

  <div class="box">

    {%- if menu.items %}
    <table class="index wide">
      <thead>
        <tr>
          <th>{{ _('Target type') }}</th>
          <th>{{ _('Target') }}</th>
          <th>{{ _('Label') }}</th>
          <th>{{ _('Current page ID') }}</th>
          <th></th>
        </tr>
      </thead>
      <tbody>
        {%- for item in menu.items|sort(attribute='position') %}
        <tr>
          <td>{{ item.target_type.name }}</td>
          <td>{{ item.target }}</td>
          <td>{{ item.label }}</td>
          <td>{{ item.current_page_id }}</td>
          <td>
            {%- if item.hidden -%}
            {{ render_tag(_('hidden'), class='color-disabled', icon='hidden') }}
            {%- endif -%}
          </td>
        </tr>
        {%- endfor %}
      </tbody>
    </table>
    {%- else %}
    <div class="dimmed-box centered">{{ _('No menus exist.') }}</div>
    {%- endif %}

  </div>

{%- endblock %}

A byceps/blueprints/admin/site/navigation/templates/layout/admin/site/navigation.html => byceps/blueprints/admin/site/navigation/templates/layout/admin/site/navigation.html +4 -0
@@ 0,0 1,4 @@
{% extends 'layout/admin/base.html' %}
{% set current_page = 'site_navigation_admin' %}
{% set current_page_brand = brand %}
{% set current_page_site = site %}

A byceps/blueprints/admin/site/navigation/views.py => byceps/blueprints/admin/site/navigation/views.py +75 -0
@@ 0,0 1,75 @@
"""
byceps.blueprints.admin.site.navigation.views
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

:Copyright: 2014-2022 Jochen Kupperschmidt
:License: Revised BSD (see `LICENSE` file for details)
"""

from flask import abort

from .....services.brand import service as brand_service
from .....services.site import service as site_service
from .....services.site.transfer.models import Site, SiteID
from .....services.site_navigation import service as navigation_service
from .....services.site_navigation.transfer.models import MenuAggregate, MenuID
from .....util.framework.blueprint import create_blueprint
from .....util.framework.templating import templated
from .....util.views import permission_required


blueprint = create_blueprint('site_navigation_admin', __name__)


@blueprint.get('/for_site/<site_id>')
@permission_required('site.view')
@templated
def index_for_site(site_id):
    """List menus for that site."""
    site = _get_site_or_404(site_id)

    brand = brand_service.get_brand(site.brand_id)

    menus = navigation_service.get_menus(site.id)

    return {
        'site': site,
        'brand': brand,
        'menus': menus,
    }


@blueprint.get('/<menu_id>')
@permission_required('site.view')
@templated
def view(menu_id):
    """Show a single menu."""
    menu = _get_menu_aggregate_or_404(menu_id)

    site = site_service.get_site(menu.site_id)

    brand = brand_service.get_brand(site.brand_id)

    return {
        'menu': menu,
        'site': site,
        'brand': brand,
    }


def _get_site_or_404(site_id: SiteID) -> Site:
    site = site_service.find_site(site_id)

    if site is None:
        abort(404)

    return site


def _get_menu_aggregate_or_404(menu_id: MenuID) -> MenuAggregate:
    menu = navigation_service.find_menu_aggregate(menu_id)

    if menu is None:
        abort(404)

    return menu

M byceps/blueprints/blueprints.py => byceps/blueprints/blueprints.py +1 -0
@@ 121,6 121,7 @@ def _get_blueprints_admin(app: Flask) -> Iterator[BlueprintReg]:
        (app, 'admin.shop.shop',                 '/admin/shop/shop'         ),
        (app, 'admin.shop.storefront',           '/admin/shop/storefronts'  ),
        (app, 'admin.site',                      '/admin/sites'             ),
        (app, 'admin.site.navigation',           '/admin/sites/navigation'  ),
        (app, 'admin.snippet',                   '/admin/snippets'          ),
        (app, 'admin.snippet.mountpoint',        '/admin/snippets/mountpoints'  ),
        (app, 'admin.ticketing',                 '/admin/ticketing'         ),

M byceps/services/site_navigation/service.py => byceps/services/site_navigation/service.py +84 -17
@@ 7,7 7,7 @@ byceps.services.site_navigation.service
"""

from __future__ import annotations
from typing import Optional
from typing import Iterable, Optional

from sqlalchemy import select



@@ 15,7 15,14 @@ from ...database import db
from ...services.site.transfer.models import SiteID

from .dbmodels import Item as DbItem, Menu as DbMenu
from .transfer.models import Item, ItemID, ItemTargetType, Menu, MenuID
from .transfer.models import (
    Item,
    ItemID,
    ItemTargetType,
    Menu,
    MenuAggregate,
    MenuID,
)


def create_menu(


@@ 61,24 68,24 @@ def create_item(
    return _db_entity_to_item(db_item)


def get_items_for_menu(
    site_id: SiteID, name: str, language_code: str
) -> list[Item]:
    """Return the items of a menu.
def find_menu(menu_id: MenuID) -> Optional[Menu]:
    """Return the menu, or `None` if not found."""
    db_menu = _find_db_menu(menu_id)

    An empty list is returned if the menu does not exist, is hidden, or
    contains no visible items.
    if db_menu is None:
        return None

    return _db_entity_to_menu(db_menu)


def get_menu(menu_id: MenuID) -> Menu:
    """Return the menu.

    Raise error if not found.
    """
    db_items = db.session.scalars(
        select(DbItem)
        .join(DbMenu)
        .filter(DbMenu.site_id == site_id)
        .filter(DbMenu.name == name)
        .filter(DbMenu.language_code == language_code)
        .filter(DbMenu.hidden == False)
    )
    db_menu = _get_db_menu(menu_id)

    return [_db_entity_to_item(db_item) for db_item in db_items]
    return _db_entity_to_menu(db_menu)


def _find_db_menu(menu_id: MenuID) -> Optional[DbMenu]:


@@ 99,6 106,49 @@ def _get_db_menu(menu_id: MenuID) -> DbMenu:
    return db_menu


def find_menu_aggregate(menu_id: MenuID) -> Optional[MenuAggregate]:
    """Return the menu aggregate, or `None` if not found."""
    db_menu = _find_db_menu(menu_id)
    if db_menu is None:
        return None

    db_items = db.session.scalars(
        select(DbItem)
        .filter(DbItem.menu_id == db_menu.id)
    )

    return _db_entity_to_menu_aggregate(db_menu, db_items)


def get_menus(site_id: SiteID) -> list[Menu]:
    """Return the menus for this site."""
    db_menus = db.session.scalars(
        select(DbMenu).filter(DbMenu.site_id == site_id)
    )

    return [_db_entity_to_menu(db_menu) for db_menu in db_menus]


def get_items_for_menu(
    site_id: SiteID, name: str, language_code: str
) -> list[Item]:
    """Return the items of a menu.

    An empty list is returned if the menu does not exist, is hidden, or
    contains no visible items.
    """
    db_items = db.session.scalars(
        select(DbItem)
        .join(DbMenu)
        .filter(DbMenu.site_id == site_id)
        .filter(DbMenu.name == name)
        .filter(DbMenu.language_code == language_code)
        .filter(DbMenu.hidden == False)
    )

    return [_db_entity_to_item(db_item) for db_item in db_items]


def _db_entity_to_menu(db_menu: DbMenu) -> Menu:
    return Menu(
        id=db_menu.id,


@@ 113,9 163,26 @@ def _db_entity_to_item(db_item: DbItem) -> Item:
    return Item(
        id=db_item.id,
        menu_id=db_item.menu_id,
        position=db_item.position,
        target_type=db_item.target_type,
        target=db_item.target,
        label=db_item.label,
        current_page_id=db_item.current_page_id,
        hidden=db_item.hidden,
    )


def _db_entity_to_menu_aggregate(
    db_menu: DbMenu, db_items: Iterable[DbItem]
) -> MenuAggregate:
    menu = _db_entity_to_menu(db_menu)
    items = [_db_entity_to_item(db_item) for db_item in db_items]

    return MenuAggregate(
        id=menu.id,
        site_id=menu.site_id,
        name=menu.name,
        language_code=menu.language_code,
        hidden=menu.hidden,
        items=items,
    )

M byceps/services/site_navigation/transfer/models.py => byceps/services/site_navigation/transfer/models.py +6 -0
@@ 37,6 37,7 @@ class Menu:
class Item:
    id: ItemID
    menu_id: MenuID
    position: int
    target_type: ItemTargetType
    target: str
    label: str


@@ 50,3 51,8 @@ class ItemForRendering:
    label: str
    current_page_id: str
    children: list[ItemForRendering]


@dataclass(frozen=True)
class MenuAggregate(Menu):
    items: list[Item]