byceps/blueprints/admin/site/navigation/views.py
"""
byceps.blueprints.admin.site.navigation.views
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
:Copyright: 2014-2024 Jochen Kupperschmidt
:License: Revised BSD (see `LICENSE` file for details)
"""
import dataclasses
from flask import abort, request
from flask_babel import gettext
from byceps.services.brand import brand_service
from byceps.services.page import page_service
from byceps.services.site import site_service
from byceps.services.site.models import Site, SiteID
from byceps.services.site_navigation import site_navigation_service
from byceps.services.site_navigation.models import (
NavItem,
NavItemID,
NavItemTargetType,
NavMenu,
NavMenuAggregate,
NavMenuID,
)
from byceps.util.framework.blueprint import create_blueprint
from byceps.util.framework.flash import flash_error, flash_success
from byceps.util.framework.templating import templated
from byceps.util.views import (
permission_required,
redirect_to,
respond_no_content,
)
from .forms import (
ItemCreatePageForm,
ItemCreateUrlForm,
ItemCreateViewForm,
ItemUpdateForm,
MenuCreateForm,
MenuUpdateForm,
SubMenuCreateForm,
)
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 = site_navigation_service.get_menus(site.id)
menu_trees = site_navigation_service.get_menu_trees(site.id)
return {
'site': site,
'brand': brand,
'menus': menus,
'menu_trees': menu_trees,
}
@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,
}
@blueprint.get('/for_site/<site_id>/create')
@permission_required('site_navigation.administrate')
@templated
def menu_create_form(site_id, erroneous_form=None):
"""Show form to create a menu."""
site = _get_site_or_404(site_id)
brand = brand_service.get_brand(site.brand_id)
form = erroneous_form if erroneous_form else MenuCreateForm()
return {
'site': site,
'brand': brand,
'form': form,
}
@blueprint.post('/for_site/<site_id>')
@permission_required('site_navigation.administrate')
def menu_create(site_id):
"""Create a menu."""
site = _get_site_or_404(site_id)
form = MenuCreateForm(request.form)
if not form.validate():
return menu_create_form(site_id, form)
name = form.name.data.strip()
language_code = form.language_code.data.strip()
hidden = form.hidden.data
menu = site_navigation_service.create_menu(
site.id, name, language_code, hidden=hidden
)
flash_success(gettext('Menu "%(name)s" has been created.', name=menu.name))
return redirect_to('.view', menu_id=menu.id)
@blueprint.get('/for_site/<site_id>/create/below/<parent_menu_id>')
@permission_required('site_navigation.administrate')
@templated
def submenu_create_form(site_id, parent_menu_id, erroneous_form=None):
"""Show form to create a submenu."""
site = _get_site_or_404(site_id)
brand = brand_service.get_brand(site.brand_id)
parent_menu = site_navigation_service.get_menu(parent_menu_id).unwrap()
form = erroneous_form if erroneous_form else SubMenuCreateForm()
return {
'site': site,
'brand': brand,
'form': form,
'parent_menu': parent_menu,
}
@blueprint.post('/for_site/<site_id>/below/<parent_menu_id>')
@permission_required('site_navigation.administrate')
def submenu_create(site_id, parent_menu_id):
"""Create a submenu."""
site = _get_site_or_404(site_id)
parent_menu = site_navigation_service.get_menu(parent_menu_id).unwrap()
form = SubMenuCreateForm(request.form)
if not form.validate():
return submenu_create_form(site_id, parent_menu_id, form)
name = form.name.data.strip()
hidden = form.hidden.data
menu = site_navigation_service.create_menu(
site.id,
name,
parent_menu.language_code,
hidden=hidden,
parent_menu_id=parent_menu_id,
)
flash_success(gettext('Menu "%(name)s" has been created.', name=menu.name))
return redirect_to('.view', menu_id=menu.id)
@blueprint.get('/menus/<menu_id>/update')
@permission_required('site_navigation.administrate')
@templated
def menu_update_form(menu_id, erroneous_form=None):
"""Show form to update the menu."""
menu = _get_menu_or_404(menu_id)
site = site_service.get_site(menu.site_id)
brand = brand_service.get_brand(site.brand_id)
form = erroneous_form if erroneous_form else MenuUpdateForm(obj=menu)
return {
'menu': menu,
'site': site,
'brand': brand,
'form': form,
}
@blueprint.post('/menus/<menu_id>')
@permission_required('site_navigation.administrate')
def menu_update(menu_id):
"""Update the menu."""
menu = _get_menu_or_404(menu_id)
form = MenuUpdateForm(request.form)
if not form.validate():
return menu_update_form(menu.id, form)
name = form.name.data.strip()
language_code = form.language_code.data.strip()
hidden = form.hidden.data
menu = site_navigation_service.update_menu(
menu.id, name, language_code, hidden
).unwrap()
flash_success(gettext('Menu "%(name)s" has been updated.', name=menu.name))
return redirect_to('.view', menu_id=menu.id)
@blueprint.get('/for_menu/<menu_id>/create/for_page')
@permission_required('site_navigation.administrate')
@templated
def item_create_page_form(menu_id, erroneous_form=None):
"""Show form to create a menu item referencing a page."""
menu = _get_menu_or_404(menu_id)
site = site_service.get_site(menu.site_id)
brand = brand_service.get_brand(site.brand_id)
form = erroneous_form if erroneous_form else ItemCreatePageForm()
form.set_page_choices(site.id)
return {
'menu': menu,
'site': site,
'brand': brand,
'form': form,
'target_type_name': NavItemTargetType.page.name,
}
@blueprint.get('/for_menu/<menu_id>/create/for_url')
@permission_required('site_navigation.administrate')
@templated
def item_create_url_form(menu_id, erroneous_form=None):
"""Show form to create a menu item referencing a URL."""
menu = _get_menu_or_404(menu_id)
site = site_service.get_site(menu.site_id)
brand = brand_service.get_brand(site.brand_id)
form = erroneous_form if erroneous_form else ItemCreateUrlForm()
return {
'menu': menu,
'site': site,
'brand': brand,
'form': form,
'target_type_name': NavItemTargetType.url.name,
}
@blueprint.get('/for_menu/<menu_id>/create/for_view')
@permission_required('site_navigation.administrate')
@templated
def item_create_view_form(menu_id, erroneous_form=None):
"""Show form to create a menu item referencing a view."""
menu = _get_menu_or_404(menu_id)
site = site_service.get_site(menu.site_id)
brand = brand_service.get_brand(site.brand_id)
form = erroneous_form if erroneous_form else ItemCreateViewForm()
form.set_view_type_choices()
return {
'menu': menu,
'site': site,
'brand': brand,
'form': form,
'target_type_name': NavItemTargetType.view.name,
}
@blueprint.post('/for_menu/<menu_id>/<target_type_name>')
@permission_required('site_navigation.administrate')
def item_create(menu_id, target_type_name):
"""Create a menu item."""
menu = _get_menu_or_404(menu_id)
try:
target_type = NavItemTargetType[target_type_name]
except KeyError:
abort(400, f'Unknown target type "{target_type_name}"')
form = _get_create_form(target_type, request, menu)
if not form.validate():
form_view = _get_create_form_view(target_type)
return form_view(menu.id, form)
target, current_page_id = _get_target_and_current_page_id_for_create_form(
target_type, form
)
label = form.label.data.strip()
hidden = form.hidden.data
item = site_navigation_service.create_item(
menu.id, target_type, target, label, current_page_id, hidden=hidden
).unwrap()
flash_success(
gettext('Menu item "%(label)s" has been created.', label=item.label)
)
return redirect_to('.view', menu_id=menu.id)
def _get_create_form(target_type: NavItemTargetType, request, menu: NavMenu):
match target_type:
case NavItemTargetType.page:
form = ItemCreatePageForm(request.form)
form.set_page_choices(menu.site_id)
case NavItemTargetType.url:
form = ItemCreateUrlForm(request.form)
case NavItemTargetType.view:
form = ItemCreateViewForm(request.form)
form.set_view_type_choices()
return form
def _get_create_form_view(target_type: NavItemTargetType):
match target_type:
case NavItemTargetType.page:
return item_create_page_form
case NavItemTargetType.url:
return item_create_url_form
case NavItemTargetType.view:
return item_create_view_form
def _get_target_and_current_page_id_for_create_form(
target_type: NavItemTargetType, form
) -> tuple[str, str]:
match target_type:
case NavItemTargetType.page:
page = page_service.get_page(form.target_page_id.data)
target = page.name
current_page_id = page.current_page_id
case NavItemTargetType.url:
target = form.target_url.data.strip()
current_page_id = form.current_page_id.data.strip()
case NavItemTargetType.view:
view_type_name = form.target_view_type.data
view_type = site_navigation_service.find_view_type_by_name(
view_type_name
)
if not view_type:
abort(400, f'Unknown view type "{view_type_name}"')
target = view_type.name
current_page_id = view_type.current_page_id
return target, current_page_id
@blueprint.get('/items/<uuid:item_id>/update')
@permission_required('site_navigation.administrate')
@templated
def item_update_form(item_id, erroneous_form=None):
"""Show form to update the menu item."""
item = _get_item_or_404(item_id)
menu = site_navigation_service.get_menu(item.menu_id).unwrap()
site = site_service.get_site(menu.site_id)
brand = brand_service.get_brand(site.brand_id)
data = dataclasses.asdict(item)
data['target_type'] = item.target_type.name
form = erroneous_form if erroneous_form else ItemUpdateForm(data=data)
return {
'item': item,
'menu': menu,
'site': site,
'brand': brand,
'form': form,
}
@blueprint.post('/items/<uuid:item_id>')
@permission_required('site_navigation.administrate')
def item_update(item_id):
"""Update the menu item."""
item = _get_item_or_404(item_id)
form = ItemUpdateForm(request.form)
if not form.validate():
return item_update_form(item.id, form)
label = form.label.data.strip()
target_type = NavItemTargetType[form.target_type.data]
target = form.target.data.strip()
current_page_id = form.current_page_id.data.strip()
hidden = form.hidden.data
item = site_navigation_service.update_item(
item.id, target_type, target, label, current_page_id, hidden
).unwrap()
flash_success(
gettext('Menu item "%(label)s" has been updated.', label=item.label)
)
return redirect_to('.view', menu_id=item.menu_id)
@blueprint.post('/items/<uuid:item_id>/up')
@permission_required('site_navigation.administrate')
@respond_no_content
def item_move_up(item_id):
"""Move the menu item upwards by one position."""
item = _get_item_or_404(item_id)
move_result = site_navigation_service.move_item_up(item.id)
if move_result.is_err():
flash_error(
gettext(
'Item "%(label)s" is already at the top.',
label=item.label,
)
)
else:
flash_success(
gettext(
'Item "%(label)s" has been moved upwards by one position.',
label=item.label,
)
)
@blueprint.post('/items/<uuid:item_id>/down')
@permission_required('site_navigation.administrate')
@respond_no_content
def item_move_down(item_id):
"""Move the menu item downwards by one position."""
item = _get_item_or_404(item_id)
move_result = site_navigation_service.move_item_down(item.id)
if move_result.is_err():
flash_error(
gettext(
'Item "%(label)s" is already at the bottom.',
label=item.label,
)
)
else:
flash_success(
gettext(
'Item "%(label)s" has been moved downwards by one position.',
label=item.label,
)
)
@blueprint.delete('/items/<uuid:item_id>')
@permission_required('site_navigation.administrate')
@respond_no_content
def item_delete(item_id):
"""Remove the menu item."""
item = _get_item_or_404(item_id)
label = item.label
site_navigation_service.delete_item(item.id).unwrap()
flash_success(gettext('Item "%(label)s" has been deleted.', label=label))
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_or_404(menu_id: NavMenuID) -> NavMenu:
menu = site_navigation_service.find_menu(menu_id)
if menu is None:
abort(404)
return menu
def _get_menu_aggregate_or_404(menu_id: NavMenuID) -> NavMenuAggregate:
menu = site_navigation_service.find_menu_aggregate(menu_id)
if menu is None:
abort(404)
return menu
def _get_item_or_404(item_id: NavItemID) -> NavItem:
item = site_navigation_service.find_item(item_id)
if item is None:
abort(404)
return item