Source code for publiforge.lib.widget
# $Id$
# -*- coding: utf-8 -*-
"""Some various widgets."""
from webhelpers2.html import literal
from sqlalchemy import select
from ..lib.i18n import _
from ..lib.utils import has_permission
from ..models import TRUE, DBSession
from ..models.groups import GROUP_USER
from ..models.storages import Storage, StorageUser
from ..models.projects import Project, ProjectUser, ProjectGroup
from ..models.tasks import Task
from ..models.jobs import Job
# =============================================================================
[docs]class Breadcrumbs(object):
"""User breadcrumb trail, current title page and back URL management.
This class uses session and stores its history in
``session['breadcrumbs']``. It is a list of crumbs. Each crumb is a tuple
such as ``(<title>, <route_name>, <route_parts>, <chunks_to_compare>)``.
"""
# -------------------------------------------------------------------------
def __init__(self, request):
"""Constructor method."""
self._request = request
# -------------------------------------------------------------------------
[docs] def trail(self):
"""Output XHTML breadcrumb trail."""
if 'breadcrumbs' not in self._request.session \
or len(self._request.session['breadcrumbs']) < 2:
return literal(' ')
translate = self._request.localizer.translate
crumbs = []
for crumb in self._request.session['breadcrumbs'][0:-1]:
if crumb[1] is not None:
crumbs.append(u'<a href="%s">%s</a>' % (
self._request.route_path(crumb[1], **crumb[2]),
translate(crumb[0])))
else:
crumbs.append(translate(crumb[0]))
return literal(u' ยป '.join(crumbs))
# -------------------------------------------------------------------------
[docs] def current_title(self):
"""Title of current page."""
if 'breadcrumbs' not in self._request.session \
or len(self._request.session['breadcrumbs']) < 1 \
or not self._request.session['breadcrumbs'][-1][1]:
return _('home')
return self._request.localizer.translate(
self._request.session['breadcrumbs'][-1][0])
# -------------------------------------------------------------------------
[docs] def current_path(self):
"""Path of current page."""
if 'breadcrumbs' not in self._request.session \
or len(self._request.session['breadcrumbs']) < 1 \
or not self._request.session['breadcrumbs'][-1][1]:
return self._request.route_path('home')
return self._request.route_path(
self._request.session['breadcrumbs'][-1][1],
**self._request.session['breadcrumbs'][-1][2])
# -------------------------------------------------------------------------
[docs] def back_title(self):
"""Output title of previous page."""
if 'breadcrumbs' not in self._request.session \
or len(self._request.session['breadcrumbs']) < 2 \
or not self._request.session['breadcrumbs'][-2][1]:
return _('home')
return self._request.localizer.translate(
self._request.session['breadcrumbs'][-2][0])
# -------------------------------------------------------------------------
[docs] def back_path(self):
"""Output the path of previous page."""
if 'breadcrumbs' not in self._request.session \
or len(self._request.session['breadcrumbs']) < 2 \
or not self._request.session['breadcrumbs'][-2][1]:
return self._request.route_path('home')
return self._request.route_path(
self._request.session['breadcrumbs'][-2][1],
**self._request.session['breadcrumbs'][-2][2])
# -------------------------------------------------------------------------
[docs] def back_button(self):
"""A button to return to the previous page."""
return literal(
u'<a href="{0}" title="{1}">'
'<img src="/Static/Images/back.png" alt="Back"/></a>'.format(
self.back_path(), self.back_title()))
# -------------------------------------------------------------------------
[docs] def add(self, title, length=10, root_chunks=10, replace=None, anchor=None,
keep=False):
"""Add a crumb in breadcrumb trail.
:param title: (string)
Page title in breadcrumb trail.
:param length: (int, default=10)
Maximum crumb number. If 0, it keeps the current length.
:param root_chunks: (int, default=10)
Number of path chunks to compare to highlight menu item.
:param replace: (string, optional):
If current path is ``replace``, this method call :meth:`pop` before
any action.
:param anchor: (string, optional)
Anchor to add.
:param keep: (boolean, optional)
If ``True``, keep the last crumb even if it is the same as the
current.
"""
# pylint: disable = too-many-arguments
# Environment
session = self._request.session
if 'breadcrumbs' not in session:
session['breadcrumbs'] = [(_('Home'), 'home', {}, 1)]
if not length:
length = len(session['breadcrumbs'])
# Replace
if replace and self.current_path() == replace:
self.pop()
# Scan old breadcrumb trail to find the right position
route_name = \
self._request.matched_route and self._request.matched_route.name
if route_name is None:
session['breadcrumbs'].append((title, None, dict(), root_chunks))
return
compare_name = route_name.replace('_root', '_browse')\
.replace('_task', '').replace('_pack', '') + (keep and '_' or '')
crumbs = []
for crumb in session['breadcrumbs']:
crumb_name = crumb[1] and crumb[1].replace('_root', '_browse')\
.replace('_task', '').replace('_pack', '')
if len(crumbs) >= length - 1 or crumb_name == compare_name:
break
crumbs.append(crumb)
# Add new breadcrumb
params = self._request.matchdict
if anchor is not None:
params['_anchor'] = anchor
crumbs.append((title, route_name, params, root_chunks))
session['breadcrumbs'] = crumbs
# -------------------------------------------------------------------------
[docs] def pop(self):
"""Pop last breadcrumb."""
session = self._request.session
if 'breadcrumbs' in session and len(session['breadcrumbs']) > 1:
session['breadcrumbs'] = session['breadcrumbs'][0:-1]
# =============================================================================
[docs]class Menu(object):
"""User menu management."""
# -------------------------------------------------------------------------
def __init__(self, request):
"""Constructor method."""
self._request = request
self._translate = request.localizer.translate
self.active = 'user_id' in self._request.session
# -------------------------------------------------------------------------
[docs] def update(self):
"""Update the session with menu content."""
if not self.active:
return
menu = []
# Storages
if has_permission(self._request, 'stg_user'):
submenu = 'storage.index' in self._request.registry.settings \
and [self._entry(_('Advanced search'), 'file_search'),
self._entry(_('All storages'), 'storage_index')] \
or [self._entry(_('All storages'), 'storage_index')]
self._storage_entries(self._request.session['user_id'], submenu)
menu.append(
self._entry(_('Storages'), None, submenu, icon="storage"))
# Projects
if has_permission(self._request, 'prj_user'):
submenu = [self._entry(_('All projects'), 'project_index')]
self._project_entries(self._request.session['user_id'], submenu)
menu.append(
self._entry(_('Projects'), None, submenu, icon="project"))
# Administration
submenu = []
if has_permission(self._request, 'admin'):
submenu.append(self._entry(_('Site'), 'site_admin'))
if has_permission(self._request, 'usr_editor'):
submenu.append(self._entry(_('Users'), 'user_admin'))
if has_permission(self._request, 'grp_editor'):
submenu.append(self._entry(_('Groups'), 'group_admin'))
if has_permission(self._request, 'stg_editor'):
submenu.append(self._entry(_('Storages'), 'storage_admin'))
if has_permission(self._request, 'idx_editor'):
submenu.append(self._entry(_('Indexing'), 'indexer_admin'))
if has_permission(self._request, 'prj_editor'):
submenu.append(self._entry(_('Projects'), 'project_admin'))
if submenu:
menu.append(
self._entry(_('Administration'), None, submenu, icon="admin"))
self._request.session['menu'] = tuple(menu)
# -------------------------------------------------------------------------
[docs] def xhtml(self):
"""Return an <ul> structure with current entry highlighted."""
if not self.active:
return ''
if 'menu' not in self._request.session:
self.update()
# Make XHTML
menu = self._request.session['menu']
html = '<ul>%s</ul>' % self._xhtml_entries(menu, 0)
# Highlight current entry
if 'breadcrumbs' in self._request.session:
for crumb in reversed(self._request.session['breadcrumbs'][1:]):
if crumb[1] is None:
continue
params = dict(
(k, crumb[2][k]) for k in crumb[2] if k != '_anchor')
path = self._request.route_path(crumb[1], **params)
path = '/'.join(path.split('/')[0:crumb[3] + 1])\
.replace('/edit/', '/view/')
if 'href="%s"' % path in html:
html = html.replace(
'<a class="slow" href="%s"' % path,
'<a class="slow current" href="%s"' % path)
break
# Tag current project
if 'project' in self._request.session:
path = self._request.route_path(
'project_dashboard',
project_id=self._request.session['project']['project_id'],
_query={'id': self._request.session['project']['project_id']})
html = html.replace(
'<li><a class="slow" href="%s"' % path,
'<li class="active"><a class="slow" href="%s"' % path)
return literal(html)
# -------------------------------------------------------------------------
def _xhtml_entries(self, entries, depth):
"""Return <li> tags with entries.
:param entries: (tuple)
Tuple of entry tuples (See :meth:`_entry`)
:param depth: (integer)
Depth of entries in menu.
"""
html = ''
for entry in entries:
tag = (depth == 0 and '<strong>') \
or (depth == 1 and entry[3] and '<em>') or ''
html += '<li>' \
+ (entry[2] and '<a class="slow" href="%s">' % entry[2] or '')\
+ tag \
+ (entry[1] and '<img src="/Static/Images/%s.png" alt="%s"/> '
% (entry[1], entry[1]) or '') + entry[0] \
+ tag.replace('<', '</') \
+ (entry[2] and '</a>' or '')
if entry[4]:
html += \
'<ul>%s</ul>' % self._xhtml_entries(entry[4], depth + 1)
html += '</li>'
return html
# -------------------------------------------------------------------------
def _storage_entries(self, user_id, submenu):
"""Update menu entries for user storages shown in menu.
:param user_id: (string)
Current user ID.
:param submenu: (list)
Current submenu list.
"""
# Look for user storages
for storage in DBSession.query(Storage).join(StorageUser)\
.filter(Storage.access != 'closed')\
.filter(StorageUser.user_id == user_id)\
.filter(StorageUser.in_menu == TRUE).order_by(Storage.label):
submenu.append(self._entry(
storage.label, 'storage_root', None, True,
storage_id=storage.storage_id))
# -------------------------------------------------------------------------
def _project_entries(self, user_id, submenu):
"""Update menu entries for user projects shown in menu.
:param user_id: (string)
Current user ID.
:param submenu: (list)
Current submenu list.
"""
groups = [k.group_id for k in DBSession.execute(
select([GROUP_USER], GROUP_USER.c.user_id == user_id))]
for project in DBSession.query(
Project.project_id, Project.label, ProjectUser.perm,
ProjectUser.entries).join(ProjectUser)\
.filter(ProjectUser.user_id == user_id)\
.filter(ProjectUser.in_menu == TRUE).order_by(Project.label):
pid = project[0]
subentries = [
self._entry(_('Dashboard'), 'project_dashboard',
None, True, project_id=pid)]
if project[3] in ('all', 'tasks') and \
DBSession.query(Task.task_id).filter_by(project_id=pid).first()\
is not None:
subentries.append(self._entry(
_('Tasks'), 'task_index', None, True, project_id=pid))
if project[3] in ('all', 'packs'):
subentries.append(self._entry(
_('Packs'), 'pack_index', None, True, project_id=pid))
subentries.append(self._entry(
_('Last results'), 'build_results', None, True,
project_id=pid))
perm = has_permission(self._request, 'prj_editor') and 'leader' \
or project[2]
if perm != 'leader' and groups and 'leader' in \
[k[0] for k in DBSession.query(ProjectGroup.perm)
.filter_by(project_id=pid)
.filter(ProjectGroup.group_id.in_(groups))]:
perm = 'leader'
if perm == 'leader':
if DBSession.query(Job.job_id).filter_by(project_id=pid)\
.first() is not None:
subentries.append(self._entry(
_('Background jobs'), 'job_index', None, True,
project_id=pid))
subentries.append(self._entry(
_('Settings'), 'project_view', None, True, project_id=pid))
submenu.append(self._entry(
project[1], 'project_dashboard', tuple(subentries), True,
project_id=pid, _query={'id': pid}))
# -------------------------------------------------------------------------
def _entry(self, label, route_name, subentries=None, is_minor=False,
icon=None, **kwargs):
"""A menu entry tuple.
:param label: (string)
Label of the entry.
:param route_name: (string)
Name of the route for the link.
:param subentries: (list, optional)
List of subentries.
:param is_minor: (boolean, default=False)
Indicate whether this entry is a minor one.
:param icon: (string, optional)
Icon for this entry.
:param kwargs: (dictionary)
Keyworded arguments for :meth:`pyramid.request.Request.route_path`.
:return: (tuple)
A tuple such as
``(label, icon, url, is_minor, (subentry, subentry...))``.
"""
return (self._translate(label), icon,
route_name and self._request.route_path(route_name, **kwargs),
is_minor, subentries and tuple(subentries))
# =============================================================================
[docs]class TabSet(object):
"""A class to manages tabs."""
# -------------------------------------------------------------------------
def __init__(self, request, labels):
"""Constructor method."""
self._request = request
self.labels = labels
# -------------------------------------------------------------------------
[docs] def toc(self, tab_id):
"""Output a table of content of the ``TabSet`` in an ``<ul>``
structure.
:param tab_id: (string)
Tab set ID.
:return: (string)
``<ul>`` structure.
"""
translate = self._request.localizer.translate
xml = '<ul id="%s" class="tabs">\n' % tab_id
for index, label in enumerate(self.labels):
xml += ' <li><a class="tab" id="tab%d" href="#tabContent%d">' \
'<span>%s</span></a></li>\n' \
% (index, index, translate(label))
xml += '</ul>\n'
return literal(xml)
# -------------------------------------------------------------------------
[docs] def tab_begin(self, index, access_key=None):
"""Open a tab zone.
:param index: (integer)
Tab index.
:param access_key: (string, optional)
Access key for tab.
:return: (string)
Opening ``fieldset`` structure with legend.
"""
return literal(
'<fieldset class="tabContent" id="tabContent%d">\n'
' <legend%s><span>%s</span></legend>\n'
% (index, access_key and ' accesskey="%s"' % access_key or '',
self._request.localizer.translate(self.labels[index])))
# -------------------------------------------------------------------------