appy.gen: added the possibility to create and manage web pages into an app; every class can now define a method showPortlet. If the class does not define it or if it returns False, the portlet won't be shown when showing/editing instances of this class.

This commit is contained in:
Gaetan Delannay 2012-03-26 19:09:45 +02:00
parent 8aa03a091a
commit 1e9e4df5a6
9 changed files with 113 additions and 28 deletions

View file

@ -637,7 +637,7 @@ class TranslationClassDescriptor(ClassDescriptor):
self.customized = False self.customized = False
def getParents(self, allClasses=()): return ('Translation',) def getParents(self, allClasses=()): return ('Translation',)
def isFolder(self, klass=None): return False
def generateSchema(self): def generateSchema(self):
ClassDescriptor.generateSchema(self, configClass=True) ClassDescriptor.generateSchema(self, configClass=True)
@ -681,4 +681,29 @@ class TranslationClassDescriptor(ClassDescriptor):
params['format'] = gen.String.TEXT params['format'] = gen.String.TEXT
params['height'] = height params['height'] = height
self.addField(messageId, gen.String(**params)) self.addField(messageId, gen.String(**params))
class PageClassDescriptor(ClassDescriptor):
'''Represents the class that corresponds to a Page.'''
def __init__(self, klass, generator):
ClassDescriptor.__init__(self,klass,klass._appy_attributes[:],generator)
self.modelClass = self.klass
self.predefined = True
self.customized = False
def getParents(self, allClasses=()):
res = ['Page']
if self.customized:
res.append('%s.%s' % (self.klass.__module__, self.klass.__name__))
return res
def update(self, klass, attributes):
'''This method is called by the generator when he finds a custom page
definition. We must then add the custom page elements in this
default Page descriptor.
NOTE: currently, it is not possible to define a custom Page class.'''
self.orderedAttributes += attributes
self.klass = klass
self.customized = True
def isFolder(self, klass=None): return True
def generateSchema(self):
ClassDescriptor.generateSchema(self, configClass=True)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -6,7 +6,7 @@ import appy.gen as gen
from po import PoMessage, PoFile, PoParser from po import PoMessage, PoFile, PoParser
from descriptors import * from descriptors import *
from utils import produceNiceMessage, getClassName from utils import produceNiceMessage, getClassName
from model import ModelClass, User, Group, Tool, Translation from model import ModelClass, User, Group, Tool, Translation, Page
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class GeneratorError(Exception): pass class GeneratorError(Exception): pass
@ -353,11 +353,12 @@ class ZopeGenerator(Generator):
Generator.__init__(self, *args, **kwargs) Generator.__init__(self, *args, **kwargs)
# Set our own Descriptor classes # Set our own Descriptor classes
self.descriptorClasses['class'] = ClassDescriptor self.descriptorClasses['class'] = ClassDescriptor
# Create our own Tool, User, Group and Translation instances # Create Tool, User, Group, Translation and Page instances.
self.tool = ToolClassDescriptor(Tool, self) self.tool = ToolClassDescriptor(Tool, self)
self.user = UserClassDescriptor(User, self) self.user = UserClassDescriptor(User, self)
self.group = GroupClassDescriptor(Group, self) self.group = GroupClassDescriptor(Group, self)
self.translation = TranslationClassDescriptor(Translation, self) self.translation = TranslationClassDescriptor(Translation, self)
self.page = PageClassDescriptor(Page, self)
# i18n labels to generate # i18n labels to generate
self.labels = [] # i18n labels self.labels = [] # i18n labels
self.referers = {} self.referers = {}
@ -603,13 +604,14 @@ class ZopeGenerator(Generator):
'''Returns the descriptors for all the classes in the generated '''Returns the descriptors for all the classes in the generated
gen-application. If p_include is: gen-application. If p_include is:
* "all" it includes the descriptors for the config-related * "all" it includes the descriptors for the config-related
classes (tool, user, group, translation) classes (tool, user, group, translation, page)
* "allButTool" it includes the same descriptors, the tool excepted * "allButTool" it includes the same descriptors, the tool excepted
* "custom" it includes descriptors for the config-related classes * "custom" it includes descriptors for the config-related classes
for which the user has created a sub-class.''' for which the user has created a sub-class.'''
if not include: return self.classes if not include: return self.classes
res = self.classes[:] res = self.classes[:]
configClasses = [self.tool, self.user, self.group, self.translation] configClasses = [self.tool, self.user, self.group, self.translation,
self.page]
if include == 'all': if include == 'all':
res += configClasses res += configClasses
elif include == 'allButTool': elif include == 'allButTool':
@ -789,16 +791,22 @@ class ZopeGenerator(Generator):
Tool.users.klass = self.user.klass Tool.users.klass = self.user.klass
Group.users.klass = self.user.klass Group.users.klass = self.user.klass
# Generate the Tool-related classes (User, Group, Translation) # Generate the Tool-related classes (User, Group, Translation, Page)
for klass in (self.user, self.group, self.translation): for klass in (self.user, self.group, self.translation, self.page):
klassType = klass.name[len(self.applicationName):] klassType = klass.name[len(self.applicationName):]
klass.generateSchema() klass.generateSchema()
self.labels += [ Msg(klass.name, '', klassType), self.labels += [ Msg(klass.name, '', klassType),
Msg('%s_plural' % klass.name,'', klass.name+'s')] Msg('%s_plural' % klass.name,'', klass.name+'s')]
repls = self.repls.copy() repls = self.repls.copy()
if klass.isFolder():
parents = 'BaseMixin, Folder'
icon = 'folder.gif'
else:
parents = 'BaseMixin, SimpleItem'
icon = 'object.gif'
repls.update({'methods': klass.methods, 'genClassName': klass.name, repls.update({'methods': klass.methods, 'genClassName': klass.name,
'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem', 'baseMixin':'BaseMixin', 'parents': parents,
'classDoc': 'Standard Appy class', 'icon':'object.gif'}) 'classDoc': 'Standard Appy class', 'icon': icon})
self.copyFile('Class.pyt', repls, destName='%s.py' % klass.name) self.copyFile('Class.pyt', repls, destName='%s.py' % klass.name)
# Before generating the Tool class, finalize it with query result # Before generating the Tool class, finalize it with query result

View file

@ -223,14 +223,14 @@ class ToolMixin(BaseMixin):
return [importParams['headers'], elems] return [importParams['headers'], elems]
def showPortlet(self, context): def showPortlet(self, context):
if self.userIsAnon(): return False
if context.id == 'ui': context = context.getParentNode() if context.id == 'ui': context = context.getParentNode()
res = True res = True
if not self.getRootClasses(): if hasattr(context.aq_base, 'appy'):
res = False appyObj = context.appy()
# If there is no root class, show the portlet only if we are within try:
# the configuration. res = appyObj.showPortlet()
if (self.id in context.absolute_url()): res = True except AttributeError:
res = True
return res return res
def getObject(self, uid, appy=False, brain=False): def getObject(self, uid, appy=False, brain=False):
@ -1000,4 +1000,10 @@ class ToolMixin(BaseMixin):
return '<table class="main" align="center" cellpadding="0"><tr>' \ return '<table class="main" align="center" cellpadding="0"><tr>' \
'<td style="padding: 1em 1em 1em 1em">An error occurred. %s' \ '<td style="padding: 1em 1em 1em 1em">An error occurred. %s' \
'</td></tr></table>' % '\n'.join(htmlMessage) '</td></tr></table>' % '\n'.join(htmlMessage)
def getMainPages(self):
'''Returns the main pages.'''
if hasattr(self.o.aq_base, 'pages') and self.o.pages:
return [self.getObject(uid) for uid in self.o.pages ]
return ()
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -1312,6 +1312,9 @@ class BaseMixin:
Else (if the object is stored directly within the tool or the root Else (if the object is stored directly within the tool or the root
data folder) it returns None.''' data folder) it returns None.'''
parent = self.getParentNode() parent = self.getParentNode()
# Not-Managers can't navigate back to the tool
if (parent.id == 'config') and not self.getUser().has_role('Manager'):
return False
if parent.meta_type != 'Folder': return parent if parent.meta_type != 'Folder': return parent
def index_html(self): def index_html(self):

View file

@ -42,7 +42,7 @@ class ModelClass:
in order to avoid name conflicts with user-defined parts of the in order to avoid name conflicts with user-defined parts of the
application model.''' application model.'''
_appy_attributes = [] # We need to keep track of attributes order. _appy_attributes = [] # We need to keep track of attributes order.
folder = False
@classmethod @classmethod
def _appy_getTypeBody(klass, appyType, wrapperName): def _appy_getTypeBody(klass, appyType, wrapperName):
'''This method returns the code declaration for p_appyType.''' '''This method returns the code declaration for p_appyType.'''
@ -67,6 +67,12 @@ class ModelClass:
value = 'tfw' value = 'tfw'
else: else:
value = appyType.getInputLayouts() value = appyType.getInputLayouts()
elif (name == 'klass') and value and (value == klass):
# This is a auto-Ref (a Ref that references the klass itself).
# At this time, we can't reference the class that is still being
# defined. So we initialize it to None. The post-init of the
# field must be done manually in wrappers.py.
value = 'None'
elif isinstance(value, basestring): elif isinstance(value, basestring):
value = '"%s"' % value value = '"%s"' % value
elif isinstance(value, gen.Ref): elif isinstance(value, gen.Ref):
@ -83,7 +89,7 @@ class ModelClass:
elif isinstance(value, gen.Group): elif isinstance(value, gen.Group):
value = 'Grp("%s")' % value.name value = 'Grp("%s")' % value.name
elif isinstance(value, gen.Page): elif isinstance(value, gen.Page):
value = 'pages["%s"]' % value.name value = 'pges["%s"]' % value.name
elif callable(value): elif callable(value):
className = wrapperName className = wrapperName
if (appyType.type == 'Ref') and appyType.isBack: if (appyType.type == 'Ref') and appyType.isBack:
@ -104,7 +110,7 @@ class ModelClass:
else: wrapperName = 'W%s' % className else: wrapperName = 'W%s' % className
res = 'class %s(%s):\n' % (className, wrapperName) res = 'class %s(%s):\n' % (className, wrapperName)
# Tool must be folderish # Tool must be folderish
if className == 'Tool': res += ' folder=True\n' if klass.folder: res += ' folder=True\n'
# First, scan all attributes, determine all used pages and create a # First, scan all attributes, determine all used pages and create a
# dict with it. It will prevent us from creating a new Page instance # dict with it. It will prevent us from creating a new Page instance
# for every field. # for every field.
@ -114,12 +120,12 @@ class ModelClass:
exec 'appyType = klass.%s' % name exec 'appyType = klass.%s' % name
if appyType.page.name not in pages: if appyType.page.name not in pages:
pages[appyType.page.name] = appyType.page pages[appyType.page.name] = appyType.page
res += ' pages = {' res += ' pges = {'
for page in pages.itervalues(): for page in pages.itervalues():
# Determine page show # Determine page show
pageShow = page.show pageShow = page.show
if isinstance(pageShow, basestring): pageShow='"%s"' % pageShow if isinstance(pageShow, basestring): pageShow='"%s"' % pageShow
res += '"%s":Page("%s", show=%s),'% (page.name, page.name, pageShow) res += '"%s":Pge("%s", show=%s),'% (page.name, page.name, pageShow)
res += '}\n' res += '}\n'
# Secondly, dump every attribute # Secondly, dump every attribute
for name in klass._appy_attributes: for name in klass._appy_attributes:
@ -183,13 +189,27 @@ class Translation(ModelClass):
def label(self): pass def label(self): pass
def show(self, name): pass def show(self, name): pass
# The Page class ---------------------------------------------------------------
class Page(ModelClass):
_appy_attributes = ['title', 'content', 'pages']
folder = True
title = gen.String(show='edit', indexed=True)
content = gen.String(format=gen.String.XHTML, layouts='f')
# Pages can contain other pages.
def showSubPages(self): pass
pages = gen.Ref(None, multiplicity=(0,None), add=True, link=False,
back=gen.Ref(attribute='parent', show=False),
show=showSubPages)
Page.pages.klass = Page
setattr(Page, Page.pages.back.attribute, Page.pages.back)
# The Tool class --------------------------------------------------------------- # The Tool class ---------------------------------------------------------------
# Prefixes of the fields generated on the Tool. # Prefixes of the fields generated on the Tool.
toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns', toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
'enableAdvancedSearch', 'numberOfSearchColumns', 'enableAdvancedSearch', 'numberOfSearchColumns',
'searchFields', 'optionalFields', 'showWorkflow', 'searchFields', 'optionalFields', 'showWorkflow',
'showAllStatesInPhase') 'showAllStatesInPhase')
defaultToolFields = ('title', 'users', 'groups', 'translations', defaultToolFields = ('title', 'users', 'groups', 'translations', 'pages',
'enableNotifications', 'unoEnabledPython','openOfficePort', 'enableNotifications', 'unoEnabledPython','openOfficePort',
'numberOfResultsPerPage', 'listBoxesMaximumWidth', 'numberOfResultsPerPage', 'listBoxesMaximumWidth',
'appyVersion') 'appyVersion')
@ -197,6 +217,7 @@ defaultToolFields = ('title', 'users', 'groups', 'translations',
class Tool(ModelClass): class Tool(ModelClass):
# In a ModelClass we need to declare attributes in the following list. # In a ModelClass we need to declare attributes in the following list.
_appy_attributes = list(defaultToolFields) _appy_attributes = list(defaultToolFields)
folder = True
# Tool attributes # Tool attributes
title = gen.String(show=False, page=gen.Page('main', show=False)) title = gen.String(show=False, page=gen.Page('main', show=False))
@ -222,6 +243,9 @@ class Tool(ModelClass):
link=False, show='view', link=False, show='view',
back=gen.Ref(attribute='trToTool', show=False), back=gen.Ref(attribute='trToTool', show=False),
page=gen.Page('translations', show='view')) page=gen.Page('translations', show='view'))
pages = gen.Ref(Page, multiplicity=(0,None), add=True, link=False,
show='view', back=gen.Ref(attribute='toTool3', show=False),
page=gen.Page('pages', show='view'))
enableNotifications = gen.Boolean(default=True, enableNotifications = gen.Boolean(default=True,
page=gen.Page('notifications', show=False)) page=gen.Page('notifications', show=False))
@ -235,4 +259,5 @@ class Tool(ModelClass):
for k in toClean: for k in toClean:
exec 'del klass.%s' % k exec 'del klass.%s' % k
klass._appy_attributes = list(defaultToolFields) klass._appy_attributes = list(defaultToolFields)
klass.folder = True
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -1,11 +1,13 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from appy.gen import * from appy.gen import *
Grp = Group # Avoid name clashes with the Group class below and appy.gen.Group Grp = Group # Avoid name clashes with the Group class below and appy.gen.Group
Pge = Page # Avoid name clashes with the Page class below and appy.gen.Page
from appy.gen.wrappers import AbstractWrapper from appy.gen.wrappers import AbstractWrapper
from appy.gen.wrappers.ToolWrapper import ToolWrapper as WTool from appy.gen.wrappers.ToolWrapper import ToolWrapper as WTool
from appy.gen.wrappers.UserWrapper import UserWrapper as WUser from appy.gen.wrappers.UserWrapper import UserWrapper as WUser
from appy.gen.wrappers.GroupWrapper import GroupWrapper as WGroup from appy.gen.wrappers.GroupWrapper import GroupWrapper as WGroup
from appy.gen.wrappers.TranslationWrapper import TranslationWrapper as WT from appy.gen.wrappers.TranslationWrapper import TranslationWrapper as WT
from appy.gen.wrappers.PageWrapper import PageWrapper as WPage
from Globals import InitializeClass from Globals import InitializeClass
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
tfw = {"edit":"f","cell":"f","view":"f"} # Layout for Translation fields tfw = {"edit":"f","cell":"f","view":"f"} # Layout for Translation fields
@ -14,6 +16,10 @@ tfw = {"edit":"f","cell":"f","view":"f"} # Layout for Translation fields
<!User!> <!User!>
<!Group!> <!Group!>
<!Translation!> <!Translation!>
<!Page!>
Page.pages.klass = Page
setattr(Page, Page.pages.back.attribute, Page.pages.back)
<!Tool!> <!Tool!>
<!wrappers!> <!wrappers!>
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -1,4 +1,5 @@
body { font: 75% Helvetica,Arial,sans-serif; background-color: #EAEAEA; } body { font: 75% Helvetica,Arial,sans-serif; background-color: #EAEAEA;
margin-top: 18px}
pre { font: 100% Helvetica,Arial,sans-serif; margin: 0} pre { font: 100% Helvetica,Arial,sans-serif; margin: 0}
h1 { font-size: 11pt; margin:0;} h1 { font-size: 11pt; margin:0;}
h2 { font-size: 10pt; margin:0; font-style: italic; font-weight: normal;} h2 { font-size: 10pt; margin:0; font-style: italic; font-weight: normal;}
@ -38,12 +39,12 @@ img {border: 0}
.xhtml img { margin-right: 5px } .xhtml img { margin-right: 5px }
.main { width: 900px; background-color: white; box-shadow: 3px 3px 3px #A9A9A9; .main { width: 900px; background-color: white; box-shadow: 3px 3px 3px #A9A9A9;
border-style: solid; border-width: 1px; border-color: grey; } border-style: solid; border-width: 1px; border-color: grey}
.top { height: 75px; margin-left: 3em; vertical-align: top;} .top { height: 75px; margin-left: 3em; vertical-align: top;}
.lang { margin-right: 3px; } .lang { margin-right: 3px; }
.userStrip { background-color: #d7dee4; height: 40px; .userStrip { background-color: #d7dee4; height: 35px;
border-top: 1px solid #5F7983; border-bottom: 1px solid #5F7983; } border-top: 1px solid #5F7983; border-bottom: 1px solid #5F7983; }
.login { margin-top: 2px; margin-bottom: 2px; color: white;} .login { margin-top: 2px; margin-bottom: 2px; color: black;}
.buttons { margin-left: 4px;} .buttons { margin-left: 4px;}
.fakeButton { border: 1px solid #D7DEE4; background-color: #fde8e0; .fakeButton { border: 1px solid #D7DEE4; background-color: #fde8e0;
padding: 0px 8px 2px; font: italic 92% Helvetica,Arial,sans-serif} padding: 0px 8px 2px; font: italic 92% Helvetica,Arial,sans-serif}
@ -104,3 +105,4 @@ img {border: 0}
.history th { font-style: italic; text-align; left;} .history th { font-style: italic; text-align; left;}
.topSpace { margin-top: 15px;} .topSpace { margin-top: 15px;}
.discreet { color: grey} .discreet { color: grey}
.pageLink { padding-left: 6px; font-style: italic}

View file

@ -29,7 +29,11 @@
<td tal:attributes="style python: 'background-image: url(%s/ui/banner.jpg)' % appUrl"> <td tal:attributes="style python: 'background-image: url(%s/ui/banner.jpg)' % appUrl">
<table width="100%"> <table width="100%">
<tr valign="top"> <tr valign="top">
<td></td> <tal:comment replace="nothing">Links to main pages</tal:comment>
<td>
<a tal:repeat="page tool/getMainPages" class="pageLink"
tal:content="page/title" tal:attributes="href page/absolute_url"></a>
</td>
<tal:comment replace="nothing">Language selector (links or listbox)</tal:comment> <tal:comment replace="nothing">Language selector (links or listbox)</tal:comment>
<td align="right" tal:condition="tool/showLanguageSelector"> <td align="right" tal:condition="tool/showLanguageSelector">
<tal:lgs define="languages tool/getLanguages; <tal:lgs define="languages tool/getLanguages;

View file

@ -18,8 +18,14 @@ def createObject(folder, id, className, appName, wf=True):
obj.portal_type = className obj.portal_type = className
obj.id = id obj.id = id
obj._at_uid = id obj._at_uid = id
userId = obj.getUser().getId() user = obj.getUser()
# If user is anonymous, userIs is None if not user.getId():
if user.name == 'System Processes':
userId = 'admin' # This is what happens when Zope is starting.
else:
userId = None # Anonymous.
else:
userId = user.getId()
obj.creator = userId or 'Anonymous User' obj.creator = userId or 'Anonymous User'
from DateTime import DateTime from DateTime import DateTime
obj.created = DateTime() obj.created = DateTime()