appy.gen: Ploneless version.

This commit is contained in:
Gaetan Delannay 2011-11-28 22:50:01 +01:00
parent d0cbe7e573
commit a321257e55
13 changed files with 393 additions and 159 deletions

View file

@ -2566,9 +2566,9 @@ class Transition:
comments=comment) comments=comment)
# Update permissions-to-roles attributes # Update permissions-to-roles attributes
targetState.updatePermissions(wf, obj) targetState.updatePermissions(wf, obj)
# Refresh catalog-related security if required # Reindex the object if required. Not only security-related indexes
if not obj.isTemporary(): # (Allowed, State) need to be updated here.
obj.reindex(indexes=('allowedRolesAndUsers', 'State')) if not obj.isTemporary(): obj.reindex()
# Execute the related action if needed # Execute the related action if needed
msg = '' msg = ''
if doAction and self.action: msg = self.executeAction(obj, wf) if doAction and self.action: msg = self.executeAction(obj, wf)
@ -2718,7 +2718,7 @@ class Config:
# If you want to replace the default front page with a page coming from # If you want to replace the default front page with a page coming from
# your application, use the following parameter. Setting # your application, use the following parameter. Setting
# frontPage = True will replace the Plone front page with a page # frontPage = True will replace the Plone front page with a page
# whose content will come fron i18n label "front_page_text". # whose content will come from i18n label "front_page_text".
self.frontPage = False self.frontPage = False
# You can choose the Plone or Appy main template # You can choose the Plone or Appy main template
self.frontPageTemplate = 'plone' # or "appy" self.frontPageTemplate = 'plone' # or "appy"

View file

@ -515,6 +515,33 @@ class UserClassDescriptor(ClassDescriptor):
def generateSchema(self): def generateSchema(self):
ClassDescriptor.generateSchema(self, configClass=True) ClassDescriptor.generateSchema(self, configClass=True)
class GroupClassDescriptor(ClassDescriptor):
'''Represents the class that corresponds to the Group for the generated
application.'''
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 = ['Group']
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 group
definition. We must then add the custom group elements in this
default Group descriptor.
NOTE: currently, it is not possible to define a custom Group
class.'''
self.orderedAttributes += attributes
self.klass = klass
self.customized = True
def isFolder(self, klass=None): return False
def generateSchema(self):
ClassDescriptor.generateSchema(self, configClass=True)
class TranslationClassDescriptor(ClassDescriptor): class TranslationClassDescriptor(ClassDescriptor):
'''Represents the set of translation ids for a gen-application.''' '''Represents the set of translation ids for a gen-application.'''

View file

@ -9,9 +9,8 @@ from appy.gen.po import PoMessage, PoFile, PoParser
from appy.gen.generator import Generator as AbstractGenerator from appy.gen.generator import Generator as AbstractGenerator
from appy.gen.utils import getClassName from appy.gen.utils import getClassName
from appy.gen.descriptors import WorkflowDescriptor from appy.gen.descriptors import WorkflowDescriptor
from descriptors import ClassDescriptor, ToolClassDescriptor, \ from descriptors import *
UserClassDescriptor, TranslationClassDescriptor from model import ModelClass, User, Group, Tool, Translation
from model import ModelClass, User, Tool, Translation
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Generator(AbstractGenerator): class Generator(AbstractGenerator):
@ -24,9 +23,10 @@ class Generator(AbstractGenerator):
AbstractGenerator.__init__(self, *args, **kwargs) AbstractGenerator.__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 and Translation instances # Create our own Tool, User, Group and Translation 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.translation = TranslationClassDescriptor(Translation, self) self.translation = TranslationClassDescriptor(Translation, self)
# i18n labels to generate # i18n labels to generate
self.labels = [] # i18n labels self.labels = [] # i18n labels
@ -132,6 +132,7 @@ class Generator(AbstractGenerator):
msg('pdf', '', msg.FORMAT_PDF), msg('pdf', '', msg.FORMAT_PDF),
msg('doc', '', msg.FORMAT_DOC), msg('doc', '', msg.FORMAT_DOC),
msg('rtf', '', msg.FORMAT_RTF), msg('rtf', '', msg.FORMAT_RTF),
msg('front_page_text', '', msg.FRONT_PAGE_TEXT),
] ]
# Create a label for every role added by this application # Create a label for every role added by this application
for role in self.getAllUsedRoles(): for role in self.getAllUsedRoles():
@ -283,6 +284,27 @@ class Generator(AbstractGenerator):
if isBack: res += '.back' if isBack: res += '.back'
return res return res
def getClasses(self, include=None):
'''Returns the descriptors for all the classes in the generated
gen-application. If p_include is:
* "all" it includes the descriptors for the config-related
classes (tool, user, group, translation)
* "allButTool" it includes the same descriptors, the tool excepted
* "custom" it includes descriptors for the config-related classes
for which the user has created a sub-class.'''
if not include: return self.classes
res = self.classes[:]
configClasses = [self.tool, self.user, self.group, self.translation]
if include == 'all':
res += configClasses
elif include == 'allButTool':
res += configClasses[1:]
elif include == 'custom':
res += [c for c in configClasses if c.customized]
elif include == 'predefined':
res = configClasses
return res
def generateConfigureZcml(self): def generateConfigureZcml(self):
'''Generates file configure.zcml.''' '''Generates file configure.zcml.'''
repls = self.repls.copy() repls = self.repls.copy()
@ -375,27 +397,6 @@ class Generator(AbstractGenerator):
repls['totalNumberOfTests'] = self.totalNumberOfTests repls['totalNumberOfTests'] = self.totalNumberOfTests
self.copyFile('__init__.py', repls) self.copyFile('__init__.py', repls)
def getClasses(self, include=None):
'''Returns the descriptors for all the classes in the generated
gen-application. If p_include is:
* "all" it includes the descriptors for the config-related
classes (tool, user, translation)
* "allButTool" it includes the same descriptors, the tool excepted
* "custom" it includes descriptors for the config-related classes
for which the user has created a sub-class.'''
if not include: return self.classes
res = self.classes[:]
configClasses = [self.tool, self.user, self.translation]
if include == 'all':
res += configClasses
elif include == 'allButTool':
res += configClasses[1:]
elif include == 'custom':
res += [c for c in configClasses if c.customized]
elif include == 'predefined':
res = configClasses
return res
def getClassesInOrder(self, allClasses): def getClassesInOrder(self, allClasses):
'''When generating wrappers, classes mut be dumped in order (else, it '''When generating wrappers, classes mut be dumped in order (else, it
generates forward references in the Python file, that does not generates forward references in the Python file, that does not
@ -478,13 +479,14 @@ class Generator(AbstractGenerator):
msg = Msg(self.tool.name, '', Msg.CONFIG % self.applicationName) msg = Msg(self.tool.name, '', Msg.CONFIG % self.applicationName)
self.labels.append(msg) self.labels.append(msg)
# Tune the Ref field between Tool and User # Tune the Ref field between Tool->User and Group->User
Tool.users.klass = User Tool.users.klass = User
if self.user.customized: if self.user.customized:
Tool.users.klass = self.user.klass Tool.users.klass = self.user.klass
Group.users.klass = self.user.klass
# Generate the Tool-related classes (User, Translation) # Generate the Tool-related classes (User, Group, Translation)
for klass in (self.user, self.translation): for klass in (self.user, self.group, self.translation):
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),
@ -492,8 +494,7 @@ class Generator(AbstractGenerator):
repls = self.repls.copy() repls = self.repls.copy()
repls.update({'methods': klass.methods, 'genClassName': klass.name, repls.update({'methods': klass.methods, 'genClassName': klass.name,
'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem', 'baseMixin':'BaseMixin', 'parents': 'BaseMixin, SimpleItem',
'classDoc': 'User class for %s' % self.applicationName, 'classDoc': 'Standard Appy class', 'icon':'object.gif'})
'icon':'object.gif'})
self.copyFile('Class.py', repls, destName='%s.py' % klass.name) self.copyFile('Class.py', 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

@ -85,28 +85,10 @@ class PloneInstaller:
site.invokeFactory(self.appyFolderType, self.productName, site.invokeFactory(self.appyFolderType, self.productName,
title=self.productName) title=self.productName)
getattr(site.portal_types, self.appyFolderType).global_allow = 0 getattr(site.portal_types, self.appyFolderType).global_allow = 0
# 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)
appFolder.manage_permission(permission, (), acquire=0)
else: else:
appFolder = getattr(site, self.productName) 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
allCreators = set()
for klass in self.appClasses:
i += 1
if not klass.__dict__.has_key('root') or not klass.__dict__['root']:
continue # It is not a root class
creators = getattr(klass, 'creators', None)
if not creators: creators = self.defaultAddRoles
allCreators = allCreators.union(creators)
className = self.appClassNames[i]
permission = self.getAddPermission(className)
updateRolesForPermission(permission, tuple(creators), appFolder)
# Beyond content-type-specific "add" permissions, creators must also # Beyond content-type-specific "add" permissions, creators must also
# have the main permission "Add portal content". # have the main permission "Add portal content".
permission = 'Add portal content' permission = 'Add portal content'
@ -273,7 +255,8 @@ class ZopeInstaller:
indexInfo = {'State': 'FieldIndex', 'UID': 'FieldIndex', indexInfo = {'State': 'FieldIndex', 'UID': 'FieldIndex',
'Title': 'TextIndex', 'SortableTitle': 'FieldIndex', 'Title': 'TextIndex', 'SortableTitle': 'FieldIndex',
'SearchableText': 'FieldIndex', 'Creator': 'FieldIndex', 'SearchableText': 'FieldIndex', 'Creator': 'FieldIndex',
'Created': 'DateIndex', 'ClassName': 'FieldIndex'} 'Created': 'DateIndex', 'ClassName': 'FieldIndex',
'Allowed': 'KeywordIndex'}
tool = self.app.config tool = self.app.config
for className in self.config.attributes.iterkeys(): for className in self.config.attributes.iterkeys():
wrapperClass = tool.getAppyClass(className, wrapper=True) wrapperClass = tool.getAppyClass(className, wrapper=True)
@ -284,22 +267,49 @@ class ZopeInstaller:
indexInfo[indexName] = appyType.getIndexType() indexInfo[indexName] = appyType.getIndexType()
self.installIndexes(indexInfo) self.installIndexes(indexInfo)
def getAddPermission(self, className):
'''What is the name of the permission allowing to create instances of
class whose name is p_className?'''
return self.productName + ': Add ' + className
def installBaseObjects(self): def installBaseObjects(self):
'''Creates the tool and the root data folder if they do not exist.''' '''Creates the tool and the root data folder if they do not exist.'''
# Create or update the base folder for storing data # Create or update the base folder for storing data
zopeContent = self.app.objectIds() zopeContent = self.app.objectIds()
if 'data' not in zopeContent: self.app.manage_addFolder('data')
if 'data' not in zopeContent:
self.app.manage_addFolder('data')
data = self.app.data
# Manager has been granted Add permissions for all root classes.
# This may not be desired, so remove this.
for className in self.config.rootClasses:
permission = self.getAddPermission(className)
data.manage_permission(permission, (), acquire=0)
# All roles defined as creators should be able to create the
# corresponding root classes in this folder.
i = -1
for klass in self.config.appClasses:
i += 1
if not klass.__dict__.has_key('root') or \
not klass.__dict__['root']:
continue # It is not a root class
creators = getattr(klass, 'creators', None)
if not creators: creators = self.config.defaultAddRoles
className = self.config.appClassNames[i]
permission = self.getAddPermission(className)
updateRolesForPermission(permission, tuple(creators), data)
if 'config' not in zopeContent: if 'config' not in zopeContent:
toolName = '%sTool' % self.productName toolName = '%sTool' % self.productName
createObject(self.app, 'config', toolName,self.productName,wf=False) createObject(self.app, 'config', toolName,self.productName,wf=False)
# Remove some default objects created by Zope but not useful # Remove some default objects created by Zope but not useful to Appy
for name in ('standard_html_footer', 'standard_html_header',\ for name in ('standard_html_footer', 'standard_html_header',\
'standard_template.pt'): 'standard_template.pt'):
if name in zopeContent: self.app.manage_delObjects([name]) if name in zopeContent: self.app.manage_delObjects([name])
def installTool(self): def installTool(self):
'''Updates the tool (now that the catalog is created) and updates its '''Updates the tool (now that the catalog is created) and updates its
inner objects (translations, documents).''' inner objects (users, groups, translations, documents).'''
tool = self.app.config tool = self.app.config
tool.createOrUpdate(True, None) tool.createOrUpdate(True, None)
tool.refreshSecurity() tool.refreshSecurity()
@ -307,10 +317,24 @@ class ZopeInstaller:
# Create the admin user if no user exists. # Create the admin user if no user exists.
if not self.app.acl_users.getUsers(): if not self.app.acl_users.getUsers():
appyTool.create('users', name='min', firstName='ad', self.app.acl_users._doAddUser('admin', 'admin', ['Manager'], ())
login='admin', password1='admin',
password2='admin', roles=['Manager'])
appyTool.log('Admin user "admin" created.') appyTool.log('Admin user "admin" created.')
# Create group "admins" if it does not exist
if not appyTool.count('Group', login='admins'):
appyTool.create('groups', login='admins', title='Administrators',
roles=['Manager'])
appyTool.log('Group "admins" created.')
# Create a group for every global role defined in the application
for role in self.config.applicationGlobalRoles:
relatedGroup = '%s_group' % role
if appyTool.count('Group', login=relatedGroup): continue
appyTool.create('groups', login=relatedGroup, title=relatedGroup,
roles=[role])
appyTool.log('Group "%s", related to global role "%s", was ' \
'created.' % (relatedGroup, role))
# Create POD templates within the tool if required # Create POD templates within the tool if required
for contentType in self.config.attributes.iterkeys(): for contentType in self.config.attributes.iterkeys():
appyClass = tool.getAppyClass(contentType) appyClass = tool.getAppyClass(contentType)
@ -426,9 +450,16 @@ class ZopeInstaller:
continue # Back refs are initialised within fw refs continue # Back refs are initialised within fw refs
appyType.init(name, baseClass, appName) appyType.init(name, baseClass, appName)
def installRoles(self):
'''Installs the application-specific roles if not already done.'''
roles = list(self.app.__ac_roles__)
for role in self.config.applicationRoles:
if role not in roles: roles.append(role)
self.app.__ac_roles__ = tuple(roles)
def install(self): def install(self):
self.logger.info('is being installed...') self.logger.info('is being installed...')
# Create the "admin" user if no user is present in the database self.installRoles()
self.installAppyTypes() self.installAppyTypes()
self.installZopeClasses() self.installZopeClasses()
self.enableUserTracking() self.enableUserTracking()

View file

@ -30,6 +30,7 @@ class ToolMixin(BaseMixin):
if res.find('Extensions_appyWrappers') != -1: if res.find('Extensions_appyWrappers') != -1:
elems = res.split('_') elems = res.split('_')
res = '%s%s' % (elems[1], elems[4]) res = '%s%s' % (elems[1], elems[4])
if res in ('User', 'Group', 'Translation'): res = appName + res
return res return res
def getCatalog(self): def getCatalog(self):
@ -212,6 +213,17 @@ class ToolMixin(BaseMixin):
if not appy: return res if not appy: return res
return res.appy() return res.appy()
def getAllowedValue(self):
'''Gets, for the currently logged user, the value for index
"Allowed".'''
user = self.getUser()
res = ['user:%s' % user.getId(), 'Anonymous'] + user.getRoles()
try:
res += ['user:%s' % g for g in user.groups.keys()]
except AttributeError, ae:
pass # The Zope admin does not have this attribute.
return res
def executeQuery(self, className, searchName=None, startNumber=0, def executeQuery(self, className, searchName=None, startNumber=0,
search=None, remember=False, brainsOnly=False, search=None, remember=False, brainsOnly=False,
maxResults=None, noSecurity=False, sortBy=None, maxResults=None, noSecurity=False, sortBy=None,
@ -303,10 +315,9 @@ class ToolMixin(BaseMixin):
if refObject: if refObject:
refField = refObject.getAppyType(refField) refField = refObject.getAppyType(refField)
params['UID'] = getattr(refObject, refField.name).data params['UID'] = getattr(refObject, refField.name).data
# Determine what method to call on the portal catalog # Use index "Allowed" if noSecurity is False
if noSecurity: catalogMethod = 'unrestrictedSearchResults' if not noSecurity: params['Allowed'] = self.getAllowedValue()
else: catalogMethod = 'searchResults' brains = self.getPath("/catalog")(**params)
exec 'brains = self.getPath("/catalog").%s(**params)' % catalogMethod
if brainsOnly: if brainsOnly:
# Return brains only. # Return brains only.
if not maxResults: return brains if not maxResults: return brains

View file

@ -87,6 +87,8 @@ class BaseMixin:
if field.type != 'Ref': continue if field.type != 'Ref': continue
for obj in field.getValue(self): for obj in field.getValue(self):
field.back.unlinkObject(obj, self, back=True) field.back.unlinkObject(obj, self, back=True)
# Uncatalog the object
self.reindex(unindex=True)
# Delete the object # Delete the object
self.getParentNode().manage_delObjects([self.id]) self.getParentNode().manage_delObjects([self.id])
@ -306,22 +308,12 @@ class BaseMixin:
url = self.absolute_url_path() url = self.absolute_url_path()
catalog = self.getPhysicalRoot().catalog catalog = self.getPhysicalRoot().catalog
if unindex: if unindex:
method = catalog.uncatalog_object catalog.uncatalog_object(url)
else: else:
method = catalog.catalog_object
if indexes: if indexes:
return method(self, url) catalog.catalog_object(self, url, idxs=indexes)
else: else:
return method(self, url, idxs=indexes) catalog.catalog_object(self, url)
def unindex(self, indexes=None):
'''Undatalog this object.'''
url = self.absolute_url_path()
catalog = self.getPhysicalRoot().catalog
if indexes:
return catalog.catalog_object(self, url)
else:
return catalog.catalog_object(self, url, idxs=indexes)
def say(self, msg, type='info'): def say(self, msg, type='info'):
'''Prints a p_msg in the user interface. p_logLevel may be "info", '''Prints a p_msg in the user interface. p_logLevel may be "info",
@ -1170,6 +1162,23 @@ class BaseMixin:
'''Returns the name of the (Zope) class for self.''' '''Returns the name of the (Zope) class for self.'''
return self.portal_type return self.portal_type
def Allowed(self):
'''Returns the list of roles and users that are allowed to view this
object. This index value will be used within catalog queries for
filtering objects the user is allowed to see.'''
res = set()
# Get, from the workflow, roles having permission 'View'.
for role in self.getProductConfig().rolesForPermissionOn('View', self):
res.add(role)
# Add users having, locally, this role on this object.
localRoles = getattr(self, '__ac_local_roles__', None)
if not localRoles: return list(res)
for id, roles in localRoles.iteritems():
for role in roles:
if role in res:
res.add('user:%s' % id)
return list(res)
def _appy_showState(self, workflow, stateShow): def _appy_showState(self, workflow, stateShow):
'''Must I show a state whose "show value" is p_stateShow?''' '''Must I show a state whose "show value" is p_stateShow?'''
if callable(stateShow): if callable(stateShow):
@ -1280,6 +1289,24 @@ class BaseMixin:
return nobody return nobody
return user return user
def getTool(self):
'''Returns the application tool.'''
return self.getPhysicalRoot().config
def getProductConfig(self):
'''Returns a reference to the config module.'''
return self.__class__.config
def index_html(self):
"""Redirects to /ui. Transfers the status message if any."""
rq = self.REQUEST
msg = rq.get('portal_status_message', '')
if msg:
url = self.getUrl(portal_status_message=msg)
else:
url = self.getUrl()
return rq.RESPONSE.redirect(url)
def userIsAnon(self): def userIsAnon(self):
'''Is the currently logged user anonymous ?''' '''Is the currently logged user anonymous ?'''
return self.getUser().getUserName() == 'Anonymous User' return self.getUser().getUserName() == 'Anonymous User'
@ -1406,22 +1433,7 @@ class BaseMixin:
def allows(self, permission): def allows(self, permission):
'''Has the logged user p_permission on p_self ?''' '''Has the logged user p_permission on p_self ?'''
# Get first the roles that have this permission on p_self. return self.getUser().has_permission(permission, self)
zopeAttr = Permission.getZopeAttrName(permission)
if not hasattr(self.aq_base, zopeAttr): return
allowedRoles = getattr(self.aq_base, zopeAttr)
# Has the user one of those roles?
user = self.getUser()
# XXX no groups at present
#ids = [user.getId()] + user.getGroups()
ids = [user.getId()]
userGlobalRoles = user.getRoles()
for role in allowedRoles:
# Has the user this role ? Check in the local roles first.
for id, roles in self.__ac_local_roles__.iteritems():
if (role in roles) and (id in ids): return True
# Check then in the global roles.
if role in userGlobalRoles: return True
def getEditorInit(self, name): def getEditorInit(self, name):
'''Gets the Javascrit init code for displaying a rich editor for '''Gets the Javascrit init code for displaying a rich editor for

View file

@ -8,6 +8,7 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import types import types
from appy.gen import * from appy.gen import *
Grp=Group # Avoid name clash between appy.gen.Group and class Group below
# Prototypical instances of every type ----------------------------------------- # Prototypical instances of every type -----------------------------------------
class Protos: class Protos:
@ -83,8 +84,8 @@ class ModelClass:
value = '%s.%s' % (moduleName, value.__name__) value = '%s.%s' % (moduleName, value.__name__)
elif isinstance(value, Selection): elif isinstance(value, Selection):
value = 'Selection("%s")' % value.methodName value = 'Selection("%s")' % value.methodName
elif isinstance(value, Group): elif isinstance(value, Grp):
value = 'Group("%s")' % value.name value = 'Grp("%s")' % value.name
elif isinstance(value, Page): elif isinstance(value, Page):
value = 'pages["%s"]' % value.name value = 'pages["%s"]' % value.name
elif callable(value): elif callable(value):
@ -149,6 +150,23 @@ class User(ModelClass):
gm['multiplicity'] = (0, None) gm['multiplicity'] = (0, None)
roles = String(validator=Selection('getGrantableRoles'), indexed=True, **gm) roles = String(validator=Selection('getGrantableRoles'), indexed=True, **gm)
# The Group class --------------------------------------------------------------
class Group(ModelClass):
# In a ModelClass we need to declare attributes in the following list.
_appy_attributes = ['title', 'login', 'roles', 'users']
# All methods defined below are fake. Real versions are in the wrapper.
m = {'group': 'main', 'width': 25, 'indexed': True}
title = String(multiplicity=(1,1), **m)
def showLogin(self): pass
def validateLogin(self): pass
login = String(show=showLogin, validator=validateLogin,
multiplicity=(1,1), **m)
roles = String(validator=Selection('getGrantableRoles'),
multiplicity=(0,None), **m)
users = Ref(User, multiplicity=(0,None), add=False, link=True,
back=Ref(attribute='groups', show=True),
showHeaders=True, shownInfo=('title', 'login'))
# The Translation class -------------------------------------------------------- # The Translation class --------------------------------------------------------
class Translation(ModelClass): class Translation(ModelClass):
_appy_attributes = ['po', 'title'] _appy_attributes = ['po', 'title']
@ -166,7 +184,7 @@ toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
'enableAdvancedSearch', 'numberOfSearchColumns', 'enableAdvancedSearch', 'numberOfSearchColumns',
'searchFields', 'optionalFields', 'showWorkflow', 'searchFields', 'optionalFields', 'showWorkflow',
'showWorkflowCommentField', 'showAllStatesInPhase') 'showWorkflowCommentField', 'showAllStatesInPhase')
defaultToolFields = ('users', 'translations', 'enableNotifications', defaultToolFields = ('users', 'groups', 'translations', 'enableNotifications',
'unoEnabledPython', 'openOfficePort', 'unoEnabledPython', 'openOfficePort',
'numberOfResultsPerPage', 'listBoxesMaximumWidth', 'numberOfResultsPerPage', 'listBoxesMaximumWidth',
'appyVersion', 'refreshSecurity') 'appyVersion', 'refreshSecurity')
@ -187,13 +205,20 @@ class Tool(ModelClass):
refreshSecurity = Action(action=refreshSecurity, confirm=True) refreshSecurity = Action(action=refreshSecurity, confirm=True)
# Ref(User) will maybe be transformed into Ref(CustomUserClass). # Ref(User) will maybe be transformed into Ref(CustomUserClass).
users = Ref(User, multiplicity=(0,None), add=True, link=False, users = Ref(User, multiplicity=(0,None), add=True, link=False,
back=Ref(attribute='toTool', show=False), page='users', back=Ref(attribute='toTool', show=False),
queryable=True, queryFields=('login',), showHeaders=True, page=Page('users', show='view'),
shownInfo=('login', 'title', 'roles')) queryable=True, queryFields=('title', 'login'),
showHeaders=True, shownInfo=('title', 'login', 'roles'))
groups = Ref(Group, multiplicity=(0,None), add=True, link=False,
back=Ref(attribute='toTool2', show=False),
page=Page('groups', show='view'),
queryable=True, queryFields=('title', 'login'),
showHeaders=True, shownInfo=('title', 'login', 'roles'))
translations = Ref(Translation, multiplicity=(0,None),add=False,link=False, translations = Ref(Translation, multiplicity=(0,None),add=False,link=False,
back=Ref(attribute='trToTool', show=False), show='view', back=Ref(attribute='trToTool', show=False), show='view',
page=Page('translations', show='view')) page=Page('translations', show='view'))
enableNotifications = Boolean(default=True, page='notifications') enableNotifications = Boolean(default=True,
page=Page('notifications', show=False))
@classmethod @classmethod
def _appy_clean(klass): def _appy_clean(klass):

View file

@ -23,21 +23,11 @@ class <!genClassName!>(<!parents!>):
global_allow = 1 global_allow = 1
icon = "ui/<!icon!>" icon = "ui/<!icon!>"
wrapperClass = Wrapper wrapperClass = Wrapper
for elem in dir(<!baseMixin!>): config = cfg
if not elem.startswith('__'): security.declarePublic(elem)
def getTool(self): return self.getPhysicalRoot().config
def getProductConfig(self): return cfg
def index_html(self):
"""Redirects to /ui. Transfers the status message if any."""
rq = self.REQUEST
msg = rq.get('portal_status_message', '')
if msg:
url = self.getUrl(portal_status_message=msg)
else:
url = self.getUrl()
return rq.RESPONSE.redirect(url)
def do(self): def do(self):
'''BaseMixin.do can't be traversed by Zope if this class is the tool. '''BaseMixin.do can't be traversed by Zope if this class is the tool.
So here, we redefine this method.''' So here, we redefine this method.'''
return BaseMixin.do(self) return BaseMixin.do(self)
for elem in dir(<!baseMixin!>):
if not elem.startswith('__'): security.declarePublic(elem)
<!methods!> <!methods!>

View file

@ -1,8 +1,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from appy.gen import * from appy.gen import *
Grp = Group # Avoid name clashes with the Group class below and appy.gen.Group
from appy.gen.plone25.wrappers import AbstractWrapper from appy.gen.plone25.wrappers import AbstractWrapper
from appy.gen.plone25.wrappers.ToolWrapper import ToolWrapper as WTool from appy.gen.plone25.wrappers.ToolWrapper import ToolWrapper as WTool
from appy.gen.plone25.wrappers.UserWrapper import UserWrapper as WUser from appy.gen.plone25.wrappers.UserWrapper import UserWrapper as WUser
from appy.gen.plone25.wrappers.GroupWrapper import GroupWrapper as WGroup
from appy.gen.plone25.wrappers.TranslationWrapper import TranslationWrapper as WT from appy.gen.plone25.wrappers.TranslationWrapper import TranslationWrapper as WT
from Globals import InitializeClass from Globals import InitializeClass
from AccessControl import ClassSecurityInfo from AccessControl import ClassSecurityInfo
@ -10,6 +12,7 @@ tfw = {"edit":"f","cell":"f","view":"f"} # Layout for Translation fields
<!imports!> <!imports!>
<!User!> <!User!>
<!Group!>
<!Translation!> <!Translation!>
<!Tool!> <!Tool!>
<!wrappers!> <!wrappers!>

View file

@ -14,6 +14,7 @@ from ZPublisher.HTTPRequest import BaseRequest
from OFS.Image import File from OFS.Image import File
from ZPublisher.HTTPRequest import FileUpload from ZPublisher.HTTPRequest import FileUpload
from AccessControl import getSecurityManager from AccessControl import getSecurityManager
from AccessControl.PermissionRole import rolesForPermissionOn
from DateTime import DateTime from DateTime import DateTime
from Products.ExternalMethod.ExternalMethod import ExternalMethod from Products.ExternalMethod.ExternalMethod import ExternalMethod
from Products.Transience.Transience import TransientObjectContainer from Products.Transience.Transience import TransientObjectContainer

View file

@ -0,0 +1,74 @@
# ------------------------------------------------------------------------------
from appy.gen.plone25.wrappers import AbstractWrapper
# ------------------------------------------------------------------------------
class GroupWrapper(AbstractWrapper):
def showLogin(self):
'''When must we show the login field?'''
if self.o.isTemporary(): return 'edit'
return 'view'
def validateLogin(self, login):
'''Is this p_login valid?'''
return True
def getGrantableRoles(self):
'''Returns the list of roles that the admin can grant to a user.'''
res = []
for role in self.o.getProductConfig().grantableRoles:
res.append( (role, self.translate('role_%s' % role)) )
return res
def validate(self, new, errors):
'''Inter-field validation.'''
return self._callCustom('validate', new, errors)
def confirm(self, new):
'''Use this method for remembering the previous list of users for this
group.'''
obj = self.o
if hasattr(obj.aq_base, '_oldUsers'): del obj.aq_base._oldUsers
obj._oldUsers = self.users
def addUser(self, user):
'''Adds a p_user to this group.'''
# Update the Ref field.
self.link('users', user)
# Update the group-related info on the Zope user.
zopeUser = user.getZopeUser()
zopeUser.groups[self.login] = self.roles
def removeUser(self, user):
'''Removes a p_user from this group.'''
self.unlink('users', user)
# Update the group-related info on the Zope user.
zopeUser = user.getZopeUser()
del zopeUser.groups[self.login]
def onEdit(self, created):
# Create or update, on every Zope user of this group, group-related
# information.
# 1. Remove reference to this group for users that were removed from it
newUsers = self.users
# The list of previously existing users does not exist when editing a
# group from Python. For updating self.users, it is recommended to use
# methods m_addUser and m_removeUser above.
oldUsers = getattr(self.o.aq_base, '_oldUsers', ())
for user in oldUsers:
if user not in newUsers:
del user.getZopeUser().groups[self.login]
self.log('User "%s" removed from group "%s".' % \
(user.login, self.login))
# 2. Add reference to this group for users that were added to it
for user in newUsers:
zopeUser = user.getZopeUser()
# We refresh group-related info on the Zope user even if the user
# was already in the group.
zopeUser.groups[self.login] = self.roles
if user not in oldUsers:
self.log('User "%s" added to group "%s".' % \
(user.login, self.login))
if hasattr(self.o.aq_base, '_oldUsers'): del self.o._oldUsers
return self._callCustom('onEdit', created)
# ------------------------------------------------------------------------------

View file

@ -12,24 +12,18 @@ class UserWrapper(AbstractWrapper):
def validateLogin(self, login): def validateLogin(self, login):
'''Is this p_login valid?''' '''Is this p_login valid?'''
# The login can't be the id of the whole site or "admin" # The login can't be the id of the whole site or "admin"
if (login == self.o.portal_url.getPortalObject().getId()) or \ if login == 'admin':
(login == 'admin'): return self.translate('This username is reserved.')
return self.translate(u'This username is reserved. Please choose ' \ # Check that no user or group already uses this login.
'a different name.', domain='plone') if self.count('User', login=login) or self.count('Group', login=login):
# Check that the login does not already exist and check some return self.translate('This login is already in use.')
# Plone-specific rules.
pr = self.o.portal_registration
if not pr.isMemberIdAllowed(login):
return self.translate(u'The login name you selected is already ' \
'in use or is not valid. Please choose another.', domain='plone')
return True return True
def validatePassword(self, password): def validatePassword(self, password):
'''Is this p_password valid?''' '''Is this p_password valid?'''
# Password must be at least 5 chars length # Password must be at least 5 chars length
if len(password) < 5: if len(password) < 5:
return self.translate(u'Passwords must contain at least 5 letters.', return self.translate('Passwords must contain at least 5 letters.')
domain='plone')
return True return True
def showPassword(self): def showPassword(self):
@ -49,7 +43,7 @@ class UserWrapper(AbstractWrapper):
page = self.request.get('page', 'main') page = self.request.get('page', 'main')
if page == 'main': if page == 'main':
if hasattr(new, 'password1') and (new.password1 != new.password2): if hasattr(new, 'password1') and (new.password1 != new.password2):
msg = self.translate(u'Passwords do not match.', domain='plone') msg = self.translate('Passwords do not match.')
errors.password1 = msg errors.password1 = msg
errors.password2 = msg errors.password2 = msg
return self._callCustom('validate', new, errors) return self._callCustom('validate', new, errors)
@ -61,41 +55,104 @@ class UserWrapper(AbstractWrapper):
if created: if created:
# Create the corresponding Zope user # Create the corresponding Zope user
aclUsers._doAddUser(login, self.password1, self.roles, ()) aclUsers._doAddUser(login, self.password1, self.roles, ())
zopeUser = aclUsers.getUser(login)
# Remove our own password copies # Remove our own password copies
self.password1 = self.password2 = '' self.password1 = self.password2 = ''
# Perform updates on the corresponding Plone user from persistent.mapping import PersistentMapping
# The following dict will store, for every group, global roles
# granted to it.
zopeUser.groups = PersistentMapping()
else:
# Updates roles at the Zope level.
zopeUser = aclUsers.getUserById(login) zopeUser = aclUsers.getUserById(login)
# This object must be owned by its Plone user zopeUser.roles = self.roles
# "self" must be owned by its Zope user
if 'Owner' not in self.o.get_local_roles_for_userid(login): if 'Owner' not in self.o.get_local_roles_for_userid(login):
self.o.manage_addLocalRoles(login, ('Owner',)) self.o.manage_addLocalRoles(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.
userRoles = self.roles
#userGroups = zopeUser.getGroups()
# for role in self.o.getProductConfig().grantableRoles:
# # Retrieve the group corresponding to this role
# groupName = '%s_group' % role
# if role == 'Manager': groupName = 'Administrators'
# elif role == 'Reviewer': groupName = 'Reviewers'
# group = self.o.portal_groups.getGroupById(groupName)
# # Add or remove the user from this group according to its role(s).
# if role in userRoles:
# # Add the user if not already present in the group
# if groupName not in userGroups:
# group.addMember(self.login)
# else:
# # Remove the user if it was in the corresponding group
# if groupName in userGroups:
# group.removeMember(self.login)
return self._callCustom('onEdit', created) return self._callCustom('onEdit', created)
def getZopeUser(self):
'''Gets the Zope user corresponding to this user.'''
return self.o.acl_users.getUser(self.login)
def onDelete(self): def onDelete(self):
'''Before deleting myself, I must delete the corresponding Plone '''Before deleting myself, I must delete the corresponding Zope user.'''
user.''' self.o.acl_users._doDelUsers([self.login])
# Delete the corresponding Plone user self.log('User "%s" deleted.' % self.login)
self.o.acl_users._doDelUser(self.login)
self.log('Plone user "%s" deleted.' % self.login)
# Call a custom "onDelete" if any. # Call a custom "onDelete" if any.
return self._callCustom('onDelete') return self._callCustom('onDelete')
# ------------------------------------------------------------------------------
try:
from AccessControl.PermissionRole import _what_not_even_god_should_do, \
rolesForPermissionOn
from Acquisition import aq_base
except ImportError:
pass # For those using Appy without Zope
class ZopeUserPatches:
'''This class is a fake one that defines Appy variants of some of Zope's
AccessControl.User methods. The idea is to implement the notion of group
of users.'''
def getRoles(self):
'''Returns the global roles that this user (or any of its groups)
possesses.'''
res = list(self.roles)
# Add group global roles
if not hasattr(aq_base(self), 'groups'): return res
for roles in self.groups.itervalues():
for role in roles:
if role not in res: res.append(role)
return res
def getRolesInContext(self, object):
'''Return the list of global and local (to p_object) roles granted to
this user (or to any of its groups).'''
object = getattr(object, 'aq_inner', object)
# Start with user global roles
res = self.getRoles()
# Add local roles
localRoles = getattr(object, '__ac_local_roles__', None)
if not localRoles: return res
userId = self.getId()
groups = getattr(self, 'groups', ())
for id, roles in localRoles.iteritems():
if (id != userId) and (id not in groups): continue
for role in roles: res.add(role)
return res
def allowed(self, object, object_roles=None):
'''Checks whether the user has access to p_object. The user (or one of
its groups) must have one of the roles in p_object_roles.'''
if object_roles is _what_not_even_god_should_do: return 0
# If "Anonymous" is among p_object_roles, grant access.
if (object_roles is None) or ('Anonymous' in object_roles): return 1
# If "Authenticated" is among p_object_roles, grant access if the user
# is not anonymous.
if 'Authenticated' in object_roles and \
(self.getUserName() != 'Anonymous User'):
if self._check_context(object): return 1
# Try first to grant access based on global user roles
for role in self.getRoles():
if role not in object_roles: continue
if self._check_context(object): return 1
return
# Try then to grant access based on local roles
innerObject = getattr(object, 'aq_inner', object)
localRoles = getattr(innerObject, '__ac_local_roles__', None)
if not localRoles: return
userId = self.getId()
groups = getattr(self, 'groups', ())
for id, roles in localRoles.iteritems():
if (id != userId) and (id not in groups): continue
for role in roles:
if role not in object_roles: continue
if self._check_context(object): return 1
return
from AccessControl.User import SimpleUser
SimpleUser.getRoles = getRoles
SimpleUser.getRolesInContext = getRolesInContext
SimpleUser.allowed = allowed
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -54,6 +54,8 @@ class AbstractWrapper(object):
return o.workflow_history[key] return o.workflow_history[key]
elif name == 'user': elif name == 'user':
return self.o.getUser() return self.o.getUser()
elif name == 'appyUser':
return self.search('User', login=self.o.getUser().getId())[0]
elif name == 'fields': return self.o.getAllAppyTypes() elif name == 'fields': return self.o.getAllAppyTypes()
# Now, let's try to return a real attribute. # Now, let's try to return a real attribute.
res = object.__getattribute__(self, name) res = object.__getattribute__(self, name)