[gen] Refactoring.

This commit is contained in:
Gaetan Delannay 2013-08-21 13:54:56 +02:00
parent 34e3a3083e
commit 1bd77d68c4
8 changed files with 853 additions and 770 deletions

View file

@ -16,613 +16,24 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import copy, types, re import copy, types, re
from appy import Object
from appy.gen.layout import Table, defaultFieldLayouts from appy.gen.layout import Table, defaultFieldLayouts
from appy.gen import utils as gutils from appy.gen import utils as gutils
from appy.shared import utils as sutils
from appy.px import Px from appy.px import Px
from appy import Object from appy.shared import utils as sutils
from group import Group
from page import Page
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Field:
'''Basic abstract class for defining any field.'''
# Some global static variables
nullValues = (None, '', []) nullValues = (None, '', [])
validatorTypes = (types.FunctionType, types.UnboundMethodType, validatorTypes = (types.FunctionType, types.UnboundMethodType,
type(re.compile(''))) type(re.compile('')))
labelTypes = ('label', 'descr', 'help') labelTypes = ('label', 'descr', 'help')
def initMasterValue(v):
'''Standardizes p_v as a list of strings.'''
if not isinstance(v, bool) and not v: res = []
elif type(v) not in sutils.sequenceTypes: res = [v]
else: res = v
return [str(v) for v in res]
class No:
'''When you write a workflow condition method and you want to return False
but you want to give to the user some explanations about why a transition
can't be triggered, do not return False, return an instance of No
instead. When creating such an instance, you can specify an error
message.'''
def __init__(self, msg):
self.msg = msg
def __nonzero__(self):
return False
# ------------------------------------------------------------------------------
# Page. Every field lives into a Page.
# ------------------------------------------------------------------------------
class Page:
'''Used for describing a page, its related phase, show condition, etc.'''
subElements = ('save', 'cancel', 'previous', 'next', 'edit')
def __init__(self, name, phase='main', show=True, showSave=True,
showCancel=True, showPrevious=True, showNext=True,
showEdit=True):
self.name = name
self.phase = phase
self.show = show
# When editing the page, must I show the "save" button?
self.showSave = showSave
# When editing the page, must I show the "cancel" button?
self.showCancel = showCancel
# When editing the page, and when a previous page exists, must I show
# the "previous" button?
self.showPrevious = showPrevious
# When editing the page, and when a next page exists, must I show the
# "next" button?
self.showNext = showNext
# When viewing the page, must I show the "edit" button?
self.showEdit = showEdit
@staticmethod
def get(pageData):
'''Produces a Page instance from p_pageData. User-defined p_pageData
can be:
(a) a string containing the name of the page;
(b) a string containing <pageName>_<phaseName>;
(c) a Page instance.
This method returns always a Page instance.'''
res = pageData
if res and isinstance(res, basestring):
# Page data is given as a string.
pageElems = pageData.rsplit('_', 1)
if len(pageElems) == 1: # We have case (a)
res = Page(pageData)
else: # We have case (b)
res = Page(pageData[0], phase=pageData[1])
return res
def isShowable(self, obj, layoutType, elem='page'):
'''Must this page be shown for p_obj? "Show value" can be True, False
or 'view' (page is available only in "view" mode).
If p_elem is not "page", this method returns the fact that a
sub-element is viewable or not (buttons "save", "cancel", etc).'''
# Define what attribute to test for "showability".
showAttr = 'show'
if elem != 'page':
showAttr = 'show%s' % elem.capitalize()
# Get the value of the show attribute as identified above.
show = getattr(self, showAttr)
if callable(show):
show = show(obj.appy())
# Show value can be 'view', for example. Thanks to p_layoutType,
# convert show value to a real final boolean value.
res = show
if res == 'view': res = layoutType == 'view'
return res
def getInfo(self, obj, layoutType):
'''Gets information about this page, for p_obj, as an object.'''
res = Object()
for elem in Page.subElements:
setattr(res, 'show%s' % elem.capitalize(), \
self.isShowable(obj, layoutType, elem=elem))
return res
# ------------------------------------------------------------------------------
# Phase. Pages are grouped into phases.
# ------------------------------------------------------------------------------
class Phase:
'''A group of pages.'''
pxView = Px('''
<tr var="singlePage=len(phase.pages) == 1">
<td var="label='%s_phase_%s' % (zobj.meta_type, phase.name)">
<!-- The title of the phase -->
<div class="portletGroup"
if="not singlePhase and not singlePage">::_(label)</div>
<!-- The page(s) within the phase -->
<x for="aPage in phase.pages">
<!-- First line: page name and icons -->
<div if="not (singlePhase and singlePage)"
class=":aPage==page and 'portletCurrent portletPage' or \
'portletPage'">
<a href=":zobj.getUrl(page=aPage)">::_('%s_page_%s' % \
(zobj.meta_type, aPage))</a>
<x var="locked=zobj.isLocked(user, aPage);
editable=mayEdit and phase.pagesInfo[aPage].showOnEdit">
<a if="editable and not locked"
href=":zobj.getUrl(mode='edit', page=aPage)">
<img src=":url('edit')" title=":_('object_edit')"/></a>
<a if="editable and locked">
<img style="cursor: help"
var="lockDate=tool.formatDate(locked[1]);
lockMap={'user':ztool.getUserName(locked[0]), \
'date':lockDate};
lockMsg=_('page_locked', mapping=lockMap)"
src=":url('locked')" title=":lockMsg"/></a>
<a if="editable and locked and user.has_role('Manager')">
<img class="clickable" title=":_('page_unlock')" src=":url('unlock')"
onclick=":'onUnlockPage(%s,%s)' % \
(q(zobj.UID()), q(aPage))"/></a>
</x>
</div>
<!-- Next lines: links -->
<x var="links=phase.pagesInfo[aPage].links" if="links">
<div for="link in links"><a href=":link.url">:link.title</a></div>
</x>
</x>
</td>
</tr>''')
def __init__(self, name, obj):
self.name = name
self.obj = obj
# The list of names of pages in this phase
self.pages = []
# The list of hidden pages in this phase
self.hiddenPages = []
# The dict below stores info about every page listed in self.pages.
self.pagesInfo = {}
self.totalNbOfPhases = None
# The following attributes allows to browse, from a given page, to the
# last page of the previous phase and to the first page of the following
# phase if allowed by phase state.
self.previousPhase = None
self.nextPhase = None
def addPageLinks(self, field, obj):
'''If p_field is a navigable Ref, we must add, within self.pagesInfo,
objects linked to p_obj through this ReF as links.'''
if field.page.name in self.hiddenPages: return
infos = []
for ztied in field.getValue(obj, type='zobjects'):
infos.append(Object(title=ztied.title, url=ztied.absolute_url()))
self.pagesInfo[field.page.name].links = infos
def addPage(self, field, obj, layoutType):
'''Adds page-related information in the phase.'''
# If the page is already there, we have nothing more to do.
if (field.page.name in self.pages) or \
(field.page.name in self.hiddenPages): return
# Add the page only if it must be shown.
isShowableOnView = field.page.isShowable(obj, 'view')
isShowableOnEdit = field.page.isShowable(obj, 'edit')
if isShowableOnView or isShowableOnEdit:
# The page must be added.
self.pages.append(field.page.name)
# Create the dict about page information and add it in self.pageInfo
pageInfo = Object(page=field.page, showOnView=isShowableOnView,
showOnEdit=isShowableOnEdit, links=None)
pageInfo.update(field.page.getInfo(obj, layoutType))
self.pagesInfo[field.page.name] = pageInfo
else:
self.hiddenPages.append(field.page.name)
def computeNextPrevious(self, allPhases):
'''This method also fills fields "previousPhase" and "nextPhase"
if relevant, based on list of p_allPhases.'''
# Identify previous and next phases
for phase in allPhases:
if phase.name == self.name:
i = allPhases.index(phase)
if i > 0:
self.previousPhase = allPhases[i-1]
if i < (len(allPhases)-1):
self.nextPhase = allPhases[i+1]
def getPreviousPage(self, page):
'''Returns the page that precedes p_page in this phase.'''
try:
pageIndex = self.pages.index(page)
except ValueError:
# The current page is probably not visible anymore. Return the
# first available page in current phase.
res = self.pages[0]
return res, self.pagesInfo[res]
if pageIndex > 0:
# We stay on the same phase, previous page
res = self.pages[pageIndex-1]
return res, self.pagesInfo[res]
else:
if self.previousPhase:
# We go to the last page of previous phase
previousPhase = self.previousPhase
res = previousPhase.pages[-1]
return res, previousPhase.pagesInfo[res]
else:
return None, None
def getNextPage(self, page):
'''Returns the page that follows p_page in this phase.'''
try:
pageIndex = self.pages.index(page)
except ValueError:
# The current page is probably not visible anymore. Return the
# first available page in current phase.
res = self.pages[0]
return res, self.pagesInfo[res]
if pageIndex < (len(self.pages)-1):
# We stay on the same phase, next page
res = self.pages[pageIndex+1]
return res, self.pagesInfo[res]
else:
if self.nextPhase:
# We go to the first page of next phase
nextPhase = self.nextPhase
res = nextPhase.pages[0]
return res, nextPhase.pagesInfo[res]
else:
return None, None
# ------------------------------------------------------------------------------
# Group. Fields can be grouped.
# ------------------------------------------------------------------------------
class Group:
'''Used for describing a group of fields within a page.'''
def __init__(self, name, columns=['100%'], wide=True, style='section2',
hasLabel=True, hasDescr=False, hasHelp=False,
hasHeaders=False, group=None, colspan=1, align='center',
valign='top', css_class='', master=None, masterValue=None,
cellpadding=1, cellspacing=1, cellgap='0.6em', label=None,
translated=None):
self.name = name
# In its simpler form, field "columns" below can hold a list or tuple
# of column widths expressed as strings, that will be given as is in
# the "width" attributes of the corresponding "td" tags. Instead of
# strings, within this list or tuple, you may give Column instances
# (see below).
self.columns = columns
self._setColumns()
# If field "wide" below is True, the HTML table corresponding to this
# group will have width 100%. You can also specify some string value,
# which will be used for HTML param "width".
if wide == True:
self.wide = '100%'
elif isinstance(wide, basestring):
self.wide = wide
else:
self.wide = ''
# If style = 'fieldset', all widgets within the group will be rendered
# within an HTML fieldset. If style is 'section1' or 'section2', widgets
# will be rendered after the group title.
self.style = style
# If hasLabel is True, the group will have a name and the corresponding
# i18n label will be generated.
self.hasLabel = hasLabel
# If hasDescr is True, the group will have a description and the
# corresponding i18n label will be generated.
self.hasDescr = hasDescr
# If hasHelp is True, the group will have a help text associated and the
# corresponding i18n label will be generated.
self.hasHelp = hasHelp
# If hasheaders is True, group content will begin with a row of headers,
# and a i18n label will be generated for every header.
self.hasHeaders = hasHeaders
self.nbOfHeaders = len(columns)
# If this group is himself contained in another group, the following
# attribute is filled.
self.group = Group.get(group)
# If the group is rendered into another group, we can specify the number
# of columns that this group will span.
self.colspan = colspan
self.align = align
self.valign = valign
self.cellpadding = cellpadding
self.cellspacing = cellspacing
# Beyond standard cellpadding and cellspacing, cellgap can define an
# additional horizontal gap between cells in a row. So this value does
# not add space before the first cell or after the last one.
self.cellgap = cellgap
if style == 'tabs':
# Group content will be rendered as tabs. In this case, some
# param combinations have no sense.
self.hasLabel = self.hasDescr = self.hasHelp = False
# The rendering is forced to a single column
self.columns = self.columns[:1]
# Header labels will be used as labels for the tabs.
self.hasHeaders = True
self.css_class = css_class
self.master = master
self.masterValue = initMasterValue(masterValue)
if master: master.slaves.append(self)
self.label = label # See similar attr of Type class.
# If a translated name is already given here, we will use it instead of
# trying to translate the group label.
self.translated = translated
def _setColumns(self):
'''Standardizes field "columns" as a list of Column instances. Indeed,
the initial value for field "columns" may be a list or tuple of
Column instances or strings.'''
for i in range(len(self.columns)):
columnData = self.columns[i]
if not isinstance(columnData, Column):
self.columns[i] = Column(self.columns[i])
@staticmethod
def get(groupData):
'''Produces a Group instance from p_groupData. User-defined p_groupData
can be a string or a Group instance; this method returns always a
Group instance.'''
res = groupData
if res and isinstance(res, basestring):
# Group data is given as a string. 2 more possibilities:
# (a) groupData is simply the name of the group;
# (b) groupData is of the form <groupName>_<numberOfColumns>.
groupElems = groupData.rsplit('_', 1)
if len(groupElems) == 1:
res = Group(groupElems[0])
else:
try:
nbOfColumns = int(groupElems[1])
except ValueError:
nbOfColumns = 1
width = 100.0 / nbOfColumns
res = Group(groupElems[0], ['%.2f%%' % width] * nbOfColumns)
return res
def getMasterData(self):
'''Gets the master of this group (and masterValue) or, recursively, of
containing groups when relevant.'''
if self.master: return (self.master, self.masterValue)
if self.group: return self.group.getMasterData()
def generateLabels(self, messages, classDescr, walkedGroups,
forSearch=False):
'''This method allows to generate all the needed i18n labels related to
this group. p_messages is the list of i18n p_messages (a PoMessages
instance) that we are currently building; p_classDescr is the
descriptor of the class where this group is defined. If p_forSearch
is True, this group is used for grouping searches, and not fields.'''
# A part of the group label depends on p_forSearch.
if forSearch: gp = 'searchgroup'
else: gp = 'group'
if self.hasLabel:
msgId = '%s_%s_%s' % (classDescr.name, gp, self.name)
messages.append(msgId, self.name)
if self.hasDescr:
msgId = '%s_%s_%s_descr' % (classDescr.name, gp, self.name)
messages.append(msgId, ' ', nice=False)
if self.hasHelp:
msgId = '%s_%s_%s_help' % (classDescr.name, gp, self.name)
messages.append(msgId, ' ', nice=False)
if self.hasHeaders:
for i in range(self.nbOfHeaders):
msgId = '%s_%s_%s_col%d' % (classDescr.name, gp, self.name, i+1)
messages.append(msgId, ' ', nice=False)
walkedGroups.add(self)
if self.group and (self.group not in walkedGroups) and \
not self.group.label:
# We remember walked groups for avoiding infinite recursion.
self.group.generateLabels(messages, classDescr, walkedGroups,
forSearch=forSearch)
def insertInto(self, fields, uiGroups, page, metaType, forSearch=False):
'''Inserts the UiGroup instance corresponding to this Group instance
into p_fields, the recursive structure used for displaying all
fields in a given p_page (or all searches), and returns this
UiGroup instance.'''
# First, create the corresponding UiGroup if not already in p_uiGroups.
if self.name not in uiGroups:
uiGroup = uiGroups[self.name] = UiGroup(self, page, metaType,
forSearch=forSearch)
# Insert the group at the higher level (ie, directly in p_fields)
# if the group is not itself in a group.
if not self.group:
fields.append(uiGroup)
else:
outerGroup = self.group.insertInto(fields, uiGroups, page,
metaType,forSearch=forSearch)
outerGroup.addField(uiGroup)
else:
uiGroup = uiGroups[self.name]
return uiGroup
class Column:
'''Used for describing a column within a Group like defined above.'''
def __init__(self, width, align="left"):
self.width = width
self.align = align
class UiGroup:
'''On-the-fly-generated data structure that groups all fields sharing the
same appy.fields.Group instance, that some logged user can see.'''
# PX that renders a help icon for a group.
pxHelp = Px('''<acronym title="obj.translate('help', field=field)"><img
src=":url('help')"/></acronym>''')
# PX that renders the content of a group.
pxContent = Px('''
<table var="cellgap=field.cellgap" width=":field.wide"
align=":ztool.flipLanguageDirection(field.align, dir)"
id=":tagId" name=":tagName" class=":groupCss"
cellspacing=":field.cellspacing" cellpadding=":field.cellpadding">
<!-- Display the title of the group if not rendered a fieldset. -->
<tr if="(field.style != 'fieldset') and field.hasLabel">
<td colspan=":len(field.columnsWidths)" class=":field.style"
align=":dleft">
<x>::_(field.labelId)</x><x if="field.hasHelp">:field.pxHelp</x>
</td>
</tr>
<tr if="(field.style != 'fieldset') and field.hasDescr">
<td colspan=":len(field.columnsWidths)"
class="discreet">::_(field.descrId)</td>
</tr>
<!-- The column headers -->
<tr>
<th for="colNb in range(len(field.columnsWidths))"
align="ztool.flipLanguageDirection(field.columnsAligns[colNb], dir)"
width=":field.columnsWidths[colNb]">::field.hasHeaders and \
_('%s_col%d' % (field.labelId, (colNb+1))) or ''</th>
</tr>
<!-- The rows of widgets -->
<tr valign=":field.valign" for="row in field.fields">
<td for="field in row"
colspan="field.colspan"
style=":not loop.field.last and ('padding-right:%s'% cellgap) or ''">
<x if="field">
<x if="field.type == 'group'">:field.pxView</x>
<x if="field.type != 'group'">:field.pxRender</x>
</x>
</td>
</tr>
</table>''')
# PX that renders a group of fields.
pxView = Px('''
<x var="tagCss=field.master and ('slave_%s_%s' % \
(field.masterName, '_'.join(field.masterValue))) or '';
widgetCss=field.css_class;
groupCss=tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss;
tagName=field.master and 'slave' or '';
tagId='%s_%s' % (zobj.UID(), field.name)">
<!-- Render the group as a fieldset if required -->
<fieldset if="field.style == 'fieldset'">
<legend if="field.hasLabel">
<i>::_(field.labelId)></i><x if="field.hasHelp">:field.pxHelp</x>
</legend>
<div if="field.hasDescr" class="discreet">::_(field.descrId)</div>
<x>:field.pxContent</x>
</fieldset>
<!-- Render the group as a section if required -->
<x if="field.style not in ('fieldset', 'tabs')">:field.pxContent</x>
<!-- Render the group as tabs if required -->
<x if="field.style == 'tabs'" var2="lenFields=len(field.fields)">
<table width=":field.wide" class=":groupCss" id=":tagId" name=":tagName">
<!-- First row: the tabs. -->
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
<table style="position:relative; bottom:-2px"
cellpadding="0" cellspacing="0">
<tr valign="bottom">
<x for="row in field.fields"
var2="rowNb=loop.row.nb;
tabId='tab_%s_%d_%d' % (field.name, rowNb, lenFields)">
<td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td>
<td style=":url('tabBg', bg=True)" id=":tabId">
<a onclick=":'showTab(%s)' % q('%s_%d_%d' % (field.name, rowNb, \
lenFields))"
class="clickable">:_('%s_col%d' % (field.labelId, rowNb))</a>
</td>
<td><img id=":'%s_right' % tabId" src=":url('tabRight')"/></td>
</x>
</tr>
</table>
</td></tr>
<!-- Other rows: the fields -->
<tr for="row in field.fields"
id=":'tabcontent_%s_%d_%d' % (field.name, loop.row.nb, lenFields)"
style=":loop.row.nb==0 and 'display:table-row' or 'display:none')">
<td var="field=row[0]">
<x if="field.type == 'group'">:field.pxView</x>
<x if="field.type != 'group'">:field.pxRender</x>
</td>
</tr>
</table>
<script type="text/javascript">:'initTab(%s,%s)' % \
(q('tab_%s' % field.name), q('%s_1_%d' % (field.name, lenFields)))">
</script>
</x>
</x>''')
# PX that renders a group of searches.
pxViewSearches = Px('''
<x var="expanded=req.get(field.labelId, 'collapsed') == 'expanded'">
<!-- Group name, prefixed by the expand/collapse icon -->
<div class="portletGroup">
<img class="clickable" style="margin-right: 3px" align=":dleft"
id=":'%s_img' % field.labelId"
src=":expanded and url('collapse.gif') or url('expand.gif')"
onclick=":'toggleCookie(%s)' % q(field.labelId)"/>
<x if="not field.translated">:_(field.labelId)</x>
<x if="field.translated">:field.translated</x>
</div>
<!-- Group content -->
<div var="display=expanded and 'display:block' or 'display:none'"
id=":field.labelId" style=":'padding-left: 10px; %s' % display">
<x for="searches in field.widgets">
<x for="elem in searches">
<!-- An inner group within this group -->
<x if="elem.type == 'group'"
var2="field=elem">:field.pxViewSearches</x>
<!-- A search -->
<x if="elem.type != 'group'" var2="search=elem">:search.pxView</x>
</x>
</x>
</div>
</x>''')
def __init__(self, group, page, metaType, forSearch=False):
self.type = 'group'
# All p_group attributes become self attributes.
for name, value in group.__dict__.iteritems():
if not name.startswith('_'):
setattr(self, name, value)
self.columnsWidths = [col.width for col in group.columns]
self.columnsAligns = [col.align for col in group.columns]
# Names of i18n labels
labelName = self.name
prefix = metaType
if group.label:
if isinstance(group.label, basestring): prefix = group.label
else: # It is a tuple (metaType, name)
if group.label[1]: labelName = group.label[1]
if group.label[0]: prefix = group.label[0]
if forSearch: gp = 'searchgroup'
else: gp = 'group'
self.labelId = '%s_%s_%s' % (prefix, gp, labelName)
self.descrId = self.labelId + '_descr'
self.helpId = self.labelId + '_help'
# The name of the page where the group lies
self.page = page.name
# The fields belonging to the group that the current user may see.
# They will be stored by m_addField below as a list of lists because
# they will be rendered as a table.
self.fields = [[]]
# PX to user for rendering this group.
self.px = forSearch and self.pxViewSearches or self.pxView
def addField(self, field):
'''Adds p_field into self.fields. We try first to add p_field into the
last row. If it is not possible, we create a new row.'''
# Get the last row
lastRow = self.fields[-1]
numberOfColumns = len(self.columnsWidths)
# Compute the number of columns already filled in the last row.
filledColumns = 0
for rowField in lastRow: filledColumns += rowField.colspan
freeColumns = numberOfColumns - filledColumns
if freeColumns >= field.colspan:
# We can add the widget in the last row.
lastRow.append(field)
else:
if freeColumns:
# Terminate the current row by appending empty cells
for i in range(freeColumns): lastRow.append('')
# Create a new row
self.fields.append([field])
# ------------------------------------------------------------------------------
# Abstract base class for every field.
# ------------------------------------------------------------------------------
class Field:
'''Basic abstract class for defining any field.'''
# Those attributes can be overridden by subclasses for defining, # Those attributes can be overridden by subclasses for defining,
# respectively, names of CSS and Javascript files that are required by this # respectively, names of CSS and Javascript files that are required by this
# field, keyed by layoutType. # field, keyed by layoutType.
@ -759,7 +170,7 @@ class Field:
self.master = master self.master = master
if master: self.master.slaves.append(self) if master: self.master.slaves.append(self)
# When master has some value(s), there is impact on this field. # When master has some value(s), there is impact on this field.
self.masterValue = initMasterValue(masterValue) self.masterValue = gutils.initMasterValue(masterValue)
# If a field must retain attention in a particular way, set focus=True. # If a field must retain attention in a particular way, set focus=True.
# It will be rendered in a special way. # It will be rendered in a special way.
self.focus = focus self.focus = focus
@ -937,12 +348,12 @@ class Field:
# Is it a dict like {'label':..., 'descr':...}, or is it directly a # Is it a dict like {'label':..., 'descr':...}, or is it directly a
# dict with a mapping? # dict with a mapping?
for k, v in mapping.iteritems(): for k, v in mapping.iteritems():
if (k not in labelTypes) or isinstance(v, basestring): if (k not in self.labelTypes) or isinstance(v, basestring):
# It is already a mapping # It is already a mapping
return {'label':mapping, 'descr':mapping, 'help':mapping} return {'label':mapping, 'descr':mapping, 'help':mapping}
# If we are here, we have {'label':..., 'descr':...}. Complete # If we are here, we have {'label':..., 'descr':...}. Complete
# it if necessary. # it if necessary.
for labelType in labelTypes: for labelType in self.labelTypes:
if labelType not in mapping: if labelType not in mapping:
mapping[labelType] = None # No mapping for this value. mapping[labelType] = None # No mapping for this value.
return mapping return mapping
@ -1159,7 +570,7 @@ class Field:
def isEmptyValue(self, value, obj=None): def isEmptyValue(self, value, obj=None):
'''Returns True if the p_value must be considered as an empty value.''' '''Returns True if the p_value must be considered as an empty value.'''
return value in nullValues return value in self.nullValues
def validateValue(self, obj, value): def validateValue(self, obj, value):
'''This method may be overridden by child classes and will be called at '''This method may be overridden by child classes and will be called at
@ -1198,9 +609,9 @@ class Field:
if message: return message if message: return message
# Evaluate the custom validator if one has been specified # Evaluate the custom validator if one has been specified
value = self.getStorableValue(value) value = self.getStorableValue(value)
if self.validator and (type(self.validator) in validatorTypes): if self.validator and (type(self.validator) in self.validatorTypes):
obj = obj.appy() obj = obj.appy()
if type(self.validator) != validatorTypes[-1]: if type(self.validator) != self.validatorTypes[-1]:
# It is a custom function. Execute it. # It is a custom function. Execute it.
try: try:
validValue = self.validator(obj, value) validValue = self.validator(obj, value)

370
fields/group.py Normal file
View file

@ -0,0 +1,370 @@
# ------------------------------------------------------------------------------
# This file is part of Appy, a framework for building applications in the Python
# language. Copyright (C) 2007 Gaetan Delannay
# Appy is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with
# Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
from appy.px import Px
from appy.gen import utils as gutils
# ------------------------------------------------------------------------------
class Group:
'''Used for describing a group of fields within a page.'''
def __init__(self, name, columns=['100%'], wide=True, style='section2',
hasLabel=True, hasDescr=False, hasHelp=False,
hasHeaders=False, group=None, colspan=1, align='center',
valign='top', css_class='', master=None, masterValue=None,
cellpadding=1, cellspacing=1, cellgap='0.6em', label=None,
translated=None):
self.name = name
# In its simpler form, field "columns" below can hold a list or tuple
# of column widths expressed as strings, that will be given as is in
# the "width" attributes of the corresponding "td" tags. Instead of
# strings, within this list or tuple, you may give Column instances
# (see below).
self.columns = columns
self._setColumns()
# If field "wide" below is True, the HTML table corresponding to this
# group will have width 100%. You can also specify some string value,
# which will be used for HTML param "width".
if wide == True:
self.wide = '100%'
elif isinstance(wide, basestring):
self.wide = wide
else:
self.wide = ''
# If style = 'fieldset', all widgets within the group will be rendered
# within an HTML fieldset. If style is 'section1' or 'section2', widgets
# will be rendered after the group title.
self.style = style
# If hasLabel is True, the group will have a name and the corresponding
# i18n label will be generated.
self.hasLabel = hasLabel
# If hasDescr is True, the group will have a description and the
# corresponding i18n label will be generated.
self.hasDescr = hasDescr
# If hasHelp is True, the group will have a help text associated and the
# corresponding i18n label will be generated.
self.hasHelp = hasHelp
# If hasheaders is True, group content will begin with a row of headers,
# and a i18n label will be generated for every header.
self.hasHeaders = hasHeaders
self.nbOfHeaders = len(columns)
# If this group is himself contained in another group, the following
# attribute is filled.
self.group = Group.get(group)
# If the group is rendered into another group, we can specify the number
# of columns that this group will span.
self.colspan = colspan
self.align = align
self.valign = valign
self.cellpadding = cellpadding
self.cellspacing = cellspacing
# Beyond standard cellpadding and cellspacing, cellgap can define an
# additional horizontal gap between cells in a row. So this value does
# not add space before the first cell or after the last one.
self.cellgap = cellgap
if style == 'tabs':
# Group content will be rendered as tabs. In this case, some
# param combinations have no sense.
self.hasLabel = self.hasDescr = self.hasHelp = False
# The rendering is forced to a single column
self.columns = self.columns[:1]
# Header labels will be used as labels for the tabs.
self.hasHeaders = True
self.css_class = css_class
self.master = master
self.masterValue = gutils.initMasterValue(masterValue)
if master: master.slaves.append(self)
self.label = label # See similar attr of Type class.
# If a translated name is already given here, we will use it instead of
# trying to translate the group label.
self.translated = translated
def _setColumns(self):
'''Standardizes field "columns" as a list of Column instances. Indeed,
the initial value for field "columns" may be a list or tuple of
Column instances or strings.'''
for i in range(len(self.columns)):
columnData = self.columns[i]
if not isinstance(columnData, Column):
self.columns[i] = Column(self.columns[i])
@staticmethod
def get(groupData):
'''Produces a Group instance from p_groupData. User-defined p_groupData
can be a string or a Group instance; this method returns always a
Group instance.'''
res = groupData
if res and isinstance(res, basestring):
# Group data is given as a string. 2 more possibilities:
# (a) groupData is simply the name of the group;
# (b) groupData is of the form <groupName>_<numberOfColumns>.
groupElems = groupData.rsplit('_', 1)
if len(groupElems) == 1:
res = Group(groupElems[0])
else:
try:
nbOfColumns = int(groupElems[1])
except ValueError:
nbOfColumns = 1
width = 100.0 / nbOfColumns
res = Group(groupElems[0], ['%.2f%%' % width] * nbOfColumns)
return res
def getMasterData(self):
'''Gets the master of this group (and masterValue) or, recursively, of
containing groups when relevant.'''
if self.master: return (self.master, self.masterValue)
if self.group: return self.group.getMasterData()
def generateLabels(self, messages, classDescr, walkedGroups,
forSearch=False):
'''This method allows to generate all the needed i18n labels related to
this group. p_messages is the list of i18n p_messages (a PoMessages
instance) that we are currently building; p_classDescr is the
descriptor of the class where this group is defined. If p_forSearch
is True, this group is used for grouping searches, and not fields.'''
# A part of the group label depends on p_forSearch.
if forSearch: gp = 'searchgroup'
else: gp = 'group'
if self.hasLabel:
msgId = '%s_%s_%s' % (classDescr.name, gp, self.name)
messages.append(msgId, self.name)
if self.hasDescr:
msgId = '%s_%s_%s_descr' % (classDescr.name, gp, self.name)
messages.append(msgId, ' ', nice=False)
if self.hasHelp:
msgId = '%s_%s_%s_help' % (classDescr.name, gp, self.name)
messages.append(msgId, ' ', nice=False)
if self.hasHeaders:
for i in range(self.nbOfHeaders):
msgId = '%s_%s_%s_col%d' % (classDescr.name, gp, self.name, i+1)
messages.append(msgId, ' ', nice=False)
walkedGroups.add(self)
if self.group and (self.group not in walkedGroups) and \
not self.group.label:
# We remember walked groups for avoiding infinite recursion.
self.group.generateLabels(messages, classDescr, walkedGroups,
forSearch=forSearch)
def insertInto(self, fields, uiGroups, page, metaType, forSearch=False):
'''Inserts the UiGroup instance corresponding to this Group instance
into p_fields, the recursive structure used for displaying all
fields in a given p_page (or all searches), and returns this
UiGroup instance.'''
# First, create the corresponding UiGroup if not already in p_uiGroups.
if self.name not in uiGroups:
uiGroup = uiGroups[self.name] = UiGroup(self, page, metaType,
forSearch=forSearch)
# Insert the group at the higher level (ie, directly in p_fields)
# if the group is not itself in a group.
if not self.group:
fields.append(uiGroup)
else:
outerGroup = self.group.insertInto(fields, uiGroups, page,
metaType,forSearch=forSearch)
outerGroup.addField(uiGroup)
else:
uiGroup = uiGroups[self.name]
return uiGroup
class Column:
'''Used for describing a column within a Group like defined above.'''
def __init__(self, width, align="left"):
self.width = width
self.align = align
class UiGroup:
'''On-the-fly-generated data structure that groups all fields sharing the
same appy.fields.Group instance, that some logged user can see.'''
# PX that renders a help icon for a group.
pxHelp = Px('''<acronym title="obj.translate('help', field=field)"><img
src=":url('help')"/></acronym>''')
# PX that renders the content of a group.
pxContent = Px('''
<table var="cellgap=field.cellgap" width=":field.wide"
align=":ztool.flipLanguageDirection(field.align, dir)"
id=":tagId" name=":tagName" class=":groupCss"
cellspacing=":field.cellspacing" cellpadding=":field.cellpadding">
<!-- Display the title of the group if not rendered a fieldset. -->
<tr if="(field.style != 'fieldset') and field.hasLabel">
<td colspan=":len(field.columnsWidths)" class=":field.style"
align=":dleft">
<x>::_(field.labelId)</x><x if="field.hasHelp">:field.pxHelp</x>
</td>
</tr>
<tr if="(field.style != 'fieldset') and field.hasDescr">
<td colspan=":len(field.columnsWidths)"
class="discreet">::_(field.descrId)</td>
</tr>
<!-- The column headers -->
<tr>
<th for="colNb in range(len(field.columnsWidths))"
align="ztool.flipLanguageDirection(field.columnsAligns[colNb], dir)"
width=":field.columnsWidths[colNb]">::field.hasHeaders and \
_('%s_col%d' % (field.labelId, (colNb+1))) or ''</th>
</tr>
<!-- The rows of widgets -->
<tr valign=":field.valign" for="row in field.fields">
<td for="field in row"
colspan="field.colspan"
style=":not loop.field.last and ('padding-right:%s'% cellgap) or ''">
<x if="field">
<x if="field.type == 'group'">:field.pxView</x>
<x if="field.type != 'group'">:field.pxRender</x>
</x>
</td>
</tr>
</table>''')
# PX that renders a group of fields.
pxView = Px('''
<x var="tagCss=field.master and ('slave_%s_%s' % \
(field.masterName, '_'.join(field.masterValue))) or '';
widgetCss=field.css_class;
groupCss=tagCss and ('%s %s' % (tagCss, widgetCss)) or widgetCss;
tagName=field.master and 'slave' or '';
tagId='%s_%s' % (zobj.UID(), field.name)">
<!-- Render the group as a fieldset if required -->
<fieldset if="field.style == 'fieldset'">
<legend if="field.hasLabel">
<i>::_(field.labelId)></i><x if="field.hasHelp">:field.pxHelp</x>
</legend>
<div if="field.hasDescr" class="discreet">::_(field.descrId)</div>
<x>:field.pxContent</x>
</fieldset>
<!-- Render the group as a section if required -->
<x if="field.style not in ('fieldset', 'tabs')">:field.pxContent</x>
<!-- Render the group as tabs if required -->
<x if="field.style == 'tabs'" var2="lenFields=len(field.fields)">
<table width=":field.wide" class=":groupCss" id=":tagId" name=":tagName">
<!-- First row: the tabs. -->
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
<table style="position:relative; bottom:-2px"
cellpadding="0" cellspacing="0">
<tr valign="bottom">
<x for="row in field.fields"
var2="rowNb=loop.row.nb;
tabId='tab_%s_%d_%d' % (field.name, rowNb, lenFields)">
<td><img src=":url('tabLeft')" id=":'%s_left' % tabId"/></td>
<td style=":url('tabBg', bg=True)" id=":tabId">
<a onclick=":'showTab(%s)' % q('%s_%d_%d' % (field.name, rowNb, \
lenFields))"
class="clickable">:_('%s_col%d' % (field.labelId, rowNb))</a>
</td>
<td><img id=":'%s_right' % tabId" src=":url('tabRight')"/></td>
</x>
</tr>
</table>
</td></tr>
<!-- Other rows: the fields -->
<tr for="row in field.fields"
id=":'tabcontent_%s_%d_%d' % (field.name, loop.row.nb, lenFields)"
style=":loop.row.nb==0 and 'display:table-row' or 'display:none')">
<td var="field=row[0]">
<x if="field.type == 'group'">:field.pxView</x>
<x if="field.type != 'group'">:field.pxRender</x>
</td>
</tr>
</table>
<script type="text/javascript">:'initTab(%s,%s)' % \
(q('tab_%s' % field.name), q('%s_1_%d' % (field.name, lenFields)))">
</script>
</x>
</x>''')
# PX that renders a group of searches.
pxViewSearches = Px('''
<x var="expanded=req.get(field.labelId, 'collapsed') == 'expanded'">
<!-- Group name, prefixed by the expand/collapse icon -->
<div class="portletGroup">
<img class="clickable" style="margin-right: 3px" align=":dleft"
id=":'%s_img' % field.labelId"
src=":expanded and url('collapse.gif') or url('expand.gif')"
onclick=":'toggleCookie(%s)' % q(field.labelId)"/>
<x if="not field.translated">:_(field.labelId)</x>
<x if="field.translated">:field.translated</x>
</div>
<!-- Group content -->
<div var="display=expanded and 'display:block' or 'display:none'"
id=":field.labelId" style=":'padding-left: 10px; %s' % display">
<x for="searches in field.widgets">
<x for="elem in searches">
<!-- An inner group within this group -->
<x if="elem.type == 'group'"
var2="field=elem">:field.pxViewSearches</x>
<!-- A search -->
<x if="elem.type != 'group'" var2="search=elem">:search.pxView</x>
</x>
</x>
</div>
</x>''')
def __init__(self, group, page, metaType, forSearch=False):
self.type = 'group'
# All p_group attributes become self attributes.
for name, value in group.__dict__.iteritems():
if not name.startswith('_'):
setattr(self, name, value)
self.columnsWidths = [col.width for col in group.columns]
self.columnsAligns = [col.align for col in group.columns]
# Names of i18n labels
labelName = self.name
prefix = metaType
if group.label:
if isinstance(group.label, basestring): prefix = group.label
else: # It is a tuple (metaType, name)
if group.label[1]: labelName = group.label[1]
if group.label[0]: prefix = group.label[0]
if forSearch: gp = 'searchgroup'
else: gp = 'group'
self.labelId = '%s_%s_%s' % (prefix, gp, labelName)
self.descrId = self.labelId + '_descr'
self.helpId = self.labelId + '_help'
# The name of the page where the group lies
self.page = page.name
# The fields belonging to the group that the current user may see.
# They will be stored by m_addField below as a list of lists because
# they will be rendered as a table.
self.fields = [[]]
# PX to user for rendering this group.
self.px = forSearch and self.pxViewSearches or self.pxView
def addField(self, field):
'''Adds p_field into self.fields. We try first to add p_field into the
last row. If it is not possible, we create a new row.'''
# Get the last row
lastRow = self.fields[-1]
numberOfColumns = len(self.columnsWidths)
# Compute the number of columns already filled in the last row.
filledColumns = 0
for rowField in lastRow: filledColumns += rowField.colspan
freeColumns = numberOfColumns - filledColumns
if freeColumns >= field.colspan:
# We can add the widget in the last row.
lastRow.append(field)
else:
if freeColumns:
# Terminate the current row by appending empty cells
for i in range(freeColumns): lastRow.append('')
# Create a new row
self.fields.append([field])
# ------------------------------------------------------------------------------

88
fields/page.py Normal file
View file

@ -0,0 +1,88 @@
# ------------------------------------------------------------------------------
# This file is part of Appy, a framework for building applications in the Python
# language. Copyright (C) 2007 Gaetan Delannay
# Appy is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with
# Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
from appy import Object
# ------------------------------------------------------------------------------
class Page:
'''Used for describing a page, its related phase, show condition, etc.'''
subElements = ('save', 'cancel', 'previous', 'next', 'edit')
def __init__(self, name, phase='main', show=True, showSave=True,
showCancel=True, showPrevious=True, showNext=True,
showEdit=True):
self.name = name
self.phase = phase
self.show = show
# When editing the page, must I show the "save" button?
self.showSave = showSave
# When editing the page, must I show the "cancel" button?
self.showCancel = showCancel
# When editing the page, and when a previous page exists, must I show
# the "previous" button?
self.showPrevious = showPrevious
# When editing the page, and when a next page exists, must I show the
# "next" button?
self.showNext = showNext
# When viewing the page, must I show the "edit" button?
self.showEdit = showEdit
@staticmethod
def get(pageData):
'''Produces a Page instance from p_pageData. User-defined p_pageData
can be:
(a) a string containing the name of the page;
(b) a string containing <pageName>_<phaseName>;
(c) a Page instance.
This method returns always a Page instance.'''
res = pageData
if res and isinstance(res, basestring):
# Page data is given as a string.
pageElems = pageData.rsplit('_', 1)
if len(pageElems) == 1: # We have case (a)
res = Page(pageData)
else: # We have case (b)
res = Page(pageData[0], phase=pageData[1])
return res
def isShowable(self, obj, layoutType, elem='page'):
'''Must this page be shown for p_obj? "Show value" can be True, False
or 'view' (page is available only in "view" mode).
If p_elem is not "page", this method returns the fact that a
sub-element is viewable or not (buttons "save", "cancel", etc).'''
# Define what attribute to test for "showability".
showAttr = 'show'
if elem != 'page':
showAttr = 'show%s' % elem.capitalize()
# Get the value of the show attribute as identified above.
show = getattr(self, showAttr)
if callable(show):
show = show(obj.appy())
# Show value can be 'view', for example. Thanks to p_layoutType,
# convert show value to a real final boolean value.
res = show
if res == 'view': res = layoutType == 'view'
return res
def getInfo(self, obj, layoutType):
'''Gets information about this page, for p_obj, as an object.'''
res = Object()
for elem in Page.subElements:
setattr(res, 'show%s' % elem.capitalize(), \
self.isShowable(obj, layoutType, elem=elem))
return res
# ------------------------------------------------------------------------------

166
fields/phase.py Normal file
View file

@ -0,0 +1,166 @@
# ------------------------------------------------------------------------------
# This file is part of Appy, a framework for building applications in the Python
# language. Copyright (C) 2007 Gaetan Delannay
# Appy is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with
# Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
from appy import Object
from appy.px import Px
# ------------------------------------------------------------------------------
class Phase:
'''A group of pages.'''
pxView = Px('''
<tr var="singlePage=len(phase.pages) == 1">
<td var="label='%s_phase_%s' % (zobj.meta_type, phase.name)">
<!-- The title of the phase -->
<div class="portletGroup"
if="not singlePhase and not singlePage">::_(label)</div>
<!-- The page(s) within the phase -->
<x for="aPage in phase.pages">
<!-- First line: page name and icons -->
<div if="not (singlePhase and singlePage)"
class=":aPage==page and 'portletCurrent portletPage' or \
'portletPage'">
<a href=":zobj.getUrl(page=aPage)">::_('%s_page_%s' % \
(zobj.meta_type, aPage))</a>
<x var="locked=zobj.isLocked(user, aPage);
editable=mayEdit and phase.pagesInfo[aPage].showOnEdit">
<a if="editable and not locked"
href=":zobj.getUrl(mode='edit', page=aPage)">
<img src=":url('edit')" title=":_('object_edit')"/></a>
<a if="editable and locked">
<img style="cursor: help"
var="lockDate=tool.formatDate(locked[1]);
lockMap={'user':ztool.getUserName(locked[0]), \
'date':lockDate};
lockMsg=_('page_locked', mapping=lockMap)"
src=":url('locked')" title=":lockMsg"/></a>
<a if="editable and locked and user.has_role('Manager')">
<img class="clickable" title=":_('page_unlock')" src=":url('unlock')"
onclick=":'onUnlockPage(%s,%s)' % \
(q(zobj.UID()), q(aPage))"/></a>
</x>
</div>
<!-- Next lines: links -->
<x var="links=phase.pagesInfo[aPage].links" if="links">
<div for="link in links"><a href=":link.url">:link.title</a></div>
</x>
</x>
</td>
</tr>''')
def __init__(self, name, obj):
self.name = name
self.obj = obj
# The list of names of pages in this phase
self.pages = []
# The list of hidden pages in this phase
self.hiddenPages = []
# The dict below stores info about every page listed in self.pages.
self.pagesInfo = {}
self.totalNbOfPhases = None
# The following attributes allows to browse, from a given page, to the
# last page of the previous phase and to the first page of the following
# phase if allowed by phase state.
self.previousPhase = None
self.nextPhase = None
def addPageLinks(self, field, obj):
'''If p_field is a navigable Ref, we must add, within self.pagesInfo,
objects linked to p_obj through this ReF as links.'''
if field.page.name in self.hiddenPages: return
infos = []
for ztied in field.getValue(obj, type='zobjects'):
infos.append(Object(title=ztied.title, url=ztied.absolute_url()))
self.pagesInfo[field.page.name].links = infos
def addPage(self, field, obj, layoutType):
'''Adds page-related information in the phase.'''
# If the page is already there, we have nothing more to do.
if (field.page.name in self.pages) or \
(field.page.name in self.hiddenPages): return
# Add the page only if it must be shown.
isShowableOnView = field.page.isShowable(obj, 'view')
isShowableOnEdit = field.page.isShowable(obj, 'edit')
if isShowableOnView or isShowableOnEdit:
# The page must be added.
self.pages.append(field.page.name)
# Create the dict about page information and add it in self.pageInfo
pageInfo = Object(page=field.page, showOnView=isShowableOnView,
showOnEdit=isShowableOnEdit, links=None)
pageInfo.update(field.page.getInfo(obj, layoutType))
self.pagesInfo[field.page.name] = pageInfo
else:
self.hiddenPages.append(field.page.name)
def computeNextPrevious(self, allPhases):
'''This method also fills fields "previousPhase" and "nextPhase"
if relevant, based on list of p_allPhases.'''
# Identify previous and next phases
for phase in allPhases:
if phase.name == self.name:
i = allPhases.index(phase)
if i > 0:
self.previousPhase = allPhases[i-1]
if i < (len(allPhases)-1):
self.nextPhase = allPhases[i+1]
def getPreviousPage(self, page):
'''Returns the page that precedes p_page in this phase.'''
try:
pageIndex = self.pages.index(page)
except ValueError:
# The current page is probably not visible anymore. Return the
# first available page in current phase.
res = self.pages[0]
return res, self.pagesInfo[res]
if pageIndex > 0:
# We stay on the same phase, previous page
res = self.pages[pageIndex-1]
return res, self.pagesInfo[res]
else:
if self.previousPhase:
# We go to the last page of previous phase
previousPhase = self.previousPhase
res = previousPhase.pages[-1]
return res, previousPhase.pagesInfo[res]
else:
return None, None
def getNextPage(self, page):
'''Returns the page that follows p_page in this phase.'''
try:
pageIndex = self.pages.index(page)
except ValueError:
# The current page is probably not visible anymore. Return the
# first available page in current phase.
res = self.pages[0]
return res, self.pagesInfo[res]
if pageIndex < (len(self.pages)-1):
# We stay on the same phase, next page
res = self.pages[pageIndex+1]
return res, self.pagesInfo[res]
else:
if self.nextPhase:
# We go to the first page of next phase
nextPhase = self.nextPhase
res = nextPhase.pages[0]
return res, nextPhase.pagesInfo[res]
else:
return None, None
# ------------------------------------------------------------------------------

View file

@ -16,7 +16,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import sys, re import sys, re
from appy.fields import Field, No from appy.fields import Field
from appy.px import Px from appy.px import Px
from appy.gen.layout import Table from appy.gen.layout import Table
from appy.gen import utils as gutils from appy.gen import utils as gutils
@ -586,20 +586,21 @@ class Ref(Field):
add = self.callMethod(obj, self.add) add = self.callMethod(obj, self.add)
else: else:
add = self.add add = self.add
if not add: return No('no_add') if not add: return gutils.No('no_add')
# Have we reached the maximum number of referred elements? # Have we reached the maximum number of referred elements?
if self.multiplicity[1] != None: if self.multiplicity[1] != None:
refCount = len(getattr(obj, self.name, ())) refCount = len(getattr(obj, self.name, ()))
if refCount >= self.multiplicity[1]: return No('max_reached') if refCount >= self.multiplicity[1]: return gutils.No('max_reached')
# May the user edit this Ref field? # May the user edit this Ref field?
if not obj.allows(self.writePermission): return No('no_write_perm') if not obj.allows(self.writePermission):
return gutils.No('no_write_perm')
# Have the user the correct add permission? # Have the user the correct add permission?
tool = obj.getTool() tool = obj.getTool()
addPermission = '%s: Add %s' % (tool.getAppName(), addPermission = '%s: Add %s' % (tool.getAppName(),
tool.getPortalType(self.klass)) tool.getPortalType(self.klass))
folder = obj.getCreateFolder() folder = obj.getCreateFolder()
if not tool.getUser().has_permission(addPermission, folder): if not tool.getUser().has_permission(addPermission, folder):
return No('no_add_perm') return gutils.No('no_add_perm')
return True return True
def checkAdd(self, obj): def checkAdd(self, obj):

178
fields/search.py Normal file
View file

@ -0,0 +1,178 @@
# ------------------------------------------------------------------------------
# This file is part of Appy, a framework for building applications in the Python
# language. Copyright (C) 2007 Gaetan Delannay
# Appy is free software; you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
# Appy is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License along with
# Appy. If not, see <http://www.gnu.org/licenses/>.
# ------------------------------------------------------------------------------
from appy.px import Px
from appy.gen import utils as gutils
from appy.gen.indexer import defaultIndexes
from appy.shared import utils as sutils
from group import Group
# ------------------------------------------------------------------------------
class Search:
'''Used for specifying a search for a given class.'''
def __init__(self, name, group=None, sortBy='', sortOrder='asc', limit=None,
default=False, colspan=1, translated=None, show=True,
translatedDescr=None, **fields):
self.name = name
# Searches may be visually grouped in the portlet.
self.group = Group.get(group)
self.sortBy = sortBy
self.sortOrder = sortOrder
self.limit = limit
# If this search is the default one, it will be triggered by clicking
# on main link.
self.default = default
self.colspan = colspan
# If a translated name or description is already given here, we will
# use it instead of trying to translate from labels.
self.translated = translated
self.translatedDescr = translatedDescr
# Condition for showing or not this search
self.show = show
# In the dict below, keys are indexed field names or names of standard
# indexes, and values are search values.
self.fields = fields
@staticmethod
def getIndexName(fieldName, usage='search'):
'''Gets the name of the technical index that corresponds to field named
p_fieldName. Indexes can be used for searching (p_usage="search") or
for sorting (usage="sort"). The method returns None if the field
named p_fieldName can't be used for p_usage.'''
if fieldName == 'title':
if usage == 'search': return 'Title'
else: return 'SortableTitle'
# Indeed, for field 'title', Appy has a specific index
# 'SortableTitle', because index 'Title' is a TextIndex
# (for searchability) and can't be used for sorting.
elif fieldName == 'state': return 'State'
elif fieldName == 'created': return 'Created'
elif fieldName == 'modified': return 'Modified'
elif fieldName in defaultIndexes: return fieldName
else:
return 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
@staticmethod
def getSearchValue(fieldName, fieldValue, klass):
'''Returns a transformed p_fieldValue for producing a valid search
value as required for searching in the index corresponding to
p_fieldName.'''
field = getattr(klass, fieldName, None)
if (field and (field.getIndexType() == 'TextIndex')) or \
(fieldName == 'SearchableText'):
# For TextIndex indexes. We must split p_fieldValue into keywords.
res = gutils.Keywords(fieldValue).get()
elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'):
v = fieldValue[:-1]
# Warning: 'z' is higher than 'Z'!
res = {'query':(v,v+'z'), 'range':'min:max'}
elif type(fieldValue) in sutils.sequenceTypes:
if fieldValue and isinstance(fieldValue[0], basestring):
# We have a list of string values (ie: we need to
# search v1 or v2 or...)
res = fieldValue
else:
# We have a range of (int, float, DateTime...) values
minv, maxv = fieldValue
rangev = 'minmax'
queryv = fieldValue
if minv == None:
rangev = 'max'
queryv = maxv
elif maxv == None:
rangev = 'min'
queryv = minv
res = {'query':queryv, 'range':rangev}
else:
res = fieldValue
return res
def updateSearchCriteria(self, criteria, klass, advanced=False):
'''This method updates dict p_criteria with all the search criteria
corresponding to this Search instance. If p_advanced is True,
p_criteria correspond to an advanced search, to be stored in the
session: in this case we need to keep the Appy names for parameters
sortBy and sortOrder (and not "resolve" them to Zope's sort_on and
sort_order).'''
# Put search criteria in p_criteria
for fieldName, fieldValue in self.fields.iteritems():
# Management of searches restricted to objects linked through a
# Ref field: not implemented yet.
if fieldName == '_ref': continue
# Make the correspondence between the name of the field and the
# name of the corresponding index, excepted if advanced is True: in
# that case, the correspondence will be done later.
if not advanced:
attrName = Search.getIndexName(fieldName)
# Express the field value in the way needed by the index
criteria[attrName] = Search.getSearchValue(fieldName,
fieldValue, klass)
else:
criteria[fieldName]= fieldValue
# Add a sort order if specified
if self.sortBy:
if not advanced:
criteria['sort_on'] = Search.getIndexName(self.sortBy,
usage='sort')
if self.sortOrder == 'desc': criteria['sort_order'] = 'reverse'
else: criteria['sort_order'] = None
else:
criteria['sortBy'] = self.sortBy
criteria['sortOrder'] = self.sortOrder
def isShowable(self, klass, tool):
'''Is this Search instance (defined in p_klass) showable?'''
if self.show.__class__.__name__ == 'staticmethod':
return gutils.callMethod(tool, self.show, klass=klass)
return self.show
class UiSearch:
'''Instances of this class are generated on-the-fly for manipulating a
Search from the User Interface.'''
# PX for rendering a search.
pxView = Px('''
<div class="portletSearch">
<a href=":'%s?className=%s&amp;search=%s' % \
(queryUrl, rootClass, search['name'])"
class=":search['name'] == currentSearch and 'portletCurrent' or ''"
title=":search['translatedDescr']">:search['translated']</a>
</div>''')
def __init__(self, search, className, tool):
self.search = search
self.name = search.name
self.type = 'search'
self.colspan = search.colspan
if search.translated:
self.translated = search.translated
self.translatedDescr = search.translatedDescr
else:
# The label may be specific in some special cases.
labelDescr = ''
if search.name == 'allSearch':
label = '%s_plural' % className
elif search.name == 'customSearch':
label = 'search_results'
else:
label = '%s_search_%s' % (className, search.name)
labelDescr = label + '_descr'
self.translated = tool.translate(label)
if labelDescr:
self.translatedDescr = tool.translate(labelDescr)
else:
self.translatedDescr = ''
# ------------------------------------------------------------------------------

View file

@ -1,15 +1,13 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import types, string import types, string
from appy.gen.mail import sendNotification from appy.gen.mail import sendNotification
from appy.gen.indexer import defaultIndexes
from appy.gen import utils as gutils from appy.gen import utils as gutils
from appy.shared import utils as sutils
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
# Import stuff from appy.fields (and from a few other places too). # Import stuff from appy.fields (and from a few other places too).
# This way, when an app gets "from appy.gen import *", everything is available. # This way, when an app gets "from appy.gen import *", everything is available.
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from appy.fields import Page, Phase, Group, Field, Column, No from appy.fields import Field
from appy.fields.action import Action from appy.fields.action import Action
from appy.fields.boolean import Boolean from appy.fields.boolean import Boolean
from appy.fields.computed import Computed from appy.fields.computed import Computed
@ -22,9 +20,14 @@ from appy.fields.list import List
from appy.fields.pod import Pod from appy.fields.pod import Pod
from appy.fields.ref import Ref, autoref from appy.fields.ref import Ref, autoref
from appy.fields.string import String, Selection from appy.fields.string import String, Selection
from appy.fields.search import Search, UiSearch
from appy.fields.group import Group, Column
from appy.fields.page import Page
from appy.fields.phase import Phase
from appy.gen.layout import Table from appy.gen.layout import Table
from appy.px import Px from appy.px import Px
from appy import Object from appy import Object
No = gutils.No
# Default Appy permissions ----------------------------------------------------- # Default Appy permissions -----------------------------------------------------
r, w, d = ('read', 'write', 'delete') r, w, d = ('read', 'write', 'delete')
@ -55,160 +58,6 @@ class Import:
# and must return a similar, sorted, list. # and must return a similar, sorted, list.
self.sort = sort self.sort = sort
class Search:
'''Used for specifying a search for a given class.'''
def __init__(self, name, group=None, sortBy='', sortOrder='asc', limit=None,
default=False, colspan=1, translated=None, show=True,
translatedDescr=None, **fields):
self.name = name
# Searches may be visually grouped in the portlet.
self.group = Group.get(group)
self.sortBy = sortBy
self.sortOrder = sortOrder
self.limit = limit
# If this search is the default one, it will be triggered by clicking
# on main link.
self.default = default
self.colspan = colspan
# If a translated name or description is already given here, we will
# use it instead of trying to translate from labels.
self.translated = translated
self.translatedDescr = translatedDescr
# Condition for showing or not this search
self.show = show
# In the dict below, keys are indexed field names or names of standard
# indexes, and values are search values.
self.fields = fields
@staticmethod
def getIndexName(fieldName, usage='search'):
'''Gets the name of the technical index that corresponds to field named
p_fieldName. Indexes can be used for searching (p_usage="search") or
for sorting (usage="sort"). The method returns None if the field
named p_fieldName can't be used for p_usage.'''
if fieldName == 'title':
if usage == 'search': return 'Title'
else: return 'SortableTitle'
# Indeed, for field 'title', Appy has a specific index
# 'SortableTitle', because index 'Title' is a TextIndex
# (for searchability) and can't be used for sorting.
elif fieldName == 'state': return 'State'
elif fieldName == 'created': return 'Created'
elif fieldName == 'modified': return 'Modified'
elif fieldName in defaultIndexes: return fieldName
else:
return 'get%s%s'% (fieldName[0].upper(),fieldName[1:])
@staticmethod
def getSearchValue(fieldName, fieldValue, klass):
'''Returns a transformed p_fieldValue for producing a valid search
value as required for searching in the index corresponding to
p_fieldName.'''
field = getattr(klass, fieldName, None)
if (field and (field.getIndexType() == 'TextIndex')) or \
(fieldName == 'SearchableText'):
# For TextIndex indexes. We must split p_fieldValue into keywords.
res = gutils.Keywords(fieldValue).get()
elif isinstance(fieldValue, basestring) and fieldValue.endswith('*'):
v = fieldValue[:-1]
# Warning: 'z' is higher than 'Z'!
res = {'query':(v,v+'z'), 'range':'min:max'}
elif type(fieldValue) in sutils.sequenceTypes:
if fieldValue and isinstance(fieldValue[0], basestring):
# We have a list of string values (ie: we need to
# search v1 or v2 or...)
res = fieldValue
else:
# We have a range of (int, float, DateTime...) values
minv, maxv = fieldValue
rangev = 'minmax'
queryv = fieldValue
if minv == None:
rangev = 'max'
queryv = maxv
elif maxv == None:
rangev = 'min'
queryv = minv
res = {'query':queryv, 'range':rangev}
else:
res = fieldValue
return res
def updateSearchCriteria(self, criteria, klass, advanced=False):
'''This method updates dict p_criteria with all the search criteria
corresponding to this Search instance. If p_advanced is True,
p_criteria correspond to an advanced search, to be stored in the
session: in this case we need to keep the Appy names for parameters
sortBy and sortOrder (and not "resolve" them to Zope's sort_on and
sort_order).'''
# Put search criteria in p_criteria
for fieldName, fieldValue in self.fields.iteritems():
# Management of searches restricted to objects linked through a
# Ref field: not implemented yet.
if fieldName == '_ref': continue
# Make the correspondence between the name of the field and the
# name of the corresponding index, excepted if advanced is True: in
# that case, the correspondence will be done later.
if not advanced:
attrName = Search.getIndexName(fieldName)
# Express the field value in the way needed by the index
criteria[attrName] = Search.getSearchValue(fieldName,
fieldValue, klass)
else:
criteria[fieldName]= fieldValue
# Add a sort order if specified
if self.sortBy:
if not advanced:
criteria['sort_on'] = Search.getIndexName(self.sortBy,
usage='sort')
if self.sortOrder == 'desc': criteria['sort_order'] = 'reverse'
else: criteria['sort_order'] = None
else:
criteria['sortBy'] = self.sortBy
criteria['sortOrder'] = self.sortOrder
def isShowable(self, klass, tool):
'''Is this Search instance (defined in p_klass) showable?'''
if self.show.__class__.__name__ == 'staticmethod':
return gutils.callMethod(tool, self.show, klass=klass)
return self.show
class UiSearch:
'''Instances of this class are generated on-the-fly for manipulating a
Search from the User Interface.'''
# PX for rendering a search.
pxView = Px('''
<div class="portletSearch">
<a href=":'%s?className=%s&amp;search=%s' % \
(queryUrl, rootClass, search['name'])"
class=":search['name'] == currentSearch and 'portletCurrent' or ''"
title=":search['translatedDescr']">:search['translated']</a>
</div>''')
def __init__(self, search, className, tool):
self.search = search
self.name = search.name
self.type = 'search'
self.colspan = search.colspan
if search.translated:
self.translated = search.translated
self.translatedDescr = search.translatedDescr
else:
# The label may be specific in some special cases.
labelDescr = ''
if search.name == 'allSearch':
label = '%s_plural' % className
elif search.name == 'customSearch':
label = 'search_results'
else:
label = '%s_search_%s' % (className, search.name)
labelDescr = label + '_descr'
self.translated = tool.translate(label)
if labelDescr:
self.translatedDescr = tool.translate(labelDescr)
else:
self.translatedDescr = ''
# Workflow-specific types and default workflows -------------------------------- # Workflow-specific types and default workflows --------------------------------
appyToZopePermissions = { appyToZopePermissions = {
'read': ('View', 'Access contents information'), 'read': ('View', 'Access contents information'),

View file

@ -1,6 +1,6 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import re, os, os.path, base64, urllib import re, os, os.path, base64, urllib
from appy.shared.utils import normalizeText from appy.shared import utils as sutils
# Function for creating a Zope object ------------------------------------------ # Function for creating a Zope object ------------------------------------------
def createObject(folder, id, className, appName, wf=True, noSecurity=False): def createObject(folder, id, className, appName, wf=True, noSecurity=False):
@ -96,7 +96,7 @@ class Keywords:
toRemove = '?-+*()' toRemove = '?-+*()'
def __init__(self, keywords, operator='AND'): def __init__(self, keywords, operator='AND'):
# Clean the p_keywords that the user has entered. # Clean the p_keywords that the user has entered.
words = normalizeText(keywords) words = sutils.normalizeText(keywords)
if words == '*': words = '' if words == '*': words = ''
for c in self.toRemove: words = words.replace(c, ' ') for c in self.toRemove: words = words.replace(c, ' ')
self.keywords = words.split() self.keywords = words.split()
@ -220,4 +220,24 @@ def writeCookie(login, password, request):
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip() cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
cookieValue = urllib.quote(cookieValue) cookieValue = urllib.quote(cookieValue)
request.RESPONSE.setCookie('_appy_', cookieValue, path='/') request.RESPONSE.setCookie('_appy_', cookieValue, path='/')
# ------------------------------------------------------------------------------
def initMasterValue(v):
'''Standardizes p_v as a list of strings.'''
if not isinstance(v, bool) and not v: res = []
elif type(v) not in sutils.sequenceTypes: res = [v]
else: res = v
return [str(v) for v in res]
# ------------------------------------------------------------------------------
class No:
'''When you write a workflow condition method and you want to return False
but you want to give to the user some explanations about why a transition
can't be triggered, do not return False, return an instance of No
instead. When creating such an instance, you can specify an error
message.'''
def __init__(self, msg):
self.msg = msg
def __nonzero__(self):
return False
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------