[gen] Binary files stored in fields appy.fields.File are now stored outside the ZODB, on the filesystem; Ref fields can now also be rendered as dropdown menus: every menu represents a coherent group of link

ed objects. The main menu entry can be textual or an icon; computed fields are by default rendered in view and cell layouts.
This commit is contained in:
Gaetan Delannay 2014-02-26 10:40:27 +01:00
parent b9dcc94bdb
commit be145be254
12 changed files with 522 additions and 313 deletions

View file

@ -16,6 +16,7 @@
# ------------------------------------------------------------------------------
import sys, re
from appy import Object
from appy.fields import Field
from appy.px import Px
from appy.gen.layout import Table
@ -127,11 +128,127 @@ class Ref(Field):
onclick=":ajaxBaseCall.replace('**v**', 'True')"/>
</x>''')
# PX that displays referred objects as a list.
pxViewList = Px('''<x>
<!-- Display a simplified widget if at most 1 referenced object. -->
<table if="atMostOneRef">
<tr valign="top">
<!-- If there is no object -->
<x if="not zobjects">
<td class="discreet">:_('no_ref')</td>
<td>:field.pxAdd</td>
</x>
<!-- If there is an object -->
<x if="zobjects">
<td for="ztied in zobjects"
var2="includeShownInfo=True">:field.pxObjectTitle</td>
</x>
</tr>
</table>
<!-- Display a table in all other cases -->
<x if="not atMostOneRef">
<div if="not innerRef or showPlusIcon" style="margin-bottom: 4px">
(<x>:totalNumber</x>)
<x>:field.pxAdd</x>
<!-- The search button if field is queryable -->
<input if="zobjects and field.queryable" type="button" class="button"
style=":url('buttonSearch', bg=True)" value=":_('search_title')"
onclick=":'goto(%s)' % \
q('%s/search?className=%s&amp;ref=%s:%s' % \
(ztool.absolute_url(), tiedClassName, zobj.UID(), \
field.name))"/>
</div>
<!-- (Top) navigation -->
<x>:tool.pxNavigate</x>
<!-- No object is present -->
<p class="discreet" if="not zobjects">:_('no_ref')</p>
<table if="zobjects" class=":innerRef and 'innerAppyTable' or ''"
width="100%">
<tr valign="bottom">
<td>
<!-- Show forward or backward reference(s) -->
<table class=":not innerRef and 'list' or ''"
width=":innerRef and '100%' or field.layouts['view'].width"
var="columns=ztool.getColumnsSpecifiers(tiedClassName, \
field.shownInfo, dir)">
<tr if="field.showHeaders">
<th for="column in columns" width=":column.width"
align="column.align" var2="refField=column.field">
<span>:_(refField.labelId)</span>
<x>:field.pxSortIcons</x>
<x var="className=tiedClassName">:tool.pxShowDetails</x>
</th>
</tr>
<tr for="ztied in zobjects" valign="top"
class=":loop.ztied.odd and 'even' or 'odd'">
<td for="column in columns"
width=":column.width" align=":column.align"
var2="refField=column.field">
<!-- The "title" field -->
<x if="refField.name == 'title'">
<x>:field.pxObjectTitle</x>
<div if="ztied.mayAct()">:field.pxObjectActions</div>
</x>
<!-- Any other field -->
<x if="refField.name != 'title'">
<x var="zobj=ztied; obj=ztied.appy(); layoutType='cell';
innerRef=True; field=refField"
if="zobj.showField(field.name, \
layoutType='result')">:field.pxRender</x>
</x>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- (Bottom) navigation -->
<x>:tool.pxNavigate</x>
</x></x>''')
# PX that displays referred objects as menus.
pxViewMenus = Px('''
<table><tr style="font-size: 93%">
<td for="menu in field.getLinkedObjectsByMenu(obj, zobjects)">
<!-- A single object in the menu: show a clickable icon to get it -->
<a if="len(menu.zobjects)==1" var2="ztied=menu.zobjects[0]"
class="dropdownMenu" href=":field.getMenuUrl(zobj, ztied)"
title=":ztied.title">
<img if="menu.icon" src=":menu.icon"/><x
if="not menu.icon">:menu.text</x> 1</a>
<!-- Several objects: put them in a dropdown menu -->
<div if="len(menu.zobjects) &gt; 1" class="dropdownMenu"
var2="dropdownId='%s_%d' % (zobj.UID(), loop.menu.nb)"
onmouseover=":'toggleDropdown(%s)' % q(dropdownId)"
onmouseout=":'toggleDropdown(%s,%s)' % (q(dropdownId), q('none'))">
<img if="menu.icon" src=":menu.icon" title=":menu.text"/><x
if="not menu.icon">:menu.text</x>
<!-- Display the number of objects in the menu (if more than one) -->
<x if="len(menu.zobjects) &gt; 1">:len(menu.zobjects)</x>
<!-- The dropdown menu containing annexes -->
<div class="dropdown" id=":dropdownId">
<div for="ztied in menu.zobjects"
var2="ztiedUrl=field.getMenuUrl(zobj, ztied)">
<a href=":ztiedUrl">:ztied.title</a>
</div>
</div>
</div>
</td>
</tr></table>''')
# PX that displays referred objects through this field.
pxView = pxCell = Px('''
pxView = Px('''
<div var="innerRef=req.get('innerRef', False) == 'True';
ajaxHookId=zobj.UID() + field.name;
startNumber=int(req.get('%s_startNumber' % ajaxHookId, 0));
render=render|'list';
startNumber=field.getStartNumber(render, req, ajaxHookId);
info=field.getLinkedObjects(zobj, startNumber);
zobjects=info.objects;
totalNumber=info.totalNumber;
@ -151,93 +268,18 @@ class Ref(Field):
changeOrder=field.changeOrderEnabled(zobj);
showSubTitles=req.get('showSubTitles', 'true') == 'true'"
id=":ajaxHookId">
<!-- The definition of "atMostOneRef" above may sound strange: we
shouldn't check the actual number of referenced objects. But for
back references people often forget to specify multiplicities. So
concretely, multiplicities (0,None) are coded as (0,1). -->
<!-- Display a simplified widget if at most 1 referenced object. -->
<table if="atMostOneRef">
<tr valign="top">
<!-- If there is no object -->
<x if="not zobjects">
<td class="discreet">:_('no_ref')</td>
<td>:field.pxAdd</td>
</x>
<!-- If there is an object -->
<x if="zobjects">
<td for="ztied in zobjects"
var2="includeShownInfo=True">:field.pxObjectTitle</td>
</x>
</tr>
</table>
<!-- Display a table in all other cases -->
<x if="not atMostOneRef">
<div if="not innerRef or showPlusIcon" style="margin-bottom: 4px">
(<x>:totalNumber</x>)
<x>:field.pxAdd</x>
<!-- The search button if field is queryable -->
<input if="zobjects and field.queryable" type="button" class="button"
style=":url('buttonSearch', bg=True)" value=":_('search_title')"
onclick=":'goto(%s)' % \
q('%s/search?className=%s&amp;ref=%s:%s' % \
(ztool.absolute_url(), tiedClassName, zobj.UID(), \
field.name))"/>
</div>
<!-- (Top) navigation -->
<x>:tool.pxNavigate</x>
<!-- No object is present -->
<p class="discreet" if="not zobjects">:_('no_ref')</p>
<table if="zobjects" class=":innerRef and 'innerAppyTable' or ''"
width="100%">
<tr valign="bottom">
<td>
<!-- Show forward or backward reference(s) -->
<table class=":not innerRef and 'list' or ''"
width=":innerRef and '100%' or field.layouts['view'].width"
var="columns=ztool.getColumnsSpecifiers(tiedClassName, \
field.shownInfo, dir)">
<tr if="field.showHeaders">
<th for="column in columns" width=":column.width"
align="column.align" var2="refField=column.field">
<span>:_(refField.labelId)</span>
<x>:field.pxSortIcons</x>
<x var="className=tiedClassName">:tool.pxShowDetails</x>
</th>
</tr>
<tr for="ztied in zobjects" valign="top"
class=":loop.ztied.odd and 'even' or 'odd'">
<td for="column in columns"
width=":column.width" align=":column.align"
var2="refField=column.field">
<!-- The "title" field -->
<x if="refField.name == 'title'">
<x>:field.pxObjectTitle</x>
<div if="ztied.mayAct()">:field.pxObjectActions</div>
</x>
<!-- Any other field -->
<x if="refField.name != 'title'">
<x var="zobj=ztied; obj=ztied.appy(); layoutType='cell';
innerRef=True; field=refField"
if="zobj.showField(field.name, \
layoutType='result')">:field.pxRender</x>
</x>
</td>
</tr>
</table>
</td>
</tr>
</table>
<!-- (Bottom) navigation -->
<x>:tool.pxNavigate</x>
</x>
<x if="render == 'list'">:field.pxViewList</x>
<x if="render == 'menus'">:field.pxViewMenus</x>
</div>''')
# The "menus" render mode is only applicable in "cell", not in "view".
pxCell = Px('''<x var="render=field.render">:field.pxView</x>''')
pxEdit = Px('''
<select if="field.link"
var2="requestValue=req.get(name, []);
@ -287,7 +329,9 @@ class Ref(Field):
masterValue=None, focus=False, historized=False, mapping=None,
label=None, queryable=False, queryFields=None, queryNbCols=1,
navigable=False, searchSelect=None, changeOrder=True,
sdefault='', scolspan=1, swidth=None, sheight=None):
sdefault='', scolspan=1, swidth=None, sheight=None,
render='list', menuIdMethod=None, menuInfoMethod=None,
menuUrlMethod=None):
self.klass = klass
self.attribute = attribute
# May the user add new objects through this ref ?
@ -361,6 +405,29 @@ class Ref(Field):
# If changeOrder is False, it even if the user has the right to modify
# the field, it will not be possible to move objects or sort them.
self.changeOrder = changeOrder
# There are different ways to render a bunch of linked objects:
# - "list" (the default) renders them as a list (=a XHTML table);
# - "menus" renders them as a series of popup menus, grouped by type.
# Note that render mode "menus" will only be applied in "cell" layouts.
# Indeed, we need to keep the "list" rendering in the "view" layout
# because the "menus" rendering is minimalist and does not allow to
# perform all operations on Ref objects (add, move, delete, edit...).
self.render = render
# If render is 'menus', 2 methods must be provided.
# "menuIdMethod" will be called, with every linked object as single arg,
# and must return an ID that identifies the menu into which the object
# will be inserted.
self.menuIdMethod = menuIdMethod
# "menuInfoMethod" will be called with every collected menu ID (from
# calls to the previous method) to get info about this menu. This info
# must be a tuple (text, icon):
# - "text" is the menu name;
# - "icon" (can be None) gives the URL of an icon, if you want to render
# the menu as an icon instead of a text.
self.menuInfoMethod = menuInfoMethod
# "menuUrlMethod" is an optional method that allows to compute an
# alternative URL for the tied object that is shown within the menu.
self.menuUrlMethod = menuUrlMethod
Field.__init__(self, validator, multiplicity, default, show, page,
group, layouts, move, indexed, False,
specificReadPermission, specificWritePermission, width,
@ -448,6 +515,54 @@ class Ref(Field):
return self.getValue(obj, type='zobjects', someObjects=True,
startNumber=startNumber)
def getLinkedObjectsByMenu(self, obj, zobjects):
'''This method groups p_zobjects into sub-lists of objects, grouped by
menu (happens when self.render == 'menus').'''
res = []
# We store in "menuIds" the already encountered menus:
# ~{s_menuId : i_indexInRes}~
menuIds = {}
# Browse every object from p_zobjects and put them in their menu
# (within "res").
for zobj in zobjects:
menuId = self.menuIdMethod(obj, zobj.appy())
if menuId in menuIds:
# We have already encountered this menu.
menuIndex = menuIds[menuId]
res[menuIndex].zobjects.append(zobj)
else:
# A new menu.
menu = Object(id=menuId, zobjects=[zobj])
res.append(menu)
menuIds[menuId] = len(res) - 1
# Complete information about every menu by calling self.menuInfoMethod
for menu in res:
text, icon = self.menuInfoMethod(obj, menu.id)
menu.text = text
menu.icon = icon
return res
def getMenuUrl(self, zobj, ztied):
'''We must provide the URL of the tied object p_ztied, when shown in a
Ref field in render mode 'menus'. If self.menuUrlMethod is specified,
use it. Else, returns the "normal" URL of the view page for the tied
object, but without any navigation information, because in this
render mode, tied object's order is lost and navigation is
impossible.'''
if self.menuUrlMethod:
return self.menuUrlMethod(zobj.appy(), ztied.appy())
return ztied.getUrl(nav='')
def getStartNumber(self, render, req, ajaxHookId):
'''This method returns the index of the first linked object that must be
shown, or None if all linked objects must be shown at once (it
happens when p_render is "menus").'''
# When using 'menus' render mode, all linked objects must be shown.
if render == 'menus': return
# When using 'list' (=default) render mode, the index of the first
# object to show is in the request.
return int(req.get('%s_startNumber' % ajaxHookId, 0))
def getFormattedValue(self, obj, value, showChanges=False):
return value