import logging
import os
import pkg_resources
from mako.lookup import TemplateLookup
from formbar.fields import rules_to_string
from formbar.renderer import (
FieldRenderer,
DropdownFieldRenderer as FormbarDropdown,
SelectionFieldRenderer as FormbarSelectionField,
CheckboxFieldRenderer as FormbarCheckboxField
)
import ringo.lib.helpers as helpers
from ringo.lib.helpers import get_action_routename, literal, escape, HTML
from ringo.model.base import BaseItem, BaseList, get_item_list
from ringo.lib.table import get_table_config
import ringo.lib.security as security
base_dir = pkg_resources.get_distribution("ringo").location
template_dir = os.path.join(base_dir, 'ringo', 'templates')
template_lookup = TemplateLookup(directories=[template_dir],
default_filters=['h'])
log = logging.getLogger(__name__)
###########################################################################
# Renderers for form elements #
###########################################################################
def add_renderers(custom_renderers):
"""Helper function to add ringo ringo specific renderers for form
rendering."""
for key in custom_renderers:
if key not in renderers:
renderers[key] = custom_renderers[key]
return renderers
def get_link_url(item, request, actionname=None, backurl=False):
"""Return a url to the given item. On default the link will be a
link to the update or read view of the item depending on the
permission. If the application is configured to open items in read
mode on default the update action will not be checked. If the user
does not have enough permissions than None is returned. Optionally
you can provide the name of a action. In this case the url will be
build for the given actionname if the user has enough
permissions."""
readmode = request.registry.settings.get("app.readmode") in ["True", "true"]
if isinstance(item, BaseItem):
if actionname:
from ringo.views.helpers import get_item_modul
modul = get_item_modul(request, item)
action = modul.get_action(actionname)
if action is None:
# This can happen if the action is not part of the modul
# but a custum userdefined view. No permission checks
# are done here yet.
route_name = get_action_routename(item, actionname)
else:
permission = action.permission or action.name.lower()
if security.has_permission(permission, item, request):
route_name = get_action_routename(item,
action.name.lower())
else:
return None
elif not readmode and security.has_permission("update", item, request):
route_name = get_action_routename(item, 'update')
elif security.has_permission("read", item, request):
route_name = get_action_routename(item, 'read')
else:
return None
query = {}
if backurl:
clazz = request.context.__model__
query['backurl'] = request.session.get('%s.backurl' % clazz) or request.current_route_path()
return request.route_path(route_name, id=item.id, _query=query)
return None
def filter_options_on_permissions(request, options):
"""Will iterate over all options and build a tuple for each option
with the following information:
1. The option itself
2. The value of the option
3. A flag indicating if the requesting user is allowed to `link` the
option. This information is later used on rendering.
:request: current request
:options: list of tuple of options (item, value, linkable)
:returns: filtered list of option tuples.
"""
filtered_options = []
for option in options:
linkable = False
if (option[2] and (security.has_permission('link', option[0], request) or
not hasattr(option[0], 'owner'))):
linkable = True
filtered_options.append((option[0], option[1], linkable))
return filtered_options
[docs]class LinkFieldRenderer(FieldRenderer):
""" Will rendere a Link to the item. The item which will be linked
can be set by
* the name attribute of the entity
* the value attribute of the entity
If the item can not be determined by one of the options above the
item will be the item linked to the form.
The following option in
addition to the base Linkfield Renderer are supported:
* openmodal: If true the item will be opened in a modal form.
Defaults to false. If true a backlink will be generated too.
* action: Name of the action for which the link will be rendered.
Defaults to None with means the link will be either to the read or
update view.
* css: CSS classes for the link.
* backlink: If set to true a backlink will be generated.
* target: Set target attribute of the link.
"""
def __init__(self, field, translate):
"""@todo: to be defined"""
FieldRenderer.__init__(self, field, translate)
self.template = template_lookup.get_template("internal/linkfield.mako")
def _get_template_values(self):
values = FieldRenderer._get_template_values(self)
values['link_text'] = self._field.label
try:
item = getattr(self._field._form._item, self._field.name)
except AttributeError:
# If the attribute is not part of the item there may be a
# value in the field based on a expression. So set the item
# to the value of the field. If this is not an instance of
# BaseItem the link can not be generated.
item = self._field.value
if not item:
name = self._field.name
item = self._field._form._item
# Only print warning if the fieldname is not a helper
# field beginning with "_"
if not name.startswith("_"):
log.warning("Missing value for %s in %s" % (name, item))
backurl = self.openmodal == "true" or self.backlink == "true"
values['url'] = get_link_url(item,
self._field._form._request,
self._field.renderer.action,
backurl) or "#"
return values
def _render_label(self):
return ""
[docs]class CheckboxFieldRenderer(FormbarCheckboxField):
"""Ringo specific DropdownFieldRenderer. Will additionally check
permission to read on the items in the option list"""
def _get_template_values(self):
values = FormbarCheckboxField._get_template_values(self)
if self._field.readonly:
values['options'] = []
else:
values['options'] = filter_options_on_permissions(
self._field._form._request,
values['options'])
return values
[docs]class DropdownFieldRenderer(FormbarDropdown):
"""Ringo specific DropdownFieldRenderer. This renderer add a small
link next to the label to make it possible to jump to the selected
item in the dropdown list.
* nolink: Flag "true" or "false" to configure completly disable
linking Default to "false".
"""
def __init__(self, field, translate):
"""@todo: to be defined"""
FormbarDropdown.__init__(self, field, translate)
self.template = template_lookup.get_template("internal/dropdown.mako")
def _get_template_values(self):
values = FormbarDropdown._get_template_values(self)
if self._field.readonly:
values['options'] = []
else:
values['options'] = filter_options_on_permissions(
self._field._form._request,
values['options'])
values['h'] = helpers
return values
def render_link(self):
html = []
items = []
try:
item = getattr(self._field._form._item, self._field.name)
except AttributeError:
log.warning("Missing %s attribute in %s"
% (self._field.name, self._field._form._item))
return literal("").join(html)
if not isinstance(item, list):
items.append(item)
else:
items = item
for item in items:
url = get_link_url(item, self._field._form._request)
if url:
html.append(HTML.tag("a", href=("%s" % url), _closed=False))
html.append(escape(unicode(item)))
html.append(HTML.tag("/a", _closed=False))
return literal("").join(html)
def _render_label(self):
html = []
html.append(FormbarDropdown._render_label(self))
if not self._field.readonly and not self.nolink == "true":
link = self.render_link()
if link:
html.append(" [")
html.append(link)
html.append("]")
return literal("").join(html)
[docs]class StateFieldRenderer(FormbarDropdown):
"""Ringo specific DropdownFieldRenderer to change the current state
of an item. This renderer will render a combined widget containing
a textfield showing the current state of the item and a dropdown
with available actions which can be done from this state.
* layout: Option to change the layout of the statefield. The
renderer offers currently three options to render setting the
`layout` attribute of the renderer:
1. default: In default the renderer shows the current state with
description and the resulting state and description when
choosing a transtion.
2. simple: Simple will render a simple dropdown with the current
state as part of the fields label and the available
transitions as options of the dropdown
3. button: Botton will render the current state the available
transitions as a button. The button will work like a submit
button of the form so if the user clicks on it the form data
will be submitted and the state is changed.
"""
def __init__(self, field, translate):
"""@todo: to be defined"""
FormbarDropdown.__init__(self, field, translate)
self.template = template_lookup.get_template("internal/"
"statefield.mako")
def _render_label(self):
return literal("")
def _get_template_values(self):
values = FieldRenderer._get_template_values(self)
item = self._field._form._item
sm = item.get_statemachine(self._field.name,
request=self._field._form._request)
state = sm.get_state()
values["state"] = state
values["request"] = self._field._form._request
return values
[docs]class ListingFieldRenderer(FormbarSelectionField):
"""Renderer to render a listing of linked items. Used attributes:
* form: Name of the form which is used to add new items
* table: Name of the table configuration which is used to list items
* hideadd: Flag "true" or "false" to configure hiding the add button.
* nolinks: Flag "true" or "false" to configure completly disable
linking.
* showsearch: Flag "true" or "false" to configure rendering a search
field.
* showall: "true" or "false". If true all items (linked and
unlinked) regardless if the current user is allowed to read
or update the item will be listed. However clicking on items the
user has no permission to read will have no effect as no links are
rendered for those items. Defaults to false.
* onlylinked: "true" or "false". If true only linked items will be
rendered. Checkboxes will be hidden.
* multiple: "true" or "false". If false only one option can be
selected. Defaults to true. Note that this restriction is only
implemented on client side.
* openmodal: "true" or "false". If true the item will be opened in a
modal popup.
* backlink: "true" or "false". If true the user will be redirected
back to the listing after creating a new item. Defaults to true.
* action: Define the action which will be called when clicking an an
entry. On default the action will be determined by checking the
users permission and choosing between "read" or "update". Setting
action you can enforce using a certain action if the user has
sufficient permissions.
Example::
<entity id="foo" name="name_of_the_orm_relation" ...>
<renderer type="listing" showall="true" table="details"/>
</entity>
Please note that the name of the entity must be the name of the
relation in the ORM model which links the items you want to list in
the listing.
"""
def __init__(self, field, translate):
FormbarSelectionField.__init__(self, field, translate)
self.template = template_lookup.get_template("internal/listfield.mako")
def get_class(self):
try:
clazz = self._field._get_sa_mapped_class()
return clazz
except AttributeError:
# Special logic if the fields item is not an SQLAlchemy
# attribute but a python property (used to add futher
# informations to the fields item model). In this case getting
# the clazz from the SA attributes mapper -> clazz will
# fail.
clazz = getattr(self._field._form._item, self._field.name)
if isinstance(clazz, BaseList):
return clazz.clazz
return clazz
@property
def itemlist(self):
clazz = self.get_class()
if self.showall == 'true':
itemlist = get_item_list(self._field._form._request,
clazz)
else:
itemlist = get_item_list(self._field._form._request,
clazz,
user=self._field._form._request.user)
config = get_table_config(itemlist.clazz,
self._field._config.renderer.table)
sort_field = config.get_default_sort_column()
sort_order = config.get_default_sort_order()
itemlist.sort(sort_field, sort_order)
# Warning filtering items here can cause loosing relations to
# the filtered items. This is esspecially true if the item which
# was related to the item before now gets filtered because of a
# changes value in an attribute e.g. In this case the filtered
# item is not in the list at all and will not be sent on a POST
# request. This will result in removing the relation!
search = config.get_default_search()
itemlist.filter(search)
return itemlist
def _get_selected_items(self, items):
try:
selected = getattr(self._field._form._item, self._field.name) or []
# Usually the getattr function should return a single item
# or a list of items. But in some cases the attribute can be
# a BaseList. This can happen if the attribute is a python
# property of the item to dynamically add some attributes.
# This property might return a BaseList.
if isinstance(selected, BaseList):
selected = selected.items
elif not isinstance(selected, list):
selected = [selected]
selected_ids = [s.id for s in selected]
return [i for i in items if i.id in selected_ids]
except AttributeError:
# Can happen when designing forms an the model of the item
# is not yet configured.
log.warning("Missing %s attribute in %s"
% (self._field.name, repr(self._field._form._item)))
return []
def render(self):
"""Initialize renderer"""
html = []
active = 'active' if self._active else 'inactive'
config = self._field._config.renderer
has_errors = len(self._field.get_errors())
has_warnings = len(self._field.get_warnings())
class_options = "form-group %s %s %s" % ((has_errors and 'has-error'),
(has_warnings and 'has-warning'), (active))
html.append(HTML.tag("div", _closed=False,
rules=u"{}".format(";".join(rules_to_string(self._field))),
formgroup="{}".format(self._field.name),
desired="{}".format(self._field.desired),
required="{}".format(self._field.required),
class_=class_options))
html.append(self._render_label())
# All items which can potentially be linked. However this list
# of items may be already filtered be defining a default filter
# in the overview configuration which is used for this renderer.
items = self.itemlist.items
selected_items = self._get_selected_items(items)
if self._field.readonly or self.onlylinked == "true":
items = selected_items
# Filter options based on the configured filter expression of
# the renderer. Please note the this method return a list of
# tuples.
item_tuples = self._field.filter_options(items)
# Get filtered options and only use the items which are
# in the origin items list and has passed filtering.
item_tuples = self._field.filter_options(items)
# Filter the items again based on the permissions. This means
# resetting the third value in the tuple.
if self.showall != "true" and not self._field.readonly:
item_tuples = filter_options_on_permissions(self._field._form._request,
item_tuples)
values = {'items': item_tuples,
'selected_item_ids': [i.id for i in selected_items],
'field': self._field,
'clazz': self.get_class(),
'pclazz': self._field._form._item.__class__,
'request': self._field._form._request,
'_': self._field._form._translate,
's': security,
'h': helpers,
'url_getter': get_link_url,
'tableconfig': get_table_config(self.itemlist.clazz,
config.table)}
html.append(literal(self.template.render(**values)))
html.append(self._render_errors())
html.append(self._render_help())
html.append(HTML.tag("/div", _closed=False))
return literal("").join(html)
renderers = {
"dropdown": DropdownFieldRenderer,
"checkbox": CheckboxFieldRenderer,
"listing": ListingFieldRenderer,
"state": StateFieldRenderer,
"link": LinkFieldRenderer
}