appy.bin: backup.py: added field 'To' to mails sent by the backup procedure (so it not less directly considered as junk mail); bugfix in job.py used with Appy > 0.8; appy.gen: optimized performance (methods defined in 'show' attrs were called twice on edit.pt and view.pt); appy.gen: added String.richText allowing to have ckeditor with more text-formatting icons; added ckeditor 'show source' button by default (impossible to live without that); appy.gen: solved security-related problems; appy.gen.mail: allowto send mail as authenticated user; appy.gen: bugfixes in pages when rendered by IE.

This commit is contained in:
Gaetan Delannay 2012-05-05 17:04:19 +02:00
parent 459a714b76
commit 6245023365
21 changed files with 233 additions and 148 deletions

View file

@ -134,8 +134,8 @@ class ZodbBackuper:
'''Send content of self.logMem to self.emails.''' '''Send content of self.logMem to self.emails.'''
w = self.log w = self.log
subject = 'Backup notification.' subject = 'Backup notification.'
msg = 'From: %s\nSubject: %s\n\n%s' % (self.options.fromAddress, msg = 'From: %s\nTo: %s\nSubject: %s\n\n%s' % (self.options.fromAddress,
subject, self.logMem.getvalue()) self.emails, subject, self.logMem.getvalue())
try: try:
w('> Sending mail notifications to %s...' % self.emails) w('> Sending mail notifications to %s...' % self.emails)
smtpInfo = self.options.smtpServer.split(':', 3) smtpInfo = self.options.smtpServer.split(':', 3)
@ -151,6 +151,7 @@ class ZodbBackuper:
smtpServer.login(login, password) smtpServer.login(login, password)
res = smtpServer.sendmail(self.options.fromAddress, res = smtpServer.sendmail(self.options.fromAddress,
self.emails.split(','), msg) self.emails.split(','), msg)
smtpServer.quit()
if res: if res:
w('Could not send mail to some recipients. %s' % str(res)) w('Could not send mail to some recipients. %s' % str(res))
w('Done.') w('Done.')

View file

@ -67,7 +67,7 @@ else:
# If we are in a Appy application, the object on which we will call the # If we are in a Appy application, the object on which we will call the
# method is the config object on this root object. # method is the config object on this root object.
if not appName: if not appName:
targetObject = rootObject.data.appy() targetObject = rootObject.config.appy()
elif not appName.startswith('path='): elif not appName.startswith('path='):
objectName = 'portal_%s' % appName.lower() objectName = 'portal_%s' % appName.lower()
targetObject = getattr(rootObject, objectName).appy() targetObject = getattr(rootObject, objectName).appy()

View file

@ -356,6 +356,12 @@ class Search:
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Type: class Type:
'''Basic abstract class for defining any appy type.''' '''Basic abstract class for defining any appy type.'''
# Those attributes can be overridden by subclasses for defining,
# respectively, names of CSS and Javascript files that are required by this
# field, keyed by layoutType.
cssFiles = {}
jsFiles = {}
def __init__(self, validator, multiplicity, index, default, optional, def __init__(self, validator, multiplicity, index, default, optional,
editDefault, show, page, group, layouts, move, indexed, editDefault, show, page, group, layouts, move, indexed,
searchable, specificReadPermission, specificWritePermission, searchable, specificReadPermission, specificWritePermission,
@ -723,13 +729,25 @@ class Type:
res = copy.deepcopy(defaultFieldLayouts) res = copy.deepcopy(defaultFieldLayouts)
return res return res
def getCss(self, layoutType): def getCss(self, layoutType, res):
'''This method returns a list of CSS files that are required for '''This method completes the list p_res with the names of CSS files
displaying widgets of self's type on a given p_layoutType.''' that are required for displaying widgets of self's type on a given
p_layoutType. p_res is not a set because order of inclusion of CSS
files may be important and may be loosed by using sets.'''
if layoutType in self.cssFiles:
for fileName in self.cssFiles[layoutType]:
if fileName not in res:
res.append(fileName)
def getJs(self, layoutType): def getJs(self, layoutType, res):
'''This method returns a list of Javascript files that are required for '''This method completes the list p_res with the names of Javascript
displaying widgets of self's type on a given p_layoutType.''' files that are required for displaying widgets of self's type on a
given p_layoutType. p_res is not a set because order of inclusion of
CSS files may be important and may be loosed by using sets.'''
if layoutType in self.jsFiles:
for fileName in self.jsFiles[layoutType]:
if fileName not in res:
res.append(fileName)
def getValue(self, obj): def getValue(self, obj):
'''Gets, on_obj, the value conforming to self's type definition.''' '''Gets, on_obj, the value conforming to self's type definition.'''
@ -1025,6 +1043,9 @@ class Float(Type):
return self.pythonType(value) return self.pythonType(value)
class String(Type): class String(Type):
# Javascript files sometimes required by this type
jsFiles = {'edit': ('ckeditor/ckeditor.js',)}
# Some predefined regular expressions that may be used as validators # Some predefined regular expressions that may be used as validators
c = re.compile c = re.compile
EMAIL = c('[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.' \ EMAIL = c('[a-zA-Z][\w\.-]*[a-zA-Z0-9]@[a-zA-Z0-9][\w\.-]*[a-zA-Z0-9]\.' \
@ -1132,7 +1153,7 @@ class String(Type):
maxChars=None, colspan=1, master=None, masterValue=None, maxChars=None, colspan=1, master=None, masterValue=None,
focus=False, historized=False, mapping=None, label=None, focus=False, historized=False, mapping=None, label=None,
transform='none', styles=('p','h1','h2','h3','h4'), transform='none', styles=('p','h1','h2','h3','h4'),
allowImageUpload=True): allowImageUpload=True, richText=False):
# According to format, the widget will be different: input field, # According to format, the widget will be different: input field,
# textarea, inline editor... Note that there can be only one String # textarea, inline editor... Note that there can be only one String
# field of format CAPTCHA by page, because the captcha challenge is # field of format CAPTCHA by page, because the captcha challenge is
@ -1141,6 +1162,11 @@ class String(Type):
# When format is XHTML, the list of styles that the user will be able to # When format is XHTML, the list of styles that the user will be able to
# select in the styles dropdown is defined hereafter. # select in the styles dropdown is defined hereafter.
self.styles = styles self.styles = styles
# When richText is True, we show to the user icons in ckeditor allowing
# him to tailor text appearance, color, size, etc. While this may be an
# option if the objective is to edit web pages, this may not be desired
# for producing standardized, pod-print-ready documents.
self.richText = richText
# When format is XHTML, do we allow the user to upload images in it ? # When format is XHTML, do we allow the user to upload images in it ?
self.allowImageUpload = allowImageUpload self.allowImageUpload = allowImageUpload
# The following field has a direct impact on the text entered by the # The following field has a direct impact on the text entered by the
@ -1212,7 +1238,8 @@ class String(Type):
# When image upload is allowed, ckeditor inserts some "style" attrs # When image upload is allowed, ckeditor inserts some "style" attrs
# (ie for image size when images are resized). So in this case we # (ie for image size when images are resized). So in this case we
# can't remove style-related information. # can't remove style-related information.
value = cleanXhtml(value, keepStyles=self.allowImageUpload) keepStyles = self.allowImageUpload or self.richText
value = cleanXhtml(value, keepStyles=keepStyles)
Type.store(self, obj, value) Type.store(self, obj, value)
def getFormattedValue(self, obj, value): def getFormattedValue(self, obj, value):
@ -1379,9 +1406,8 @@ class String(Type):
return 'ZCTextIndex' return 'ZCTextIndex'
return Type.getIndexType(self) return Type.getIndexType(self)
def getJs(self, layoutType): def getJs(self, layoutType, res):
if (layoutType == 'edit') and (self.format == String.XHTML): if self.format == String.XHTML: Type.getJs(self, layoutType, res)
return ('ckeditor/ckeditor.js',)
def getCaptchaChallenge(self, session): def getCaptchaChallenge(self, session):
'''Returns a Captcha challenge in the form of a dict. At key "text", '''Returns a Captcha challenge in the form of a dict. At key "text",
@ -1445,6 +1471,11 @@ class Boolean(Type):
return res return res
class Date(Type): class Date(Type):
# Required CSS and Javascript files for this type.
cssFiles = {'edit': ('jscalendar/calendar-blue.css',)}
jsFiles = {'edit': ('jscalendar/calendar.js',
'jscalendar/lang/calendar-en.js',
'jscalendar/calendar-setup.js')}
# Possible values for "format" # Possible values for "format"
WITH_HOUR = 0 WITH_HOUR = 0
WITHOUT_HOUR = 1 WITHOUT_HOUR = 1
@ -1474,14 +1505,13 @@ class Date(Type):
master, masterValue, focus, historized, True, mapping, master, masterValue, focus, historized, True, mapping,
label) label)
def getCss(self, layoutType): def getCss(self, layoutType, res):
if (layoutType == 'edit') and self.calendar: # CSS files are only required if the calendar must be shown.
return ('jscalendar/calendar-blue.css',) if self.calendar: Type.getCss(self, layoutType, res)
def getJs(self, layoutType): def getJs(self, layoutType, res):
if (layoutType == 'edit') and self.calendar: # Javascript files are only required if the calendar must be shown.
return ('jscalendar/calendar.js', 'jscalendar/lang/calendar-en.js', if self.calendar: Type.getJs(self, layoutType, res)
'jscalendar/calendar-setup.js')
def getSelectableYears(self): def getSelectableYears(self):
'''Gets the list of years one may select for this field.''' '''Gets the list of years one may select for this field.'''
@ -2372,21 +2402,15 @@ class List(Type):
if i >= len(outerValue): return '' if i >= len(outerValue): return ''
return getattr(outerValue[i], name, '') return getattr(outerValue[i], name, '')
def getCss(self, layoutType): def getCss(self, layoutType, res):
'''Gets the CSS required by sub-fields if any.''' '''Gets the CSS required by sub-fields if any.'''
res = ()
for name, field in self.fields: for name, field in self.fields:
css = field.getCss(layoutType) field.getCss(layoutType, res)
if css: res += css
return res
def getJs(self, layoutType): def getJs(self, layoutType, res):
'''Gets the JS required by sub-fields if any.''' '''Gets the JS required by sub-fields if any.'''
res = ()
for name, field in self.fields: for name, field in self.fields:
js = field.getJs(layoutType) field.getJs(layoutType, res)
if js: res += js
return res
# Workflow-specific types and default workflows -------------------------------- # Workflow-specific types and default workflows --------------------------------
appyToZopePermissions = { appyToZopePermissions = {
@ -2768,6 +2792,14 @@ class WorkflowAuthenticated:
initial=True) initial=True)
WorkflowAuthenticated.__instance__ = WorkflowAuthenticated() WorkflowAuthenticated.__instance__ = WorkflowAuthenticated()
class WorkflowOwner:
'''One-state workflow allowing only manager and owner to consult and
edit.'''
mgr = 'Manager'
o = 'Owner'
active = State({r:(mgr, o), w:(mgr, o), d:mgr}, initial=True)
WorkflowOwner.__instance__ = WorkflowOwner()
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class Selection: class Selection:
'''Instances of this class may be given as validator of a String, in order '''Instances of this class may be given as validator of a String, in order

View file

@ -267,7 +267,7 @@ class ZopeInstaller:
appyTool.log('Admin user "admin" created.') appyTool.log('Admin user "admin" created.')
# Create group "admins" if it does not exist # Create group "admins" if it does not exist
if not appyTool.count('Group', login='admins'): if not appyTool.count('Group', noSecurity=True, login='admins'):
appyTool.create('groups', login='admins', title='Administrators', appyTool.create('groups', login='admins', title='Administrators',
roles=['Manager']) roles=['Manager'])
appyTool.log('Group "admins" created.') appyTool.log('Group "admins" created.')
@ -275,7 +275,8 @@ class ZopeInstaller:
# Create a group for every global role defined in the application # Create a group for every global role defined in the application
for role in self.config.applicationGlobalRoles: for role in self.config.applicationGlobalRoles:
relatedGroup = '%s_group' % role relatedGroup = '%s_group' % role
if appyTool.count('Group', login=relatedGroup): continue if appyTool.count('Group', noSecurity=True, login=relatedGroup):
continue
appyTool.create('groups', login=relatedGroup, title=relatedGroup, appyTool.create('groups', login=relatedGroup, title=relatedGroup,
roles=[role]) roles=[role])
appyTool.log('Group "%s", related to global role "%s", was ' \ appyTool.log('Group "%s", related to global role "%s", was ' \

View file

@ -13,9 +13,13 @@ def sendMail(tool, to, subject, body, attachments=None):
list of email addresses).''' list of email addresses).'''
# Just log things if mail is disabled # Just log things if mail is disabled
fromAddress = tool.mailFrom fromAddress = tool.mailFrom
if not tool.mailEnabled: if not tool.mailEnabled or not tool.mailHost:
tool.log('Mail disabled: should send mail from %s to %s.' % \ if not tool.mailHost:
(fromAddress, str(to))) msg = ' (no mailhost defined)'
else:
msg = ''
tool.log('Mail disabled%s: should send mail from %s to %s.' % \
(msg, fromAddress, str(to)))
tool.log('Subject: %s' % subject) tool.log('Subject: %s' % subject)
tool.log('Body: %s' % body) tool.log('Body: %s' % body)
if attachments: if attachments:
@ -65,11 +69,24 @@ def sendMail(tool, to, subject, body, attachments=None):
msg.attach(part) msg.attach(part)
# Send the email # Send the email
try: try:
mh = smtplib.SMTP(tool.mailHost) smtpInfo = tool.mailHost.split(':', 3)
mh.sendmail(fromAddress, [to], msg.as_string()) login = password = None
mh.quit() if len(smtpInfo) == 2:
# We simply have server and port
server, port = smtpInfo
else:
# We also have login and password
server, port, login, password = smtpInfo
smtpServer = smtplib.SMTP(server, port=int(port))
if login:
smtpServer.login(login, password)
res = smtpServer.sendmail(fromAddress, [to], msg.as_string())
smtpServer.quit()
if res:
tool.log('Could not send mail to some recipients. %s' % str(res),
type='warning')
except smtplib.SMTPException, e: except smtplib.SMTPException, e:
tool.log('Mail sending failed: %s' % str(e)) tool.log('Mail sending failed: %s' % str(e), type='error')
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
def sendNotification(obj, transition, transitionName, workflow): def sendNotification(obj, transition, transitionName, workflow):

View file

@ -43,9 +43,10 @@ class Migrator:
# Manage groups. Exclude not-used default Plone groups. # Manage groups. Exclude not-used default Plone groups.
for groupId in user.getGroups(): for groupId in user.getGroups():
if groupId in self.bypassGroups: continue if groupId in self.bypassGroups: continue
if tool.count('Group', login=groupId): if tool.count('Group', noSecurity=True, login=groupId):
# The Appy group already exists, get it # The Appy group already exists, get it
appyGroup = tool.search('Group', login=groupId)[0] appyGroup = tool.search('Group', noSecurity=True,
login=groupId)[0]
else: else:
# Create the group. Todo: get Plone group roles and title # Create the group. Todo: get Plone group roles and title
appyGroup = tool.create('groups', login=groupId, appyGroup = tool.create('groups', login=groupId,

View file

@ -981,7 +981,8 @@ class ToolMixin(BaseMixin):
if rolesToShow: if rolesToShow:
res.append(', '.join([self.translate('role_%s'%r) \ res.append(', '.join([self.translate('role_%s'%r) \
for r in rolesToShow])) for r in rolesToShow]))
return (' | '.join(res), appyUser.o.getUrl(mode='edit')) return (' | '.join(res), appyUser.o.getUrl(mode='edit', page='main',
nav=''))
def generateUid(self, className): def generateUid(self, className):
'''Generates a UID for an instance of p_className.''' '''Generates a UID for an instance of p_className.'''

View file

@ -589,11 +589,15 @@ class BaseMixin:
klass = self.getTool().getAppyClass(className, wrapper=True) klass = self.getTool().getAppyClass(className, wrapper=True)
return klass.__fields__ return klass.__fields__
def getGroupedAppyTypes(self, layoutType, pageName): def getGroupedAppyTypes(self, layoutType, pageName, cssJs=None):
'''Returns the fields sorted by group. For every field, the appyType '''Returns the fields sorted by group. For every field, the appyType
(dict version) is given.''' (dict version) is given.'''
res = [] res = []
groups = {} # The already encountered groups groups = {} # The already encountered groups
# If a dict is given in p_cssJs, we must fill it with the CSS and JS
# files required for every returned appyType.
collectCssJs = isinstance(cssJs, dict)
css = js = None
# If param "refresh" is there, we must reload the Python class # If param "refresh" is there, we must reload the Python class
refresh = ('refresh' in self.REQUEST) refresh = ('refresh' in self.REQUEST)
if refresh: if refresh:
@ -602,6 +606,11 @@ class BaseMixin:
if refresh: appyType = appyType.reload(klass, self) if refresh: appyType = appyType.reload(klass, self)
if appyType.page.name != pageName: continue if appyType.page.name != pageName: continue
if not appyType.isShowable(self, layoutType): continue if not appyType.isShowable(self, layoutType): continue
if collectCssJs:
if css == None: css = []
appyType.getCss(layoutType, css)
if js == None: js = []
appyType.getJs(layoutType, js)
if not appyType.group: if not appyType.group:
res.append(appyType.__dict__) res.append(appyType.__dict__)
else: else:
@ -610,6 +619,9 @@ class BaseMixin:
groupDescr = appyType.group.insertInto(res, groups, groupDescr = appyType.group.insertInto(res, groups,
appyType.page, self.meta_type) appyType.page, self.meta_type)
GroupDescr.addWidget(groupDescr, appyType.__dict__) GroupDescr.addWidget(groupDescr, appyType.__dict__)
if collectCssJs:
cssJs['css'] = css
cssJs['js'] = js
return res return res
def getAppyTypes(self, layoutType, pageName): def getAppyTypes(self, layoutType, pageName):
@ -622,23 +634,19 @@ class BaseMixin:
res.append(appyType) res.append(appyType)
return res return res
def getCssJs(self, fields, layoutType): def getCssJs(self, fields, layoutType, res):
'''Gets the list of Javascript and CSS files required by Appy types '''Gets, in p_res ~{'css':[s_css], 'js':[s_js]}~ the lists of
p_fields when shown on p_layoutType.''' Javascript and CSS files required by Appy types p_fields when shown
on p_layoutType.'''
# Lists css and js below are not sets, because order of Javascript # Lists css and js below are not sets, because order of Javascript
# inclusion can be important, and this could be losed by using sets. # inclusion can be important, and this could be losed by using sets.
css = [] css = []
js = [] js = []
for field in fields: for field in fields:
fieldCss = field.getCss(layoutType) field.getCss(layoutType, css)
if fieldCss: field.getJs(layoutType, js)
for fcss in fieldCss: res['css'] = css
if fcss not in css: css.append(fcss) res['js'] = js
fieldJs = field.getJs(layoutType)
if fieldJs:
for fjs in fieldJs:
if fjs not in js: js.append(fjs)
return {'css':css, 'js':js}
def getAppyTypesFromNames(self, fieldNames, asDict=True): def getAppyTypesFromNames(self, fieldNames, asDict=True):
'''Gets the Appy types named p_fieldNames.''' '''Gets the Appy types named p_fieldNames.'''
@ -874,7 +882,8 @@ class BaseMixin:
else: else:
appyClass = self.getTool().getAppyClass(className) appyClass = self.getTool().getAppyClass(className)
if hasattr(appyClass, 'workflow'): wf = appyClass.workflow if hasattr(appyClass, 'workflow'): wf = appyClass.workflow
else: wf = gen.WorkflowAnonymous else:
wf = gen.WorkflowAnonymous
if not name: return wf if not name: return wf
return WorkflowDescriptor.getWorkflowName(wf) return WorkflowDescriptor.getWorkflowName(wf)
@ -1495,7 +1504,7 @@ class BaseMixin:
# Define the attributes that will initialize the ckeditor instance for # Define the attributes that will initialize the ckeditor instance for
# this field. # this field.
field = self.getAppyType(name) field = self.getAppyType(name)
ckAttrs = {'toolbar': 'Appy', ckAttrs = {'toolbar': field.richText and 'AppyRich' or 'Appy',
'format_tags': '%s' % ';'.join(field.styles)} 'format_tags': '%s' % ';'.join(field.styles)}
if field.allowImageUpload: if field.allowImageUpload:
ckAttrs['filebrowserUploadUrl'] = '%s/upload' % self.absolute_url() ckAttrs['filebrowserUploadUrl'] = '%s/upload' % self.absolute_url()

View file

@ -125,6 +125,8 @@ class ModelClass:
# 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
elif callable(pageShow):
pageShow = '%s.%s' % (wrapperName, pageShow.__name__)
res += '"%s":Pge("%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
@ -194,7 +196,7 @@ class Page(ModelClass):
_appy_attributes = ['title', 'content', 'pages'] _appy_attributes = ['title', 'content', 'pages']
folder = True folder = True
title = gen.String(show='edit', indexed=True) title = gen.String(show='edit', indexed=True)
content = gen.String(format=gen.String.XHTML, layouts='f') content = gen.String(format=gen.String.XHTML, layouts='f', richText=True)
# Pages can contain other pages. # Pages can contain other pages.
def showSubPages(self): pass def showSubPages(self): pass
pages = gen.Ref(None, multiplicity=(0,None), add=True, link=False, pages = gen.Ref(None, multiplicity=(0,None), add=True, link=False,
@ -209,10 +211,10 @@ toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns',
'enableAdvancedSearch', 'numberOfSearchColumns', 'enableAdvancedSearch', 'numberOfSearchColumns',
'searchFields', 'optionalFields', 'showWorkflow', 'searchFields', 'optionalFields', 'showWorkflow',
'showAllStatesInPhase') 'showAllStatesInPhase')
defaultToolFields = ('title', 'unoEnabledPython','openOfficePort', defaultToolFields = ('title', 'mailHost', 'mailEnabled', 'mailFrom',
'numberOfResultsPerPage', 'mailHost', 'mailEnabled', 'appyVersion', 'users', 'groups', 'translations', 'pages',
'mailFrom', 'appyVersion', 'users', 'groups', 'unoEnabledPython','openOfficePort',
'translations', 'pages') 'numberOfResultsPerPage')
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.
@ -220,33 +222,40 @@ class Tool(ModelClass):
folder = True folder = True
# Tool attributes # Tool attributes
def isManager(self): pass
title = gen.String(show=False, page=gen.Page('main', show=False)) title = gen.String(show=False, page=gen.Page('main', show=False))
def validPythonWithUno(self, value): pass # Real method in the wrapper
unoEnabledPython = gen.String(validator=validPythonWithUno)
openOfficePort = gen.Integer(default=2002)
numberOfResultsPerPage = gen.Integer(default=30)
mailHost = gen.String(default='localhost:25') mailHost = gen.String(default='localhost:25')
mailEnabled = gen.Boolean(default=False) mailEnabled = gen.Boolean(default=False)
mailFrom = gen.String(default='info@appyframework.org') mailFrom = gen.String(default='info@appyframework.org')
appyVersion = gen.String(show=False, layouts='f') appyVersion = gen.String(layouts='f')
# Ref(User) will maybe be transformed into Ref(CustomUserClass). # Ref(User) will maybe be transformed into Ref(CustomUserClass).
users = gen.Ref(User, multiplicity=(0,None), add=True, link=False, users = gen.Ref(User, multiplicity=(0,None), add=True, link=False,
back=gen.Ref(attribute='toTool', show=False), back=gen.Ref(attribute='toTool', show=False),
page=gen.Page('users', show='view'), page=gen.Page('users', show=isManager),
queryable=True, queryFields=('title', 'login'), queryable=True, queryFields=('title', 'login'),
showHeaders=True, shownInfo=('title', 'login', 'roles')) showHeaders=True, shownInfo=('title', 'login', 'roles'))
groups = gen.Ref(Group, multiplicity=(0,None), add=True, link=False, groups = gen.Ref(Group, multiplicity=(0,None), add=True, link=False,
back=gen.Ref(attribute='toTool2', show=False), back=gen.Ref(attribute='toTool2', show=False),
page=gen.Page('groups', show='view'), page=gen.Page('groups', show=isManager),
queryable=True, queryFields=('title', 'login'), queryable=True, queryFields=('title', 'login'),
showHeaders=True, shownInfo=('title', 'login', 'roles')) showHeaders=True, shownInfo=('title', 'login', 'roles'))
translations = gen.Ref(Translation, multiplicity=(0,None), add=False, translations = gen.Ref(Translation, multiplicity=(0,None), add=False,
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=isManager))
pages = gen.Ref(Page, multiplicity=(0,None), add=True, link=False, pages = gen.Ref(Page, multiplicity=(0,None), add=True, link=False,
show='view', back=gen.Ref(attribute='toTool3', show=False), show='view', back=gen.Ref(attribute='toTool3', show=False),
page=gen.Page('pages', show='view')) page=gen.Page('pages', show=isManager))
# Document generation page
dgp = {'page': gen.Page('documentGeneration', show=isManager)}
def validPythonWithUno(self, value): pass # Real method in the wrapper
unoEnabledPython = gen.String(show=False,validator=validPythonWithUno,**dgp)
openOfficePort = gen.Integer(default=2002, show=False, **dgp)
# User interface page
numberOfResultsPerPage = gen.Integer(default=30,
page=gen.Page('userInterface', show=False))
@classmethod @classmethod
def _appy_clean(klass): def _appy_clean(klass):

View file

@ -64,7 +64,7 @@ img {border: 0}
.portletSearch { font-size: 90%; font-style: italic; padding-left: 1em} .portletSearch { font-size: 90%; font-style: italic; padding-left: 1em}
.phase { border-style: dashed; border-width: thin; padding: 4px 0.6em 5px 1em;} .phase { border-style: dashed; border-width: thin; padding: 4px 0.6em 5px 1em;}
.phaseSelected { background-color: #F4F5F6; } .phaseSelected { background-color: #F4F5F6; }
.content { padding: 14px 14px 9px 15px;} .content { padding: 14px 14px 9px 15px; }
.grey { display: none; position: absolute; left: 0px; top: 0px; .grey { display: none; position: absolute; left: 0px; top: 0px;
background:grey; opacity:0.5; -moz-opacity:0.5; -khtml-opacity:0.5; background:grey; opacity:0.5; -moz-opacity:0.5; -khtml-opacity:0.5;
filter:alpha(Opacity=50);} filter:alpha(Opacity=50);}

View file

@ -9,20 +9,22 @@ CKEDITOR.editorConfig = function( config )
config.toolbar_Appy = config.toolbar_Appy =
[ [
{ name: 'basicstyles', items : [ 'Format', 'Bold', 'Italic', 'Underline', { name: 'basicstyles', items : [ 'Format', 'Bold', 'Italic', 'Underline',
'Strike', 'Subscript', 'Superscript', '-', 'Strike', 'Subscript', 'Superscript'] },
'RemoveFormat' ] },
{ name: 'paragraph', items : [ 'NumberedList', 'BulletedList', '-', { name: 'paragraph', items : [ 'NumberedList', 'BulletedList', '-',
'Outdent', 'Indent', '-', 'JustifyLeft', 'Outdent', 'Indent', '-', 'JustifyLeft',
'JustifyCenter', 'JustifyRight', 'JustifyCenter', 'JustifyRight',
'JustifyBlock'] }, 'JustifyBlock'] },
{ name: 'clipboard', items : [ 'Cut', 'Copy', 'Paste', 'PasteText', { name: 'clipboard', items : [ 'Cut', 'Copy', 'Paste', 'PasteText',
'PasteFromWord', '-', 'Undo', 'Redo' ] }, 'PasteFromWord', 'Undo', 'Redo']},
{ name: 'editing', items : [ 'Find', 'Replace', '-', 'SelectAll', '-', { name: 'editing', items : [ 'Find', 'Replace', '-', 'SelectAll', '-',
'SpellChecker', 'Scayt']}, 'SpellChecker', 'Scayt']},
{ name: 'insert', items : [ 'Image', 'Table', 'HorizontalRule', { name: 'insert', items : [ 'Image', 'Table', 'SpecialChar', 'Link',
'SpecialChar', 'PageBreak', 'Link', 'Unlink', 'Unlink', 'Source', 'Maximize']},
'-', 'Maximize']},
]; ];
config.toolbar_AppyRich = config.toolbar_Appy.concat(
[{name: 'styles', items: [ 'Font', 'FontSize', 'TextColor', 'BGColor',
'RemoveFormat' ]},]
)
config.format_p = { element:'p', attributes:{'style':'margin:0;padding:0'}}; config.format_p = { element:'p', attributes:{'style':'margin:0;padding:0'}};
config.format_h1 = { element:'h1', attributes:{'style':'margin:0;padding:0'}}; config.format_h1 = { element:'h1', attributes:{'style':'margin:0;padding:0'}};
config.format_h2 = { element:'h2', attributes:{'style':'margin:0;padding:0'}}; config.format_h2 = { element:'h2', attributes:{'style':'margin:0;padding:0'}};

View file

@ -1,16 +1,17 @@
<tal:main define="tool context/getTool"> <tal:main define="tool context/getTool">
<html metal:use-macro="context/ui/template/macros/main"> <html metal:use-macro="context/ui/template/macros/main">
<metal:fill fill-slot="content" <metal:fill fill-slot="content"
tal:define="contextObj context/getParentNode; tal:define="contextObj context/getParentNode;
dummy python: contextObj.allows('Modify portal content', raiseError=True); dummy python: contextObj.allows('Modify portal content', raiseError=True);
errors request/errors | python:{}; errors request/errors | python:{};
layoutType python:'edit'; layoutType python:'edit';
layout python: contextObj.getPageLayout(layoutType); layout python: contextObj.getPageLayout(layoutType);
phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType=layoutType); page request/page|python:'main';
phase phaseInfo/name; cssJs python: {};
page request/page|python:'main'; groupedWidgets python: contextObj.getGroupedAppyTypes(layoutType, page, cssJs=cssJs);
cssJs python: contextObj.getCssJs(contextObj.getAppyTypes(layoutType, page), layoutType); phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType=layoutType);
confirmMsg request/confirmMsg | nothing;" phase phaseInfo/name;
confirmMsg request/confirmMsg | nothing;"
tal:on-error="structure python: tool.manageError(error)"> tal:on-error="structure python: tool.manageError(error)">
<tal:comment replace="nothing">Include type-specific CSS and JS.</tal:comment> <tal:comment replace="nothing">Include type-specific CSS and JS.</tal:comment>

View file

@ -42,7 +42,7 @@
</tal:comment> </tal:comment>
<table metal:define-macro="widgets" <table metal:define-macro="widgets"
tal:attributes="width layout/width"> tal:attributes="width layout/width">
<tr tal:repeat="widget python: contextObj.getGroupedAppyTypes(layoutType, page)"> <tr tal:repeat="widget groupedWidgets">
<td tal:condition="python: widget['type'] == 'group'"> <td tal:condition="python: widget['type'] == 'group'">
<metal:call use-macro="app/ui/widgets/show/macros/group"/> <metal:call use-macro="app/ui/widgets/show/macros/group"/>
</td> </td>

View file

@ -12,11 +12,9 @@
<div class="portletContent" tal:condition="python: contextObj and contextObj.mayNavigate()"> <div class="portletContent" tal:condition="python: contextObj and contextObj.mayNavigate()">
<div class="portletTitle" tal:define="parent contextObj/getParent"> <div class="portletTitle" tal:define="parent contextObj/getParent">
<span tal:replace="contextObj/Title"></span> <span tal:replace="contextObj/Title"></span>
<div style="float:right" tal:condition="python: parent"> <a tal:condition="python: parent" tal:attributes="href parent/absolute_url">
<a tal:attributes="href parent/absolute_url">
<img tal:attributes="src string: $appUrl/ui/gotoSource.png"/> <img tal:attributes="src string: $appUrl/ui/gotoSource.png"/>
</a> </a>
</div>
</div> </div>
<metal:phases use-macro="here/ui/portlet/macros/phases"/> <metal:phases use-macro="here/ui/portlet/macros/phases"/>
</div> </div>
@ -34,10 +32,9 @@
class python:test(not currentSearch and (currentClass==rootClass), 'portletCurrent', '')" class python:test(not currentSearch and (currentClass==rootClass), 'portletCurrent', '')"
tal:content="structure python: _(rootClass + '_plural')"> tal:content="structure python: _(rootClass + '_plural')">
</a> </a>
<div style="float: right" <span tal:define="addPermission python: '%s: Add %s' % (appName, rootClass);
tal:define="addPermission python: '%s: Add %s' % (appName, rootClass); userMayAdd python: user.has_permission(addPermission, appFolder);
userMayAdd python: user.has_permission(addPermission, appFolder); createMeans python: tool.getCreateMeans(rootClass)">
createMeans python: tool.getCreateMeans(rootClass)">
<tal:comment replace="nothing">Create a new object from a web form</tal:comment> <tal:comment replace="nothing">Create a new object from a web form</tal:comment>
<a tal:condition="python: ('form' in createMeans) and userMayAdd" <a tal:condition="python: ('form' in createMeans) and userMayAdd"
tal:attributes="href python: '%s/do?action=Create&className=%s' % (toolUrl, rootClass); tal:attributes="href python: '%s/do?action=Create&className=%s' % (toolUrl, rootClass);
@ -57,30 +54,30 @@
title python: _('search_objects')"> title python: _('search_objects')">
<img tal:attributes="src string: $appUrl/ui/search.gif"/> <img tal:attributes="src string: $appUrl/ui/search.gif"/>
</a> </a>
</div> </span>
<tal:comment replace="nothing">Searches for this content type.</tal:comment> <tal:comment replace="nothing">Searches for this content type.</tal:comment>
<tal:searchOrGroup repeat="searchOrGroup python: tool.getSearches(rootClass)"> <tal:searchOrGroup repeat="searchOrGroup python: tool.getSearches(rootClass)">
<tal:group condition="searchOrGroup/isGroup"> <tal:group condition="searchOrGroup/isGroup">
<tal:expanded define="group searchOrGroup; <tal:expanded define="group searchOrGroup;
expanded python: request.get(group['labelId'], 'collapsed') == 'expanded'"> expanded python: request.get(group['labelId'], 'collapsed') == 'expanded'">
<tal:comment replace="nothing">Group name</tal:comment> <tal:comment replace="nothing">Group name</tal:comment>
<dt class="portletGroup"> <div class="portletGroup">
<img align="left" style="cursor:pointer; margin-right: 3px" <img align="left" style="cursor:pointer; margin-right: 3px"
tal:attributes="id python: '%s_img' % group['labelId']; tal:attributes="id python: '%s_img' % group['labelId'];
src python:test(expanded, 'ui/collapse.gif', 'ui/expand.gif'); src python:test(expanded, 'ui/collapse.gif', 'ui/expand.gif');
onClick python:'toggleCookie(\'%s\')' % group['labelId']"/> onClick python:'toggleCookie(\'%s\')' % group['labelId']"/>
<span tal:replace="group/label"/> <span tal:replace="group/label"/>
</dt> </div>
<tal:comment replace="nothing">Group searches</tal:comment> <tal:comment replace="nothing">Group searches</tal:comment>
<span tal:attributes="id group/labelId; <div tal:attributes="id group/labelId;
style python:test(expanded, 'display:block', 'display:none')"> style python:test(expanded, 'display:block', 'display:none')">
<dt class="portletSearch" tal:repeat="search group/searches"> <div class="portletSearch" tal:repeat="search group/searches">
<a tal:attributes="href python: '%s?className=%s&search=%s' % (queryUrl, rootClass, search['name']); <a tal:attributes="href python: '%s?className=%s&search=%s' % (queryUrl, rootClass, search['name']);
title search/descr; title search/descr;
class python: test(search['name'] == currentSearch, 'portletCurrent', '');" class python: test(search['name'] == currentSearch, 'portletCurrent', '');"
tal:content="structure search/label"></a> tal:content="structure search/label"></a>
</dt> </div>
</span> </div>
</tal:expanded> </tal:expanded>
</tal:group> </tal:group>
<dt tal:define="search searchOrGroup" tal:condition="not: searchOrGroup/isGroup" <dt tal:define="search searchOrGroup" tal:condition="not: searchOrGroup/isGroup"
@ -100,10 +97,9 @@
currently shown object, made of phases and contained pages. currently shown object, made of phases and contained pages.
</tal:comment> </tal:comment>
<table metal:define-macro="phases" <table metal:define-macro="phases"
tal:define="phases contextObj/getAppyPhases|nothing; tal:define="phases contextObj/getAppyPhases;
singlePhase python: len(phases) == 1; singlePhase python: len(phases) == 1;
page python: req.get('page', 'main')" page python: req.get('page', 'main')">
tal:condition="python: phases" width="100%">
<tal:phase repeat="phase phases"> <tal:phase repeat="phase phases">
<tal:comment replace="nothing">The box containing phase-related information</tal:comment> <tal:comment replace="nothing">The box containing phase-related information</tal:comment>
<tr tal:define="singlePage python: len(phase['pages']) == 1"> <tr tal:define="singlePage python: len(phase['pages']) == 1">
@ -116,30 +112,25 @@
tal:content="structure python: _(label)"> tal:content="structure python: _(label)">
</div> </div>
<tal:comment replace="nothing">The page(s) within the phase</tal:comment> <tal:comment replace="nothing">The page(s) within the phase</tal:comment>
<table width="100%" cellpadding="0"> <tal:page repeat="aPage phase/pages">
<tal:page repeat="aPage phase/pages"> <tal:comment replace="nothing">First line: page name and icons</tal:comment>
<tal:comment replace="nothing">1st line: page name and icons</tal:comment> <div tal:condition="python: not (singlePhase and singlePage)"
<tr valign="top" tal:condition="python: not (singlePhase and singlePage)"> tal:attributes="class python: test(aPage == page, 'portletCurrent portletPage', 'portletPage')">
<td tal:attributes="class python: test(aPage == page, 'portletCurrent portletPage', 'portletPage')"> <a tal:attributes="href python: contextObj.getUrl(page=aPage)"
<a tal:attributes="href python: contextObj.getUrl(page=aPage)"
tal:content="structure python: _('%s_page_%s' % (contextObj.meta_type, aPage))"> tal:content="structure python: _('%s_page_%s' % (contextObj.meta_type, aPage))">
</a> </a>
</td> <a tal:condition="python: contextObj.allows('Modify portal content') and phase['pagesInfo'][aPage]['showOnEdit']"
<td align="right"> tal:attributes="href python: contextObj.getUrl(mode='edit', page=aPage)">
<img title="Edit" style="cursor:pointer" <img title="Edit" tal:attributes="src string: $appUrl/ui/edit.gif"/>
tal:attributes="onClick python: 'href: window.location=\'%s\'' % contextObj.getUrl(mode='edit', page=aPage); </a>
src string: $appUrl/ui/edit.gif" </div>
tal:condition="python: contextObj.allows('Modify portal content') and phase['pagesInfo'][aPage]['showOnEdit']"/> <tal:comment replace="nothing">Next lines: links</tal:comment>
</td> <tal:links define="links python: phase['pagesInfo'][aPage].get('links')" tal:condition="links">
</tr> <div tal:repeat="link links">
<tal:comment replace="nothing">2nd line: links</tal:comment> <a tal:content="link/title" tal:attributes="href link/url"></a>
<tal:links define="links python: phase['pagesInfo'][aPage].get('links')" tal:condition="links"> </div>
<tr tal:repeat="link links"> </tal:links>
<td><a tal:content="link/title" tal:attributes="href link/url"></a></td> </tal:page>
</tr>
</tal:links>
</tal:page>
</table>
</td> </td>
</tr> </tr>
<tal:comment replace="nothing">The down arrow pointing to the next phase (if any)</tal:comment> <tal:comment replace="nothing">The down arrow pointing to the next phase (if any)</tal:comment>

View file

@ -4,7 +4,8 @@
tal:define="className request/className; tal:define="className request/className;
refInfo request/ref|nothing; refInfo request/ref|nothing;
searchInfo python: tool.getSearchInfo(className, refInfo); searchInfo python: tool.getSearchInfo(className, refInfo);
cssJs python: tool.getCssJs(searchInfo['fields'], 'edit')"> cssJs python: {};
dummy python: tool.getCssJs(searchInfo['fields'], 'edit', cssJs)">
<tal:comment replace="nothing">Include type-specific CSS and JS.</tal:comment> <tal:comment replace="nothing">Include type-specific CSS and JS.</tal:comment>
<link tal:repeat="cssFile cssJs/css" rel="stylesheet" type="text/css" <link tal:repeat="cssFile cssJs/css" rel="stylesheet" type="text/css"

View file

@ -1,15 +1,16 @@
<tal:main define="tool context/config"> <tal:main define="tool context/config">
<html metal:use-macro="context/ui/template/macros/main"> <html metal:use-macro="context/ui/template/macros/main">
<metal:fill fill-slot="content" <metal:fill fill-slot="content"
tal:define="contextObj python: context.getParentNode(); tal:define="contextObj python: context.getParentNode();
dummy python: contextObj.allows('View', raiseError=True); dummy python: contextObj.allows('View', raiseError=True);
portal_type python: context.portal_type.lower().replace(' ', '_'); portal_type python: context.portal_type.lower().replace(' ', '_');
errors python: req.get('errors', {}); errors python: req.get('errors', {});
layoutType python:'view'; layoutType python:'view';
layout python: contextObj.getPageLayout(layoutType); layout python: contextObj.getPageLayout(layoutType);
phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType='view'); phaseInfo python: contextObj.getAppyPhases(currentOnly=True, layoutType='view');
page req/page|python:'main'; page req/page|python:'main';
phase phaseInfo/name;" phase phaseInfo/name;
groupedWidgets python: contextObj.getGroupedAppyTypes(layoutType, page);"
tal:on-error="structure python: tool.manageError(error)"> tal:on-error="structure python: tool.manageError(error)">
<metal:prologue use-macro="context/ui/page/macros/prologue"/> <metal:prologue use-macro="context/ui/page/macros/prologue"/>
<metal:show use-macro="context/ui/page/macros/show"/> <metal:show use-macro="context/ui/page/macros/show"/>

View file

@ -1,8 +1,10 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from appy.gen import WorkflowOwner
from appy.gen.wrappers import AbstractWrapper from appy.gen.wrappers import AbstractWrapper
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class GroupWrapper(AbstractWrapper): class GroupWrapper(AbstractWrapper):
workflow = WorkflowOwner
def showLogin(self): def showLogin(self):
'''When must we show the login field?''' '''When must we show the login field?'''

View file

@ -30,16 +30,23 @@ class ToolWrapper(AbstractWrapper):
return NOT_UNO_ENABLED_PYTHON % value return NOT_UNO_ENABLED_PYTHON % value
return True return True
def isManager(self):
'''Some pages on the tool can only be accessed by God.'''
if self.user.has_role('Manager'): return 'view'
podOutputFormats = ('odt', 'pdf', 'doc', 'rtf') podOutputFormats = ('odt', 'pdf', 'doc', 'rtf')
def getPodOutputFormats(self): def getPodOutputFormats(self):
'''Gets the available output formats for POD documents.''' '''Gets the available output formats for POD documents.'''
return [(of, self.translate(of)) for of in self.podOutputFormats] return [(of, self.translate(of)) for of in self.podOutputFormats]
def getInitiator(self): def getInitiator(self, field=False):
'''Retrieves the object that triggered the creation of the object '''Retrieves the object that triggered the creation of the object
being currently created (if any).''' being currently created (if any), or the name of the field in this
object if p_field is given.'''
nav = self.o.REQUEST.get('nav', '') nav = self.o.REQUEST.get('nav', '')
if nav: return self.getObject(nav.split('.')[1]) if not nav or not nav.startswith('ref.'): return
if not field: return self.getObject(nav.split('.')[1])
return nav.split('.')[2].split(':')[0]
def getObject(self, uid): def getObject(self, uid):
'''Allow to retrieve an object from its unique identifier p_uid.''' '''Allow to retrieve an object from its unique identifier p_uid.'''

View file

@ -1,13 +1,15 @@
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
from appy.gen import WorkflowOwner
from appy.gen.wrappers import AbstractWrapper from appy.gen.wrappers import AbstractWrapper
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class UserWrapper(AbstractWrapper): class UserWrapper(AbstractWrapper):
workflow = WorkflowOwner
def showLogin(self): def showLogin(self):
'''When must we show the login field?''' '''When must we show the login field?'''
if self.o.isTemporary(): return 'edit' if self.o.isTemporary(): return 'edit'
return 'view' return ('view', 'result')
def showName(self): def showName(self):
'''Name and first name, by default, are always shown.''' '''Name and first name, by default, are always shown.'''
@ -29,7 +31,8 @@ class UserWrapper(AbstractWrapper):
if login == 'admin': if login == 'admin':
return 'This username is reserved.' # XXX Translate return 'This username is reserved.' # XXX Translate
# Check that no user or group already uses this login. # Check that no user or group already uses this login.
if self.count('User', login=login) or self.count('Group', login=login): if self.count('User', noSecurity=True, login=login) or \
self.count('Group', noSecurity=True, login=login):
return 'This login is already in use.' # XXX Translate return 'This login is already in use.' # XXX Translate
return True return True

View file

@ -54,7 +54,8 @@ class AbstractWrapper(object):
elif name == 'user': elif name == 'user':
return self.o.getUser() return self.o.getUser()
elif name == 'appyUser': elif name == 'appyUser':
return self.search1('User', login=self.o.getUser().getId()) return self.search1('User', noSecurity=True,
login=self.o.getUser().getId())
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)

View file

@ -106,8 +106,7 @@ class Debianizer:
def __init__(self, app, out, appVersion='0.1.0', def __init__(self, app, out, appVersion='0.1.0',
pythonVersions=('2.6',), zopePort=8080, pythonVersions=('2.6',), zopePort=8080,
depends=('zope2.12', 'openoffice.org', 'imagemagick'), depends=('openoffice.org', 'imagemagick'), sign=False):
sign=False):
# app is the path to the Python package to Debianize. # app is the path to the Python package to Debianize.
self.app = app self.app = app
self.appName = os.path.basename(app) self.appName = os.path.basename(app)
@ -262,6 +261,10 @@ class Debianizer:
# Create postinst, a script that will: # Create postinst, a script that will:
# - bytecompile Python files after the Debian install # - bytecompile Python files after the Debian install
# - change ownership of some files if required # - change ownership of some files if required
# - [in the case of a app-package] execute:
# apt-get -t squeeze-backports install zope2.12
# (if zope2.12 is defined as a simple dependency in field "Depends:"
# it will fail because it will not be searched in squeeze-backports).
# - [in the case of an app-package] call update-rc.d for starting it at # - [in the case of an app-package] call update-rc.d for starting it at
# boot time. # boot time.
f = file('postinst', 'w') f = file('postinst', 'w')
@ -273,6 +276,8 @@ class Debianizer:
self.appName) self.appName)
content += 'if [ -e %s ]\nthen\n%sfi\n' % (bin, cmds) content += 'if [ -e %s ]\nthen\n%sfi\n' % (bin, cmds)
if self.appName != 'appy': if self.appName != 'appy':
# Install zope2.12 from squeeze-backports
content += 'apt-get -t squeeze-backports install zope2.12\n'
# Allow user "zope", that runs the Zope instance, to write the # Allow user "zope", that runs the Zope instance, to write the
# database and log files. # database and log files.
content += 'chown -R zope:root /var/lib/%s\n' % self.appNameLower content += 'chown -R zope:root /var/lib/%s\n' % self.appNameLower