[gen] Changed the way to customize the Config in an app.
This commit is contained in:
parent
88bd5e5bce
commit
8833f7b0ae
|
@ -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()
|
||||
|
|
|
@ -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']
|
||||
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
|
||||
languageSelector = False
|
||||
# People having one of these roles will be able to create instances
|
||||
# of classes defined in your application.
|
||||
self.defaultCreators = ['Manager', 'Owner']
|
||||
defaultCreators = ['Manager', 'Owner']
|
||||
# Number of translations for every page on a Translation object
|
||||
self.translationsPerPage = 30
|
||||
translationsPerPage = 30
|
||||
# Language that will be used as a basis for translating to other
|
||||
# languages.
|
||||
self.sourceLanguage = 'en'
|
||||
sourceLanguage = 'en'
|
||||
# Activate or not the button on home page for asking a new password
|
||||
self.activateForgotPassword = True
|
||||
activateForgotPassword = True
|
||||
# Enable session timeout?
|
||||
self.enableSessionTimeout = False
|
||||
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
|
||||
discreetLogin = False
|
||||
# When using Ogone, place an instance of appy.gen.ogone.OgoneConfig in
|
||||
# the field below.
|
||||
self.ogone = None
|
||||
ogone = None
|
||||
# When using Google analytics, specify here the Analytics ID
|
||||
self.googleAnalyticsId = None
|
||||
googleAnalyticsId = None
|
||||
# Create a group for every global role?
|
||||
self.groupsForGlobalRoles = False
|
||||
groupsForGlobalRoles = False
|
||||
# When using a LDAP for authenticating users, place an instance of class
|
||||
# LdapConfig above in the field below.
|
||||
ldap = None
|
||||
# ------------------------------------------------------------------------------
|
||||
|
|
|
@ -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,11 +200,13 @@ 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
|
||||
# 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
|
||||
|
@ -224,7 +220,7 @@ class Generator:
|
|||
moreAttrs.sort()
|
||||
if moreAttrs: attrs += moreAttrs
|
||||
# Add attributes added as back references
|
||||
if appyType == 'class':
|
||||
if genType == 'class':
|
||||
# Determine the class type (standard, tool, user...)
|
||||
if issubclass(moduleElem, gen.Tool):
|
||||
if not self.tool:
|
||||
|
@ -245,14 +241,12 @@ class Generator:
|
|||
# Manage classes containing tests
|
||||
if self.containsTests(moduleElem):
|
||||
self.modulesWithTests.add(module.__name__)
|
||||
elif appyType == 'workflow':
|
||||
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__)
|
||||
elif isinstance(moduleElem, gen.Config):
|
||||
self.config = moduleElem
|
||||
|
||||
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):
|
||||
|
|
|
@ -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
5
gen/ldap.py
Normal file
|
@ -0,0 +1,5 @@
|
|||
# ------------------------------------------------------------------------------
|
||||
def authenticate(login, password, ldapConfig, tool):
|
||||
'''Tries to authenticate user p_login in the LDAP.'''
|
||||
return
|
||||
# ------------------------------------------------------------------------------
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in a new issue