Allowed to express layouts in a more concise manner and various graphical improvements.

This commit is contained in:
Gaetan Delannay 2010-09-13 21:04:10 +02:00
parent eb52c1bb7d
commit 0b4f6e1f79
13 changed files with 187 additions and 96 deletions

View file

@ -31,7 +31,8 @@ class Group:
'''Used for describing a group of widgets within a page.'''
def __init__(self, name, columns=['100%'], wide=True, style='fieldset',
hasLabel=True, hasDescr=False, hasHelp=False,
hasHeaders=False, group=None, colspan=1, valign='top'):
hasHeaders=False, group=None, colspan=1, align='center',
valign='top'):
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
@ -66,6 +67,7 @@ class 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
if style == 'tabs':
# Group content will be rendered as tabs. In this case, some
@ -351,26 +353,7 @@ class Type:
self.type = self.__class__.__name__
self.pythonType = None # The True corresponding Python type
# Get the layouts. Consult layout.py for more info about layouts.
areDefaultLayouts = False
if not layouts:
# Get the default layouts as defined by the subclass
areDefaultLayouts = True
layouts = self.getDefaultLayouts()
if not layouts:
# Get the global default layouts
layouts = copy.deepcopy(defaultFieldLayouts)
else:
layouts = copy.deepcopy(layouts)
# We make copies of layouts, because every layout can be different,
# even if the user decides to reuse one from one field to another.
# This is because we modify every layout for adding
# master/slave-related info, focus-related info, etc, which can be
# different from one field to the other.
# Express the layouts in a standardized way.
self.layouts = self.formatLayouts(layouts, areDefaultLayouts)
self.hasLabel = self.hasLayoutElement('l')
self.hasDescr = self.hasLayoutElement('d')
self.hasHelp = self.hasLayoutElement('h')
self.layouts = self.formatLayouts(layouts)
# Can we filter this field?
self.filterable = False
# Can this field have values that can be edited and validated?
@ -451,21 +434,57 @@ class Type:
else:
res = self.show
# Take into account possible values 'view' and 'edit' for 'show' param.
if (res == 'view' and isEdit) or (res == 'edit' and not isEdit):
res = False
if res == 'view':
if isEdit: res = False
else: res = True
elif res == 'edit':
if isEdit: res = True
else: res = False
return res
def formatLayouts(self, layouts, areDefault):
'''Standardizes the given dict of p_layouts. p_areDefault is True if
p_layouts are the global default layouts or a subclass-specific set
of default layouts.'''
# Create a Table instance for every simple layout string.
def showPage(self, obj):
'''Must the page where this field lies be shown ? "Show value" can be
True, False or 'view' (page is available only in "view" mode).'''
if callable(self.pageShow):
return self.pageShow(obj.appy())
else:
return self.pageShow
def formatLayouts(self, layouts):
'''Standardizes the given p_layouts. .'''
# First, get the layouts as a dictionary, if p_layouts is None or
# expressed as a simple string.
areDefault = False
if not layouts:
# Get the default layouts as defined by the subclass
areDefault = True
layouts = self.getDefaultLayouts()
if not layouts:
# Get the global default layouts
layouts = copy.deepcopy(defaultFieldLayouts)
else:
if isinstance(layouts, basestring) or isinstance(layouts, Table):
# The user specified a single layoutString (the "edit" one)
layouts = {'edit': layouts}
else:
layouts = copy.deepcopy(layouts)
# Here, we make a copy of the layouts, because every layout can
# be different, even if the user decides to reuse one from one
# field to another. This is because we modify every layout for
# adding master/slave-related info, focus-related info, etc,
# which can be different from one field to the other.
# We have now a dict of layouts in p_layouts. Ensure now that a Table
# instance is created for every layout (=value from the dict). Indeed,
# a layout could have been expressed as a simple layout string.
for layoutType in layouts.iterkeys():
if isinstance(layouts[layoutType], basestring):
layouts[layoutType] = Table(layouts[layoutType])
# Create the "cell" layout if not specified.
# Create the "view" layout from the "edit" layout if not specified
if 'view' not in layouts:
layouts['view'] = Table(other=layouts['edit'], derivedType='view')
# Create the "cell" layout from the 'view' layout if not specified.
if 'cell' not in layouts:
layouts['cell'] = Table('f')
layouts['cell'] = Table(other=layouts['view'], derivedType='cell')
# Put the required CSS classes in the layouts
layouts['cell'].addCssClasses('no-style-table')
if self.master:
@ -473,7 +492,12 @@ class Type:
# allowing to show/hide, in Javascript, its widget according to
# master value.
classes = 'slave_%s' % self.master.id
classes += ' slaveValue_%s_%s' % (self.master.id, self.masterValue)
if type(self.masterValue) not in sequenceTypes:
masterValues = [self.masterValue]
else:
masterValues = self.masterValue
for masterValue in masterValues:
classes += ' slaveValue_%s_%s' % (self.master.id, masterValue)
layouts['view'].addCssClasses(classes)
layouts['edit'].addCssClasses(classes)
if self.focus:
@ -489,17 +513,21 @@ class Type:
if not self.required:
for layoutType in layouts.iterkeys():
layouts[layoutType].removeElement('r')
# Derive some boolean values from the layouts.
self.hasLabel = self.hasLayoutElement('l', layouts)
self.hasDescr = self.hasLayoutElement('d', layouts)
self.hasHelp = self.hasLayoutElement('h', layouts)
# Store Table instance's dicts instead of instances: this way, they can
# be manipulated in ZPTs.
for layoutType in layouts.iterkeys():
layouts[layoutType] = layouts[layoutType].get()
return layouts
def hasLayoutElement(self, element):
def hasLayoutElement(self, element, layouts):
'''This method returns True if the given layout p_element can be found
at least once among the various p_layouts defined for this field.'''
for layout in self.layouts.itervalues():
if element in layout['layoutString']: return True
for layout in layouts.itervalues():
if element in layout.layoutString: return True
return False
def getDefaultLayouts(self):
@ -1161,14 +1189,16 @@ class Ref(Type):
def getDefaultLayouts(self): return {'view': 'l-f', 'edit': 'lrv-f'}
def isShowable(self, obj, layoutType):
res = Type.isShowable(self, obj, layout)
res = Type.isShowable(self, obj, layoutType)
if not res: return res
# We add here specific Ref rules for preventing to show the field under
# some inappropriate circumstances.
if (layoutType == 'edit') and self.add: return False
if self.isBack:
if layoutType == 'edit': return False
else:
return obj.getBRefs(self.relationship)
return True
return res
def getValue(self, obj):
if self.isBack:
@ -1622,6 +1652,6 @@ class Config:
# ------------------------------------------------------------------------------
# Special field "type" is mandatory for every class. If one class does not
# define it, we will add a copy of the instance defined below.
title = String(multiplicity=(1,1), indexed=True, show='edit')
title = String(multiplicity=(1,1), show='edit')
title.init('title', None, 'appy')
# ------------------------------------------------------------------------------

View file

@ -118,21 +118,47 @@ class Row(LayoutElement):
# ------------------------------------------------------------------------------
class Table(LayoutElement):
'''Represents a table where to dispose graphical elements.'''
def __init__(self, layoutString, style=None, css_class='', cellpadding=0,
cellspacing=0, width='100%', align='left'):
self.style = style
self.css_class = css_class
self.cellpadding = cellpadding
self.cellspacing = cellspacing
self.width = width
self.align = align
simpleParams = ('style', 'css_class', 'cellpadding', 'cellspacing', 'width',
'align')
derivedRepls = {'view': 'hrv', 'cell': 'l'}
def __init__(self, layoutString=None, style=None, css_class='',
cellpadding=0, cellspacing=0, width='100%', align='left',
other=None, derivedType=None):
if other:
# We need to create a Table instance from another Table instance,
# given in p_other. In this case, we ignore previous params.
if derivedType != None:
# We will not simply clone p_other. If p_derivedType is:
# - "view", p_derivedFrom is an "edit" layout, and we must
# create the corresponding "view" layout;
# - "cell", p_derivedFrom is a "view" layout, and we must
# create the corresponding "cell" layout;
self.layoutString = Table.deriveLayout(other.layoutString,
derivedType)
else:
self.layoutString = layoutString
source = 'other.'
else:
source = ''
self.layoutString = layoutString
# Initialise simple params, either from the true params, either from
# the p_other Table instance.
for param in Table.simpleParams:
exec 'self.%s = %s%s' % (param, source, param)
# The following attribute will store a special Row instance used for
# defining column properties.
self.headerRow = None
# The content rows are stored hereafter.
# The content rows will be stored hereafter.
self.rows = []
self.layoutString = layoutString
self.decodeRows(layoutString)
self.decodeRows(self.layoutString)
@staticmethod
def deriveLayout(layout, derivedType):
'''Returns a layout derived from p_layout.'''
res = layout
for letter in Table.derivedRepls[derivedType]:
res = res.replace(letter, '')
return res
def addCssClasses(self, css_class):
'''Adds a single or a group of p_css_class.'''
@ -186,5 +212,5 @@ class Table(LayoutElement):
# ------------------------------------------------------------------------------
defaultPageLayouts = {
'view': Table('m;-s|-n!-w|-b|'), 'edit': Table('m;-w|-b|')}
defaultFieldLayouts = {'view': 'l;f!', 'edit': 'lrv;f!'}
defaultFieldLayouts = {'edit': 'lrv;f!'}
# ------------------------------------------------------------------------------

View file

@ -652,6 +652,7 @@ class Generator(AbstractGenerator):
Flavour._appy_addImportRelatedFields(classDescr)
Flavour._appy_addWorkflowFields(self.flavour)
Flavour._appy_addWorkflowFields(self.podTemplate)
Flavour._appy_addWorkflowFields(self.user)
# Complete self.flavour.orderedAttributes from the attributes that we
# just added to the Flavour model class.
for fieldName in Flavour._appy_attributes:

View file

@ -139,7 +139,15 @@ class PloneInstaller:
site.invokeFactory(self.appyFolderType, self.productName,
title=self.productName)
getattr(site.portal_types, self.appyFolderType).global_allow = 0
appFolder = getattr(site, self.productName)
# Manager has been granted Add permissions for all root classes.
# This may not be desired, so remove this.
appFolder = getattr(site, self.productName)
for className in self.config.rootClasses:
permission = self.getAddPermission(className)
print 'Permission is', permission
appFolder.manage_permission(permission, (), acquire=0)
else:
appFolder = getattr(site, self.productName)
# All roles defined as creators should be able to create the
# corresponding root content types in this folder.
i = -1

View file

@ -46,6 +46,7 @@ class AbstractMixin:
appyType.store(obj, value)
if created:
# Now we have a title for the object, so we derive a nice id
obj.unmarkCreationFlag()
obj._renameAfterCreation(check_auto_id=True)
if previousData:
# Keep in history potential changes on historized fields
@ -181,31 +182,45 @@ class AbstractMixin:
obj.plone_utils.addPortalMessage(msg)
return self.goto('%s/skyn/view' % obj.absolute_url(), True)
if rq.get('buttonPrevious.x', None):
# Go to the previous page (edit mode) for this object.
# Go to the previous page for this object.
# We recompute the list of phases and pages because things
# may have changed since the object has been updated (ie,
# additional pages may be shown or hidden now, so the next and
# previous pages may have changed).
# previous pages may have changed). Moreover, previous and next
# pages may not be available in "edit" mode, so we return the edit
# or view pages depending on page.show.
currentPage = rq.get('page')
phaseInfo = self.getAppyPhases(page=currentPage)
previousPage = self.getPreviousPage(phaseInfo, currentPage)
previousPage, show = self.getPreviousPage(phaseInfo, currentPage)
if previousPage:
rq.set('page', previousPage)
return obj.skyn.edit(obj)
# Return the edit or view page?
if show != 'view':
rq.set('page', previousPage)
return obj.skyn.edit(obj)
else:
urlBack = '%s/skyn/view?page=%s' % (obj.absolute_url(),
previousPage)
return self.goto(urlBack)
else:
obj.plone_utils.addPortalMessage(msg)
return self.goto('%s/skyn/view' % obj.absolute_url(), True)
return self.goto('%s/skyn/view' % obj.absolute_url())
if rq.get('buttonNext.x', None):
# Go to the next page (edit mode) for this object
# Go to the next page for this object
currentPage = rq.get('page')
phaseInfo = self.getAppyPhases(page=currentPage)
nextPage = self.getNextPage(phaseInfo, currentPage)
nextPage, show = self.getNextPage(phaseInfo, currentPage)
if nextPage:
rq.set('page', nextPage)
return obj.skyn.edit(obj)
# Return the edit or view page?
if show != 'view':
rq.set('page', nextPage)
return obj.skyn.edit(obj)
else:
urlBack = '%s/skyn/view?page=%s' % (obj.absolute_url(),
nextPage)
return self.goto(urlBack)
else:
obj.plone_utils.addPortalMessage(msg)
return self.goto('%s/skyn/view' % obj.absolute_url(), True)
return self.goto('%s/skyn/view' % obj.absolute_url())
return obj.skyn.edit(obj)
def onDelete(self):
@ -575,28 +590,36 @@ class AbstractMixin:
pageIndex = phase['pages'].index(page)
if pageIndex > 0:
# We stay on the same phase, previous page
return phase['pages'][pageIndex-1]
res = phase['pages'][pageIndex-1]
show = phase['pageShows'][res]
return res, show
else:
if phase['previousPhase']:
# We go to the last page of previous phase
previousPhase = phase['previousPhase']
return previousPhase['pages'][-1]
res = previousPhase['pages'][-1]
show = previousPhase['pageShows'][res]
return res, show
else:
return None
return None, None
def getNextPage(self, phase, page):
'''Returns the page that follows p_page which is in p_phase.'''
pageIndex = phase['pages'].index(page)
if pageIndex < len(phase['pages'])-1:
# We stay on the same phase, next page
return phase['pages'][pageIndex+1]
res = phase['pages'][pageIndex+1]
show = phase['pageShows'][res]
return res, show
else:
if phase['nextPhase']:
# We go to the first page of next phase
nextPhase = phase['nextPhase']
return nextPhase['pages'][0]
res = nextPhase['pages'][0]
show = nextPhase['pageShows'][res]
return res, show
else:
return None
return None, None
def changeRefOrder(self, fieldName, objectUid, newIndex, isDelta):
'''This method changes the position of object with uid p_objectUid in
@ -873,12 +896,6 @@ class AbstractMixin:
res = [o.appy() for o in objs]
return res
def _appy_showPage(self, page, pageShow):
'''Must I show p_page?'''
if callable(pageShow):
return pageShow(self.appy())
else: return pageShow
def _appy_showState(self, workflow, stateShow):
'''Must I show a state whose "show value" is p_stateShow?'''
if callable(stateShow):

View file

@ -78,7 +78,7 @@ class User(ModelClass):
firstName = String(**gm)
def showLogin(self): pass
def validateLogin(self): pass
login = String(show=showLogin, validator=validateLogin, **gm)
login = String(show=showLogin, validator=validateLogin, indexed=True, **gm)
def showPassword(self): pass
def validatePassword(self): pass
password1 = String(format=String.PASSWORD, show=showPassword,
@ -142,7 +142,7 @@ class Flavour(ModelClass):
res.group = copy.copy(appyType.group)
res.phase = 'main'
# Set default layouts for all Flavour fields
res.layouts = None
res.layouts = res.formatLayouts(None)
res.specificReadPermission = False
res.specificWritePermission = False
res.multiplicity = (0, appyType.multiplicity[1])

View file

@ -651,8 +651,8 @@
This macro shows the range of buttons (next, previous, save,...).
</tal:comment>
<div metal:define-macro="buttons"
tal:define="previousPage python: contextObj.getPreviousPage(phaseInfo, page);
nextPage python: contextObj.getNextPage(phaseInfo, page);
tal:define="previousPage python: contextObj.getPreviousPage(phaseInfo, page)[0];
nextPage python: contextObj.getNextPage(phaseInfo, page)[0];
isEdit python: layoutType == 'edit';">
<br/>
<tal:previousButton condition="previousPage">
@ -682,7 +682,7 @@
tal:attributes="src string:$portal_url/skyn/cancel.png"/>
</tal:cancelButton>
<tal:editLink condition="not:isEdit">
<tal:editLink condition="python: not isEdit and (phaseInfo['pageShows'][page] != 'view')">
<img tal:define="nav request/nav|nothing;
nav python: test(nav, '&nav=%s' % nav, '')"
title="Edit" i18n:domain="plone" i18n:attributes="title" style="cursor:pointer"

View file

@ -153,19 +153,20 @@
<table tal:define="phases contextObj/getAppyPhases|nothing;
page python: request.get('page', 'main')"
tal:condition="python: phases and not ((len(phases)==1) and len(phases[0]['pages'])==1)"
cellspacing="1" cellpadding="0" width="100%">
cellspacing="1" cellpadding="2" width="100%">
<tal:phase repeat="phase phases">
<tal:comment replace="nothing">The box containing phase-related information</tal:comment>
<tr>
<td tal:define="label python:'%s_phase_%s' % (contextObj.meta_type, phase['name']);
displayLink python: (phase['phaseStatus'] != 'Future') and ('/portal_factory' not in contextObj.absolute_url()) and (len(phase['pages']) == 1)"
tal:attributes="class python: (len(phases) &gt; 1) and ('appyPhase step%s' % phase['phaseStatus']) or 'appyPhase'">
<span class="portletGroup" tal:condition="python: len(phases) &gt; 1">
<div class="portletGroup" tal:condition="python: len(phases) &gt; 1">
<a tal:attributes="href python: '%s?page=%s' % (contextObj.getUrl(), phase['pages'][0]);"
tal:condition="displayLink"
tal:content="python: tool.translate(label)"></a>
<span tal:condition="not: displayLink" tal:replace="python: tool.translate(label)"/>
</span>
</div>
<div class="portletMenu">
<table width="100%" cellpadding="0" cellspacing="0"
tal:condition="python: len(phase['pages']) &gt; 1">
<tr tal:repeat="aPage phase/pages" valign="top">
@ -184,6 +185,7 @@
</td>
</tr>
</table>
</div>
</td>
</tr>
<tal:comment replace="nothing">The down arrow pointing to the next phase (if any)</tal:comment>

View file

@ -93,7 +93,7 @@
<tal:comment replace="nothing">First row: the tabs.</tal:comment>
<tr valign="middle"><td style="border-bottom: 1px solid #ff8040">
<table cellpadding="0" cellspacing="0" style="position:relative; bottom:-1px;">
<tr valign="bottom">
<tr valign="middle">
<tal:tab repeat="widgetRow widget/widgets">
<tal:id define="tabId python:'tab_%s_%d_%d' % (widget['name'], repeat['widgetRow'].number(), len(widget['widgets']))">
<td><img tal:attributes="src string: $portal_url/skyn/tabLeft.png;
@ -134,12 +134,13 @@
This macro displays the content of a group of widgets.
It is exclusively called by macro "group" above.
</tal:comment>
<table metal:define-macro="groupContent" align="center"
tal:attributes="width python: test(widget['wide'], '100%', '')">
<table metal:define-macro="groupContent"
tal:attributes="width python: test(widget['wide'], '100%', '');
align widget/align">
<tal:comment replace="nothing">Display the title of the group if it is not rendered a fieldset.</tal:comment>
<tr tal:condition="python: (widget['style'] != 'fieldset') and widget['hasLabel']">
<td tal:attributes="colspan python: len(widget['columnsWidths']);
class widget/style">
class widget/style" align="left">
<span tal:replace="structure python: contextObj.translate(widget['labelId'])"/>
<tal:help condition="widget/hasHelp">
<metal:call use-macro="portal/skyn/widgets/show/macros/help"/>

View file

@ -4,10 +4,6 @@
border-left: 1px solid #8cacbb;
border-right: 1px solid #8cacbb;
}
.portletGroup {
font-size: 85%;
padding-left: 0.7em;
}
/* Stylesheet with Internet Explorer-specific workarounds. */
* html #portal-columns {

View file

@ -6,7 +6,7 @@ textarea { width: 99%; }
#portal-breadcrumbs { display: none; }
#importedElem { color: grey; font-style: italic; }
label { font-weight: bold; font-style: italic; }
label { font-weight: bold; font-style: italic; line-height: 1.4em;}
.discreet { font-size: 94%; }
.appyList { line-height: 1.1em; margin: 0 0 0.5em 1.2em; padding: 0; }
.appyBullet { margin: 0; }
@ -26,7 +26,7 @@ label { font-weight: bold; font-style: italic; }
.appyPhase {
border-style: dashed;
border-width: thin;
padding: 0 0.1em 0 1em;
padding: 0 0.6em 0 1em;
}
.appyState {
@ -68,14 +68,14 @@ label { font-weight: bold; font-style: italic; }
background-color: #cde2a7;
background-image: url(&dtml-portal_url;/skyn/done.png);
background-repeat: no-repeat;
background-position: -1px 4px;
background-position: -1px 7px;
}
.stepCurrent {
background-color: #eef3f5;
background-image: url(&dtml-portal_url;/skyn/current.png);
background-repeat: no-repeat;
background-position: -1px 4px;
background-position: -1px 7px;
}
.stepFuture {
@ -224,11 +224,13 @@ th {
font-variant: small-caps;
font-weight: bold;
font-style: normal;
margin: 0.3em 0 0.3em 0;
}
.portletSep { border-top: 1px dashed #8cacbb; }
.portletGroupItem { padding-left: 0.8em; font-style: italic; }
.portletPageItem { font-style: italic; }
.portletCurrent { font-weight: bold; }
.portletMenu { margin-bottom: 0.4em; }
div.appyGrey {
display: none;

View file

@ -63,6 +63,9 @@ class UserWrapper(AbstractWrapper):
# Perform updates on the corresponding Plone user
ploneUser = self.o.portal_membership.getMemberById(self.login)
ploneUser.setMemberProperties({'fullname': self.title})
# This object must be owned by its Plone user
if 'Owner' not in self.o.get_local_roles_for_userid(self.login):
self.o.manage_addLocalRoles(self.login, ('Owner',))
# Change group membership according to self.roles. Indeed, instead of
# granting roles directly to the user, we will add the user to a
# Appy-created group having this role.

View file

@ -83,6 +83,9 @@ class PhaseDescr(Descr):
self.obj = obj
self.phaseStatus = None
self.pages = [] # The list of pages in this phase
# Below, a dict of "show" values (True, False or 'view') for every page
# in self.pages.
self.pageShows = {}
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
@ -92,9 +95,11 @@ class PhaseDescr(Descr):
def addPage(self, appyType, obj):
toAdd = appyType.page
if (toAdd not in self.pages) and \
obj._appy_showPage(appyType.page, appyType.pageShow):
self.pages.append(toAdd)
if toAdd not in self.pages:
showValue = appyType.showPage(obj)
if showValue:
self.pages.append(toAdd)
self.pageShows[toAdd] = showValue
def computeStatus(self, allPhases):
'''Compute status of whole phase based on individual status of states