[gen] Changed the way to customize the Config in an app.

This commit is contained in:
Gaetan Delannay 2013-07-24 15:53:19 +02:00
parent 88bd5e5bce
commit 8833f7b0ae
12 changed files with 182 additions and 160 deletions

View file

@ -8,7 +8,7 @@ from appy.px import Px
class OgoneConfig:
'''If you plan, in your app, to perform on-line payments via the Ogone (r)
system, create an instance of this class in your app and place it in the
'ogone' attr of your appy.gen.Config instance.'''
'ogone' attr of your appy.gen.Config class.'''
def __init__(self):
# self.env refers to the Ogone environment and can be "test" or "prod".
self.env = 'test'
@ -103,7 +103,7 @@ class Ogone(Field):
necessary info for making the payment.'''
tool = obj.getTool()
# Basic Ogone parameters were generated in the app config module.
res = obj.getProductConfig().ogone.copy()
res = obj.getProductConfig(True).ogone.copy()
shaKey = res['shaInKey']
# Remove elements from the Ogone config that we must not send in the
# payment request.
@ -139,7 +139,7 @@ class Ogone(Field):
'''Returns True if the SHA-1 signature from Ogone matches retrieved
params.'''
response = obj.REQUEST.form
shaKey = obj.getProductConfig().ogone['shaOutKey']
shaKey = obj.getProductConfig(True).ogone['shaOutKey']
digest = self.createShaDigest(response, shaKey,
keysToIgnore=self.noShaOutKeys)
return digest.lower() == response['SHASIGN'].lower()

View file

@ -554,51 +554,63 @@ class Tool(Model):
class User(Model):
'''If you want to extend or modify the User class, subclass me.'''
# ------------------------------------------------------------------------------
class LdapConfig:
'''Parameters for authenticating users to an external LDAP.'''
server = '' # Name of the LDAP server
port = None # Port for this server.
# Login and password of the technical power user that the Appy application
# will use to connect to the LDAP.
adminLogin = ''
adminPassword = ''
# LDAP attribute to use as login for authenticating users.
loginAttribute = 'dn' # Can also be "mail", "sAMAccountName", "cn"
baseDn = '' # Base distinguished name where to find users in the LDAP.
# ------------------------------------------------------------------------------
class Config:
'''If you want to specify some configuration parameters for appy.gen and
your application, please create an instance of this class and modify its
attributes. You may put your instance anywhere in your application
(main package, sub-package, etc).'''
your application, please create a class named "Config" in the __init__.py
file of your application and override some of the attributes defined
here, ie:
# The default Config instance, used if the application does not give one.
defaultConfig = None
def getDefault():
if not Config.defaultConfig:
Config.defaultConfig = Config()
return Config.defaultConfig
getDefault = staticmethod(getDefault)
import appy.gen
class Config(appy.gen.Config):
langages = ('en', 'fr')
'''
def __init__(self):
# For every language code that you specify in this list, appy.gen will
# produce and maintain translation files.
self.languages = ['en']
# If languageSelector is True, on every page, a language selector will
# allow to switch between languages defined in self.languages. Else,
# the browser-defined language will be used for choosing the language
# of returned pages.
self.languageSelector = False
# People having one of these roles will be able to create instances
# of classes defined in your application.
self.defaultCreators = ['Manager', 'Owner']
# Number of translations for every page on a Translation object
self.translationsPerPage = 30
# Language that will be used as a basis for translating to other
# languages.
self.sourceLanguage = 'en'
# Activate or not the button on home page for asking a new password
self.activateForgotPassword = True
# Enable session timeout?
self.enableSessionTimeout = False
# If the following field is True, the login/password widget will be
# discreet. This is for sites where authentication is not foreseen for
# the majority of visitors (just for some administrators).
self.discreetLogin = False
# When using Ogone, place an instance of appy.gen.ogone.OgoneConfig in
# the field below.
self.ogone = None
# When using Google analytics, specify here the Analytics ID
self.googleAnalyticsId = None
# Create a group for every global role?
self.groupsForGlobalRoles = False
# For every language code that you specify in this list, appy.gen will
# produce and maintain translation files.
languages = ['en']
# If languageSelector is True, on every page, a language selector will
# allow to switch between languages defined in self.languages. Else,
# the browser-defined language will be used for choosing the language
# of returned pages.
languageSelector = False
# People having one of these roles will be able to create instances
# of classes defined in your application.
defaultCreators = ['Manager', 'Owner']
# Number of translations for every page on a Translation object
translationsPerPage = 30
# Language that will be used as a basis for translating to other
# languages.
sourceLanguage = 'en'
# Activate or not the button on home page for asking a new password
activateForgotPassword = True
# Enable session timeout?
enableSessionTimeout = False
# If the following field is True, the login/password widget will be
# discreet. This is for sites where authentication is not foreseen for
# the majority of visitors (just for some administrators).
discreetLogin = False
# When using Ogone, place an instance of appy.gen.ogone.OgoneConfig in
# the field below.
ogone = None
# When using Google analytics, specify here the Analytics ID
googleAnalyticsId = None
# Create a group for every global role?
groupsForGlobalRoles = False
# When using a LDAP for authenticating users, place an instance of class
# LdapConfig above in the field below.
ldap = None
# ------------------------------------------------------------------------------

View file

@ -146,29 +146,23 @@ class Generator:
self.user = None
self.workflows = []
self.initialize()
self.config = gen.Config.getDefault()
self.config = gen.Config
self.modulesWithTests = set()
self.totalNumberOfTests = 0
def determineAppyType(self, klass):
'''Is p_klass an Appy class ? An Appy workflow? None of this ?
If it (or a parent) declares at least one appy type definition,
it will be considered an Appy class. If it (or a parent) declares at
least one state definition, it will be considered an Appy
workflow.'''
res = 'none'
for attrValue in klass.__dict__.itervalues():
if isinstance(attrValue, gen.Field):
res = 'class'
elif isinstance(attrValue, gen.State):
res = 'workflow'
if not res:
for baseClass in klass.__bases__:
baseClassType = self.determineAppyType(baseClass)
if baseClassType != 'none':
res = baseClassType
break
return res
def determineGenType(self, klass):
'''If p_klass is:
* a gen-class, this method returns "class";
* a gen-workflow, this method it "workflow";
* none of it, this method returns None.
If p_klass declares at least one static attribute that is a
appy.fields.Field, it will be considered a gen-class. If p_klass
declares at least one static attribute that is a appy.gen.State,
it will be considered a gen-workflow.'''
for attr in klass.__dict__.itervalues():
if isinstance(attr, gen.Field): return 'class'
elif isinstance(attr, gen.State): return 'workflow'
def containsTests(self, moduleOrClass):
'''Returns True if p_moduleOrClass contains doctests. This method also
@ -206,53 +200,53 @@ class Generator:
# Find all classes in this module
for name in module.__dict__.keys():
exec 'moduleElem = module.%s' % name
if (type(moduleElem) == classType) and \
(moduleElem.__module__ == module.__name__):
# We have found a Python class definition in this module.
appyType = self.determineAppyType(moduleElem)
if appyType == 'none': continue
# Produce a list of static class attributes (in the order
# of their definition).
attrs = astClasses[moduleElem.__name__].attributes
# Collect non-parsable attrs = back references added
# programmatically
moreAttrs = []
for eName, eValue in moduleElem.__dict__.iteritems():
if isinstance(eValue, gen.Field) and (eName not in attrs):
moreAttrs.append(eName)
# Sort them in alphabetical order: else, order would be random
moreAttrs.sort()
if moreAttrs: attrs += moreAttrs
# Add attributes added as back references
if appyType == 'class':
# Determine the class type (standard, tool, user...)
if issubclass(moduleElem, gen.Tool):
if not self.tool:
klass = self.descriptorClasses['tool']
self.tool = klass(moduleElem, attrs, self)
else:
self.tool.update(moduleElem, attrs)
elif issubclass(moduleElem, gen.User):
if not self.user:
klass = self.descriptorClasses['user']
self.user = klass(moduleElem, attrs, self)
else:
self.user.update(moduleElem, attrs)
# Ignore non-classes module elements or classes that were imported
# from other modules.
if (type(moduleElem) != classType) or \
(moduleElem.__module__ != module.__name__): continue
# Ignore classes that are not gen-classes or gen-workflows.
genType = self.determineGenType(moduleElem)
if not genType: continue
# Produce a list of static class attributes (in the order
# of their definition).
attrs = astClasses[moduleElem.__name__].attributes
# Collect non-parsable attrs = back references added
# programmatically
moreAttrs = []
for eName, eValue in moduleElem.__dict__.iteritems():
if isinstance(eValue, gen.Field) and (eName not in attrs):
moreAttrs.append(eName)
# Sort them in alphabetical order: else, order would be random
moreAttrs.sort()
if moreAttrs: attrs += moreAttrs
# Add attributes added as back references
if genType == 'class':
# Determine the class type (standard, tool, user...)
if issubclass(moduleElem, gen.Tool):
if not self.tool:
klass = self.descriptorClasses['tool']
self.tool = klass(moduleElem, attrs, self)
else:
descriptorClass = self.descriptorClasses['class']
descriptor = descriptorClass(moduleElem,attrs, self)
self.classes.append(descriptor)
# Manage classes containing tests
if self.containsTests(moduleElem):
self.modulesWithTests.add(module.__name__)
elif appyType == 'workflow':
descriptorClass = self.descriptorClasses['workflow']
descriptor = descriptorClass(moduleElem, attrs, self)
self.workflows.append(descriptor)
if self.containsTests(moduleElem):
self.modulesWithTests.add(module.__name__)
elif isinstance(moduleElem, gen.Config):
self.config = moduleElem
self.tool.update(moduleElem, attrs)
elif issubclass(moduleElem, gen.User):
if not self.user:
klass = self.descriptorClasses['user']
self.user = klass(moduleElem, attrs, self)
else:
self.user.update(moduleElem, attrs)
else:
descriptorClass = self.descriptorClasses['class']
descriptor = descriptorClass(moduleElem,attrs, self)
self.classes.append(descriptor)
# Manage classes containing tests
if self.containsTests(moduleElem):
self.modulesWithTests.add(module.__name__)
elif genType == 'workflow':
descriptorClass = self.descriptorClasses['workflow']
descriptor = descriptorClass(moduleElem, attrs, self)
self.workflows.append(descriptor)
if self.containsTests(moduleElem):
self.modulesWithTests.add(module.__name__)
def walkApplication(self):
'''This method walks into the application and creates the corresponding
@ -262,6 +256,13 @@ class Generator:
sys.path.append(containingFolder)
# What is the name of the application ?
appName = os.path.basename(self.application)
# Get the app-specific config if any
exec 'import %s as appModule' % appName
if hasattr (appModule, 'Config'):
self.config = appModule.Config
if not issubclass(self.config, gen.Config):
raise Exception('Your Config class must subclass ' \
'appy.gen.Config.')
# Collect modules (only a the first level) in this application. Import
# them all, to be sure class definitions are complete (ie, back
# references are set from one class to the other). Moreover, potential
@ -556,9 +557,6 @@ class ZopeGenerator(Generator):
if theImport not in imports:
imports.append(theImport)
repls['imports'] = '\n'.join(imports)
# Compute default add roles
repls['defaultAddRoles'] = ','.join(
['"%s"' % r for r in self.config.defaultCreators])
# Compute list of add permissions
addPermissions = ''
for classDescr in classesAll:
@ -599,16 +597,6 @@ class ZopeGenerator(Generator):
repls['gRoles'] = ','.join(['"%s"' % r.name for r in globalRoles])
grantableRoles = self.getAllUsedRoles(local=False, grantable=True)
repls['grRoles'] = ','.join(['"%s"' % r.name for r in grantableRoles])
# Generate configuration options
repls['languages'] = ','.join('"%s"' % l for l in self.config.languages)
repls['languageSelector'] = self.config.languageSelector
repls['sourceLanguage'] = self.config.sourceLanguage
repls['enableSessionTimeout'] = self.config.enableSessionTimeout
repls['discreetLogin'] = self.config.discreetLogin
repls['ogone'] = repr(self.config.ogone)
repls['googleAnalyticsId'] = repr(self.config.googleAnalyticsId)
repls['activateForgotPassword'] = self.config.activateForgotPassword
repls['groupsForGlobalRoles'] = self.config.groupsForGlobalRoles
self.copyFile('config.pyt', repls, destName='config.py')
def generateInit(self):

View file

@ -75,7 +75,7 @@ class ZopeInstaller:
self.classes = classes
# Unwrap some useful config variables
self.productName = config.PROJECTNAME
self.languages = config.languages
self.languages = config.appConfig.languages
self.logger = config.logger
self.addContentPermissions = config.ADD_CONTENT_PERMISSIONS
@ -251,7 +251,7 @@ class ZopeInstaller:
# Create a group for every global role defined in the application
# (if required).
if self.app.config.getProductConfig().groupsForGlobalRoles:
if self.config.appConfig.groupsForGlobalRoles:
for role in self.config.applicationGlobalRoles:
groupId = role.lower()
if appyTool.count('Group', noSecurity=True, login=groupId):
@ -333,7 +333,7 @@ class ZopeInstaller:
# launching Zope in test mode, the temp folder does not exist.
if not hasattr(self.app, 'temp_folder'): return
sessionData = self.app.temp_folder.session_data
if self.config.enableSessionTimeout:
if self.config.appConfig.enableSessionTimeout:
sessionData.setDelNotificationTarget(onDelSession)
else:
sessionData.setDelNotificationTarget(None)

5
gen/ldap.py Normal file
View file

@ -0,0 +1,5 @@
# ------------------------------------------------------------------------------
def authenticate(login, password, ldapConfig, tool):
'''Tries to authenticate user p_login in the LDAP.'''
return
# ------------------------------------------------------------------------------

View file

@ -2,7 +2,7 @@
import os, os.path, sys, re, time, random, types, base64, urllib
from appy import Object
import appy.gen
from appy.gen import Search, String, Page
from appy.gen import Search, String, Page, ldap
from appy.gen.utils import SomeObjects, getClassName, GroupDescr, SearchDescr
from appy.gen.mixins import BaseMixin
from appy.gen.wrappers import AbstractWrapper
@ -139,6 +139,8 @@ class ToolMixin(BaseMixin):
'''Gets attribute named p_name.'''
if source == 'config':
obj = self.getProductConfig()
elif source == 'app':
obj = self.getProductConfig(True)
else:
obj = self.appy()
return getattr(obj, name, None)
@ -160,7 +162,7 @@ class ToolMixin(BaseMixin):
'''We must show the language selector if the app config requires it and
it there is more than 2 supported languages. Moreover, on some pages,
switching the language is not allowed.'''
cfg = self.getProductConfig()
cfg = self.getProductConfig(True)
if not cfg.languageSelector: return
if len(cfg.languages) < 2: return
page = self.REQUEST.get('ACTUAL_URL').split('/')[-1]
@ -168,11 +170,11 @@ class ToolMixin(BaseMixin):
def showForgotPassword(self):
'''We must show link "forgot password?" when the app requires it.'''
return self.getProductConfig().activateForgotPassword
return self.getProductConfig(True).activateForgotPassword
def getLanguages(self):
'''Returns the supported languages. First one is the default.'''
return self.getProductConfig().languages
return self.getProductConfig(True).languages
def getLanguageName(self, code):
'''Gets the language name (in this language) from a 2-chars language
@ -1005,27 +1007,43 @@ class ToolMixin(BaseMixin):
'''Returns the encrypted version of clear p_password.'''
return self.acl_users._encryptPassword(password)
def _zopeAuthenticate(self, request):
'''Performs the Zope-level authentication. Returns True if
authentication succeeds.'''
user = self.acl_users.validate(rq)
return not self.userIsAnon()
def _ldapAuthenticate(self, login, password):
'''Performs a LDAP-based authentication. Returns True if authentication
succeeds.'''
# Check if LDAP is configured.
ldapConfig = self.getProductConfig(True).ldap
if not ldapConfig: return
user = ldap.authenticate(login, password, ldapConfig, self)
if not user: return
return True
def performLogin(self):
'''Logs the user in.'''
rq = self.REQUEST
jsEnabled = rq.get('js_enabled', False) in ('1', 1)
cookiesEnabled = rq.get('cookies_enabled', False) in ('1', 1)
urlBack = rq['HTTP_REFERER']
if jsEnabled and not cookiesEnabled:
msg = self.translate('enable_cookies')
return self.goto(urlBack, msg)
# Perform the Zope-level authentication
# Extract the login and password
login = rq.get('__ac_name', '')
self._updateCookie(login, rq.get('__ac_password', ''))
user = self.acl_users.validate(rq)
if self.userIsAnon():
rq.RESPONSE.expireCookie('__ac', path='/')
msg = self.translate('login_ko')
logMsg = 'Authentication failed (tried with login "%s").' % login
else:
password = rq.get('__ac_password', '')
# Perform the Zope-level authentication
self._updateCookie(login, password)
if self._zopeAuthenticate(rq) or self._ldapAuthenticate(login,password):
msg = self.translate('login_ok')
logMsg = 'User "%s" logged in.' % login
else:
rq.RESPONSE.expireCookie('__ac', path='/')
msg = self.translate('login_ko')
logMsg = 'Authentication failed with login "%s".' % login
self.log(logMsg)
return self.goto(self.getApp().absolute_url(), msg)
@ -1303,7 +1321,7 @@ class ToolMixin(BaseMixin):
# Disable Google Analytics when we are in debug mode.
if self.isDebug(): return
# Disable Google Analytics if no ID is found in the config.
gaId = self.getProductConfig().googleAnalyticsId
gaId = self.getProductConfig(True).googleAnalyticsId
if not gaId: return
# Google Analytics must be enabled: return the chunk of Javascript
# code specified by Google.

View file

@ -1646,9 +1646,12 @@ class BaseMixin:
'''Returns the application tool.'''
return self.getPhysicalRoot().config
def getProductConfig(self):
'''Returns a reference to the config module.'''
return self.__class__.config
def getProductConfig(self, app=False):
'''Returns a reference to the config module. If p_app is True, it
returns the application config.'''
res = self.__class__.config
if app: res = res.appConfig
return res
def getParent(self):
'''If this object is stored within another one, this method returns it.

View file

@ -164,6 +164,9 @@ class User(ModelClass):
def showRoles(self): pass
roles = gen.String(show=showRoles, indexed=True,
validator=gen.Selection('getGrantableRoles'), **gm)
# Where is this user stored? By default, in the ZODB. But the user can be
# stored in an external LDAP.
source = gen.String(show=False, default='zodb', layouts='f', **gm)
# The Group class --------------------------------------------------------------
class Group(ModelClass):

View file

@ -24,7 +24,6 @@ logger = logging.getLogger('<!applicationName!>')
# Some global variables --------------------------------------------------------
PROJECTNAME = '<!applicationName!>'
diskFolder = os.path.dirname(<!applicationName!>.__file__)
defaultAddRoles = [<!defaultAddRoles!>]
ADD_CONTENT_PERMISSIONS = {
<!addPermissions!>}
@ -43,16 +42,10 @@ applicationRoles = [<!roles!>]
applicationGlobalRoles = [<!gRoles!>]
grantableRoles = [<!grRoles!>]
# Configuration options
languages = [<!languages!>]
languageSelector = <!languageSelector!>
sourceLanguage = '<!sourceLanguage!>'
activateForgotPassword = <!activateForgotPassword!>
enableSessionTimeout = <!enableSessionTimeout!>
discreetLogin = <!discreetLogin!>
ogone = <!ogone!>
googleAnalyticsId = <!googleAnalyticsId!>
groupsForGlobalRoles = <!groupsForGlobalRoles!>
try:
appConfig = <!applicationName!>.Config
except AttributeError:
appConfig = appy.gen.Config
# When Zope is starting or runs in test mode, there is no request object. We
# create here a fake one for storing Appy wrappers.

View file

@ -16,7 +16,7 @@
contextObj python: tool.getPublishedObject(layoutType) or tool.getHomeObject();
showPortlet python: tool.showPortlet(context, layoutType);
dir python: tool.getLanguageDirection(lang);
discreetLogin python: tool.getAttr('discreetLogin', source='config');
discreetLogin python: tool.getAttr('discreetLogin', source='app');
dleft python: (dir == 'ltr') and 'left' or 'right';
dright python: (dir == 'ltr') and 'right' or 'left';
x python: resp.setHeader('Content-type', 'text/html;;charset=UTF-8');

View file

@ -14,7 +14,7 @@ class TranslationWrapper(AbstractWrapper):
# either from the config object.
sourceLanguage = self.sourceLanguage
if not sourceLanguage:
sourceLanguage = self.o.getProductConfig().sourceLanguage
sourceLanguage = self.o.getProductConfig(True).sourceLanguage
sourceTranslation = getattr(tool.o, sourceLanguage).appy()
# p_field is the Computed field. We need to get the name of the
# corresponding field holding the translation message.
@ -43,7 +43,7 @@ class TranslationWrapper(AbstractWrapper):
if field.type == 'Computed': name = field.name[:-6]
else: name = field.name
# Get the source message
sourceLanguage = self.o.getProductConfig().sourceLanguage
sourceLanguage = self.o.getProductConfig(True).sourceLanguage
sourceTranslation = getattr(tool.o, sourceLanguage).appy()
sourceMsg = getattr(sourceTranslation, name)
if field.isEmptyValue(sourceMsg): return False

View file

@ -360,7 +360,7 @@ class AbstractWrapper(object):
obj = zobj and zobj.appy() or None;
showPortlet=ztool.showPortlet(zobj, layoutType);
dir=ztool.getLanguageDirection(lang);
discreetLogin=ztool.getAttr('discreetLogin', source='config');
discreetLogin=ztool.getProductConfig(True).discreetLogin;
dleft=(dir == 'ltr') and 'left' or 'right';
dright=(dir == 'ltr') and 'right' or 'left';
x=resp.setHeader('Content-type', ztool.xhtmlEncoding);
@ -879,7 +879,7 @@ class AbstractWrapper(object):
p_cfg is the product config that holds the default value.'''
res = klass._getParentAttr('creators')
# Return default creators if no creators was found.
if not res: res = cfg.defaultAddRoles
if not res: res = cfg.appConfig.defaultCreators
return res
@classmethod