From 6245023365d3e430d7927b9296614f005246738c Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Sat, 5 May 2012 17:04:19 +0200 Subject: [PATCH] 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. --- bin/backup.py | 5 +- bin/job.py | 2 +- gen/__init__.py | 88 ++++++++++++++++++++++++------------ gen/installer.py | 5 +- gen/mail.py | 31 ++++++++++--- gen/migrator.py | 5 +- gen/mixins/ToolMixin.py | 3 +- gen/mixins/__init__.py | 39 ++++++++++------ gen/model.py | 37 +++++++++------ gen/ui/appy.css | 2 +- gen/ui/ckeditor/config.js | 14 +++--- gen/ui/edit.pt | 21 +++++---- gen/ui/page.pt | 2 +- gen/ui/portlet.pt | 71 +++++++++++++---------------- gen/ui/search.pt | 3 +- gen/ui/view.pt | 19 ++++---- gen/wrappers/GroupWrapper.py | 2 + gen/wrappers/ToolWrapper.py | 13 ++++-- gen/wrappers/UserWrapper.py | 7 ++- gen/wrappers/__init__.py | 3 +- shared/packaging.py | 9 +++- 21 files changed, 233 insertions(+), 148 deletions(-) diff --git a/bin/backup.py b/bin/backup.py index 1eac11b..49542de 100644 --- a/bin/backup.py +++ b/bin/backup.py @@ -134,8 +134,8 @@ class ZodbBackuper: '''Send content of self.logMem to self.emails.''' w = self.log subject = 'Backup notification.' - msg = 'From: %s\nSubject: %s\n\n%s' % (self.options.fromAddress, - subject, self.logMem.getvalue()) + msg = 'From: %s\nTo: %s\nSubject: %s\n\n%s' % (self.options.fromAddress, + self.emails, subject, self.logMem.getvalue()) try: w('> Sending mail notifications to %s...' % self.emails) smtpInfo = self.options.smtpServer.split(':', 3) @@ -151,6 +151,7 @@ class ZodbBackuper: smtpServer.login(login, password) res = smtpServer.sendmail(self.options.fromAddress, self.emails.split(','), msg) + smtpServer.quit() if res: w('Could not send mail to some recipients. %s' % str(res)) w('Done.') diff --git a/bin/job.py b/bin/job.py index 07c29dc..f33579f 100644 --- a/bin/job.py +++ b/bin/job.py @@ -67,7 +67,7 @@ else: # If we are in a Appy application, the object on which we will call the # method is the config object on this root object. if not appName: - targetObject = rootObject.data.appy() + targetObject = rootObject.config.appy() elif not appName.startswith('path='): objectName = 'portal_%s' % appName.lower() targetObject = getattr(rootObject, objectName).appy() diff --git a/gen/__init__.py b/gen/__init__.py index 477253e..e6fb0f2 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -356,6 +356,12 @@ class Search: # ------------------------------------------------------------------------------ class 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, editDefault, show, page, group, layouts, move, indexed, searchable, specificReadPermission, specificWritePermission, @@ -723,13 +729,25 @@ class Type: res = copy.deepcopy(defaultFieldLayouts) return res - def getCss(self, layoutType): - '''This method returns a list of CSS files that are required for - displaying widgets of self's type on a given p_layoutType.''' + def getCss(self, layoutType, res): + '''This method completes the list p_res with the names of CSS 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.cssFiles: + for fileName in self.cssFiles[layoutType]: + if fileName not in res: + res.append(fileName) - def getJs(self, layoutType): - '''This method returns a list of Javascript files that are required for - displaying widgets of self's type on a given p_layoutType.''' + def getJs(self, layoutType, res): + '''This method completes the list p_res with the names of Javascript + 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): '''Gets, on_obj, the value conforming to self's type definition.''' @@ -1025,6 +1043,9 @@ class Float(Type): return self.pythonType(value) 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 c = re.compile 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, focus=False, historized=False, mapping=None, label=None, transform='none', styles=('p','h1','h2','h3','h4'), - allowImageUpload=True): + allowImageUpload=True, richText=False): # According to format, the widget will be different: input field, # textarea, inline editor... Note that there can be only one String # 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 # select in the styles dropdown is defined hereafter. 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 ? self.allowImageUpload = allowImageUpload # 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 # (ie for image size when images are resized). So in this case we # 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) def getFormattedValue(self, obj, value): @@ -1379,9 +1406,8 @@ class String(Type): return 'ZCTextIndex' return Type.getIndexType(self) - def getJs(self, layoutType): - if (layoutType == 'edit') and (self.format == String.XHTML): - return ('ckeditor/ckeditor.js',) + def getJs(self, layoutType, res): + if self.format == String.XHTML: Type.getJs(self, layoutType, res) def getCaptchaChallenge(self, session): '''Returns a Captcha challenge in the form of a dict. At key "text", @@ -1445,6 +1471,11 @@ class Boolean(Type): return res 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" WITH_HOUR = 0 WITHOUT_HOUR = 1 @@ -1474,14 +1505,13 @@ class Date(Type): master, masterValue, focus, historized, True, mapping, label) - def getCss(self, layoutType): - if (layoutType == 'edit') and self.calendar: - return ('jscalendar/calendar-blue.css',) + def getCss(self, layoutType, res): + # CSS files are only required if the calendar must be shown. + if self.calendar: Type.getCss(self, layoutType, res) - def getJs(self, layoutType): - if (layoutType == 'edit') and self.calendar: - return ('jscalendar/calendar.js', 'jscalendar/lang/calendar-en.js', - 'jscalendar/calendar-setup.js') + def getJs(self, layoutType, res): + # Javascript files are only required if the calendar must be shown. + if self.calendar: Type.getJs(self, layoutType, res) def getSelectableYears(self): '''Gets the list of years one may select for this field.''' @@ -2372,21 +2402,15 @@ class List(Type): if i >= len(outerValue): return '' return getattr(outerValue[i], name, '') - def getCss(self, layoutType): + def getCss(self, layoutType, res): '''Gets the CSS required by sub-fields if any.''' - res = () for name, field in self.fields: - css = field.getCss(layoutType) - if css: res += css - return res + field.getCss(layoutType, res) - def getJs(self, layoutType): + def getJs(self, layoutType, res): '''Gets the JS required by sub-fields if any.''' - res = () for name, field in self.fields: - js = field.getJs(layoutType) - if js: res += js - return res + field.getJs(layoutType, res) # Workflow-specific types and default workflows -------------------------------- appyToZopePermissions = { @@ -2768,6 +2792,14 @@ class WorkflowAuthenticated: initial=True) 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: '''Instances of this class may be given as validator of a String, in order diff --git a/gen/installer.py b/gen/installer.py index 9c1965e..f2fc513 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -267,7 +267,7 @@ class ZopeInstaller: appyTool.log('Admin user "admin" created.') # 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', roles=['Manager']) appyTool.log('Group "admins" created.') @@ -275,7 +275,8 @@ class ZopeInstaller: # Create a group for every global role defined in the application for role in self.config.applicationGlobalRoles: relatedGroup = '%s_group' % role - if appyTool.count('Group', login=relatedGroup): continue + if appyTool.count('Group', noSecurity=True, login=relatedGroup): + continue appyTool.create('groups', login=relatedGroup, title=relatedGroup, roles=[role]) appyTool.log('Group "%s", related to global role "%s", was ' \ diff --git a/gen/mail.py b/gen/mail.py index fc48838..9ed0368 100644 --- a/gen/mail.py +++ b/gen/mail.py @@ -13,9 +13,13 @@ def sendMail(tool, to, subject, body, attachments=None): list of email addresses).''' # Just log things if mail is disabled fromAddress = tool.mailFrom - if not tool.mailEnabled: - tool.log('Mail disabled: should send mail from %s to %s.' % \ - (fromAddress, str(to))) + if not tool.mailEnabled or not tool.mailHost: + if not tool.mailHost: + 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('Body: %s' % body) if attachments: @@ -65,11 +69,24 @@ def sendMail(tool, to, subject, body, attachments=None): msg.attach(part) # Send the email try: - mh = smtplib.SMTP(tool.mailHost) - mh.sendmail(fromAddress, [to], msg.as_string()) - mh.quit() + smtpInfo = tool.mailHost.split(':', 3) + login = password = None + 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: - tool.log('Mail sending failed: %s' % str(e)) + tool.log('Mail sending failed: %s' % str(e), type='error') # ------------------------------------------------------------------------------ def sendNotification(obj, transition, transitionName, workflow): diff --git a/gen/migrator.py b/gen/migrator.py index 2089b01..87b646e 100644 --- a/gen/migrator.py +++ b/gen/migrator.py @@ -43,9 +43,10 @@ class Migrator: # Manage groups. Exclude not-used default Plone groups. for groupId in user.getGroups(): 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 - appyGroup = tool.search('Group', login=groupId)[0] + appyGroup = tool.search('Group', noSecurity=True, + login=groupId)[0] else: # Create the group. Todo: get Plone group roles and title appyGroup = tool.create('groups', login=groupId, diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index d92bdfc..8d6ee7d 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -981,7 +981,8 @@ class ToolMixin(BaseMixin): if rolesToShow: res.append(', '.join([self.translate('role_%s'%r) \ 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): '''Generates a UID for an instance of p_className.''' diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 3a1cac7..a77ace7 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -589,11 +589,15 @@ class BaseMixin: klass = self.getTool().getAppyClass(className, wrapper=True) 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 (dict version) is given.''' res = [] 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 refresh = ('refresh' in self.REQUEST) if refresh: @@ -602,6 +606,11 @@ class BaseMixin: if refresh: appyType = appyType.reload(klass, self) if appyType.page.name != pageName: 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: res.append(appyType.__dict__) else: @@ -610,6 +619,9 @@ class BaseMixin: groupDescr = appyType.group.insertInto(res, groups, appyType.page, self.meta_type) GroupDescr.addWidget(groupDescr, appyType.__dict__) + if collectCssJs: + cssJs['css'] = css + cssJs['js'] = js return res def getAppyTypes(self, layoutType, pageName): @@ -622,23 +634,19 @@ class BaseMixin: res.append(appyType) return res - def getCssJs(self, fields, layoutType): - '''Gets the list of Javascript and CSS files required by Appy types - p_fields when shown on p_layoutType.''' + def getCssJs(self, fields, layoutType, res): + '''Gets, in p_res ~{'css':[s_css], 'js':[s_js]}~ the lists of + 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 # inclusion can be important, and this could be losed by using sets. css = [] js = [] for field in fields: - fieldCss = field.getCss(layoutType) - if fieldCss: - for fcss in fieldCss: - if fcss not in css: css.append(fcss) - fieldJs = field.getJs(layoutType) - if fieldJs: - for fjs in fieldJs: - if fjs not in js: js.append(fjs) - return {'css':css, 'js':js} + field.getCss(layoutType, css) + field.getJs(layoutType, js) + res['css'] = css + res['js'] = js def getAppyTypesFromNames(self, fieldNames, asDict=True): '''Gets the Appy types named p_fieldNames.''' @@ -874,7 +882,8 @@ class BaseMixin: else: appyClass = self.getTool().getAppyClass(className) if hasattr(appyClass, 'workflow'): wf = appyClass.workflow - else: wf = gen.WorkflowAnonymous + else: + wf = gen.WorkflowAnonymous if not name: return wf return WorkflowDescriptor.getWorkflowName(wf) @@ -1495,7 +1504,7 @@ class BaseMixin: # Define the attributes that will initialize the ckeditor instance for # this field. field = self.getAppyType(name) - ckAttrs = {'toolbar': 'Appy', + ckAttrs = {'toolbar': field.richText and 'AppyRich' or 'Appy', 'format_tags': '%s' % ';'.join(field.styles)} if field.allowImageUpload: ckAttrs['filebrowserUploadUrl'] = '%s/upload' % self.absolute_url() diff --git a/gen/model.py b/gen/model.py index d360cf2..9f92714 100644 --- a/gen/model.py +++ b/gen/model.py @@ -125,6 +125,8 @@ class ModelClass: # Determine page show pageShow = page.show 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 += '}\n' # Secondly, dump every attribute @@ -194,7 +196,7 @@ class Page(ModelClass): _appy_attributes = ['title', 'content', 'pages'] folder = True title = gen.String(show='edit', indexed=True) - content = gen.String(format=gen.String.XHTML, layouts='f') + content = gen.String(format=gen.String.XHTML, layouts='f', richText=True) # Pages can contain other pages. def showSubPages(self): pass pages = gen.Ref(None, multiplicity=(0,None), add=True, link=False, @@ -209,10 +211,10 @@ toolFieldPrefixes = ('defaultValue', 'podTemplate', 'formats', 'resultColumns', 'enableAdvancedSearch', 'numberOfSearchColumns', 'searchFields', 'optionalFields', 'showWorkflow', 'showAllStatesInPhase') -defaultToolFields = ('title', 'unoEnabledPython','openOfficePort', - 'numberOfResultsPerPage', 'mailHost', 'mailEnabled', - 'mailFrom', 'appyVersion', 'users', 'groups', - 'translations', 'pages') +defaultToolFields = ('title', 'mailHost', 'mailEnabled', 'mailFrom', + 'appyVersion', 'users', 'groups', 'translations', 'pages', + 'unoEnabledPython','openOfficePort', + 'numberOfResultsPerPage') class Tool(ModelClass): # In a ModelClass we need to declare attributes in the following list. @@ -220,33 +222,40 @@ class Tool(ModelClass): folder = True # Tool attributes + def isManager(self): pass 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') mailEnabled = gen.Boolean(default=False) 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). users = gen.Ref(User, multiplicity=(0,None), add=True, link=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'), showHeaders=True, shownInfo=('title', 'login', 'roles')) groups = gen.Ref(Group, multiplicity=(0,None), add=True, link=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'), showHeaders=True, shownInfo=('title', 'login', 'roles')) translations = gen.Ref(Translation, multiplicity=(0,None), add=False, link=False, show='view', 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, 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 def _appy_clean(klass): diff --git a/gen/ui/appy.css b/gen/ui/appy.css index 726ff74..fd01f47 100644 --- a/gen/ui/appy.css +++ b/gen/ui/appy.css @@ -64,7 +64,7 @@ img {border: 0} .portletSearch { font-size: 90%; font-style: italic; padding-left: 1em} .phase { border-style: dashed; border-width: thin; padding: 4px 0.6em 5px 1em;} .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; background:grey; opacity:0.5; -moz-opacity:0.5; -khtml-opacity:0.5; filter:alpha(Opacity=50);} diff --git a/gen/ui/ckeditor/config.js b/gen/ui/ckeditor/config.js index 7f2d17e..3d7e470 100644 --- a/gen/ui/ckeditor/config.js +++ b/gen/ui/ckeditor/config.js @@ -9,20 +9,22 @@ CKEDITOR.editorConfig = function( config ) config.toolbar_Appy = [ { name: 'basicstyles', items : [ 'Format', 'Bold', 'Italic', 'Underline', - 'Strike', 'Subscript', 'Superscript', '-', - 'RemoveFormat' ] }, + 'Strike', 'Subscript', 'Superscript'] }, { name: 'paragraph', items : [ 'NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'] }, { name: 'clipboard', items : [ 'Cut', 'Copy', 'Paste', 'PasteText', - 'PasteFromWord', '-', 'Undo', 'Redo' ] }, + 'PasteFromWord', 'Undo', 'Redo']}, { name: 'editing', items : [ 'Find', 'Replace', '-', 'SelectAll', '-', 'SpellChecker', 'Scayt']}, - { name: 'insert', items : [ 'Image', 'Table', 'HorizontalRule', - 'SpecialChar', 'PageBreak', 'Link', 'Unlink', - '-', 'Maximize']}, + { name: 'insert', items : [ 'Image', 'Table', 'SpecialChar', 'Link', + 'Unlink', 'Source', '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_h1 = { element:'h1', attributes:{'style':'margin:0;padding:0'}}; config.format_h2 = { element:'h2', attributes:{'style':'margin:0;padding:0'}}; diff --git a/gen/ui/edit.pt b/gen/ui/edit.pt index 9d579bd..1c46f9f 100644 --- a/gen/ui/edit.pt +++ b/gen/ui/edit.pt @@ -1,16 +1,17 @@ Include type-specific CSS and JS. diff --git a/gen/ui/page.pt b/gen/ui/page.pt index 07f020d..5e13dd5 100644 --- a/gen/ui/page.pt +++ b/gen/ui/page.pt @@ -42,7 +42,7 @@ - + diff --git a/gen/ui/portlet.pt b/gen/ui/portlet.pt index a58f76a..8e8eaf0 100644 --- a/gen/ui/portlet.pt +++ b/gen/ui/portlet.pt @@ -12,11 +12,9 @@
-
- + -
@@ -34,10 +32,9 @@ class python:test(not currentSearch and (currentClass==rootClass), 'portletCurrent', '')" tal:content="structure python: _(rootClass + '_plural')"> -
+ Create a new object from a web form -
+ Searches for this content type. Group name -
+
-
+ Group searches - -
+
-
-
+ +
+ page python: req.get('page', 'main')"> The box containing phase-related information @@ -116,30 +112,25 @@ tal:content="structure python: _(label)"> The page(s) within the phase -
- - 1st line: page name and icons - - - - - 2nd line: links - - - - - - -
- + First line: page name and icons + - -
+ + + + + + Next lines: links + +
+ +
+
+ The down arrow pointing to the next phase (if any) diff --git a/gen/ui/search.pt b/gen/ui/search.pt index 9d91287..b73d101 100644 --- a/gen/ui/search.pt +++ b/gen/ui/search.pt @@ -4,7 +4,8 @@ tal:define="className request/className; refInfo request/ref|nothing; searchInfo python: tool.getSearchInfo(className, refInfo); - cssJs python: tool.getCssJs(searchInfo['fields'], 'edit')"> + cssJs python: {}; + dummy python: tool.getCssJs(searchInfo['fields'], 'edit', cssJs)"> Include type-specific CSS and JS. diff --git a/gen/wrappers/GroupWrapper.py b/gen/wrappers/GroupWrapper.py index 335178c..41dae73 100644 --- a/gen/wrappers/GroupWrapper.py +++ b/gen/wrappers/GroupWrapper.py @@ -1,8 +1,10 @@ # ------------------------------------------------------------------------------ +from appy.gen import WorkflowOwner from appy.gen.wrappers import AbstractWrapper # ------------------------------------------------------------------------------ class GroupWrapper(AbstractWrapper): + workflow = WorkflowOwner def showLogin(self): '''When must we show the login field?''' diff --git a/gen/wrappers/ToolWrapper.py b/gen/wrappers/ToolWrapper.py index 2e0adb5..23ba2f9 100644 --- a/gen/wrappers/ToolWrapper.py +++ b/gen/wrappers/ToolWrapper.py @@ -30,16 +30,23 @@ class ToolWrapper(AbstractWrapper): return NOT_UNO_ENABLED_PYTHON % value 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') def getPodOutputFormats(self): '''Gets the available output formats for POD documents.''' 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 - 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', '') - 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): '''Allow to retrieve an object from its unique identifier p_uid.''' diff --git a/gen/wrappers/UserWrapper.py b/gen/wrappers/UserWrapper.py index 64c4ff8..e0382f7 100644 --- a/gen/wrappers/UserWrapper.py +++ b/gen/wrappers/UserWrapper.py @@ -1,13 +1,15 @@ # ------------------------------------------------------------------------------ +from appy.gen import WorkflowOwner from appy.gen.wrappers import AbstractWrapper # ------------------------------------------------------------------------------ class UserWrapper(AbstractWrapper): + workflow = WorkflowOwner def showLogin(self): '''When must we show the login field?''' if self.o.isTemporary(): return 'edit' - return 'view' + return ('view', 'result') def showName(self): '''Name and first name, by default, are always shown.''' @@ -29,7 +31,8 @@ class UserWrapper(AbstractWrapper): if login == 'admin': return 'This username is reserved.' # XXX Translate # 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 True diff --git a/gen/wrappers/__init__.py b/gen/wrappers/__init__.py index 6e89b45..02a571a 100644 --- a/gen/wrappers/__init__.py +++ b/gen/wrappers/__init__.py @@ -54,7 +54,8 @@ class AbstractWrapper(object): elif name == 'user': return self.o.getUser() 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() # Now, let's try to return a real attribute. res = object.__getattribute__(self, name) diff --git a/shared/packaging.py b/shared/packaging.py index 53c5b34..d951b44 100644 --- a/shared/packaging.py +++ b/shared/packaging.py @@ -106,8 +106,7 @@ class Debianizer: def __init__(self, app, out, appVersion='0.1.0', pythonVersions=('2.6',), zopePort=8080, - depends=('zope2.12', 'openoffice.org', 'imagemagick'), - sign=False): + depends=('openoffice.org', 'imagemagick'), sign=False): # app is the path to the Python package to Debianize. self.app = app self.appName = os.path.basename(app) @@ -262,6 +261,10 @@ class Debianizer: # Create postinst, a script that will: # - bytecompile Python files after the Debian install # - 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 # boot time. f = file('postinst', 'w') @@ -273,6 +276,8 @@ class Debianizer: self.appName) content += 'if [ -e %s ]\nthen\n%sfi\n' % (bin, cmds) 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 # database and log files. content += 'chown -R zope:root /var/lib/%s\n' % self.appNameLower