diff --git a/fields/page.py b/fields/page.py index d7d804f..aac6feb 100644 --- a/fields/page.py +++ b/fields/page.py @@ -21,6 +21,7 @@ from appy import Object class Page: '''Used for describing a page, its related phase, show condition, etc.''' subElements = ('save', 'cancel', 'previous', 'next', 'edit') + def __init__(self, name, phase='main', show=True, showSave=True, showCancel=True, showPrevious=True, showNext=True, showEdit=True, label=None): @@ -61,9 +62,8 @@ class Page: res = Page(pageData[0], phase=pageData[1]) return res - def isShowable(self, obj, elem='page'): - '''Must this page be shown for p_obj? The method can return True, False - or 'view' (page is available only in "view" mode). + def isShowable(self, obj, layoutType, elem='page'): + '''Is this page showable for p_obj on p_layoutType ("view" or "edit")? If p_elem is not "page", this method returns the fact that a sub-element is viewable or not (buttons "save", "cancel", etc).''' @@ -72,16 +72,14 @@ class Page: # Get the value of the show attribute as identified above. res = getattr(self, attr) if callable(res): res = res(obj.appy()) + if isinstance(res, str): return res == layoutType return res def getInfo(self, obj, layoutType): '''Gets information about this page, for p_obj, as an object.''' res = Object() for elem in Page.subElements: - showable = self.isShowable(obj, elem) - # "showable" can be True, False or "view" - if layoutType == 'edit': showable = showable==True - else: showable = bool(showable) + showable = self.isShowable(obj, layoutType, elem) setattr(res, 'show%s' % elem.capitalize(), showable) return res diff --git a/fields/phase.py b/fields/phase.py index 63d13ff..3f999be 100644 --- a/fields/phase.py +++ b/fields/phase.py @@ -33,8 +33,11 @@ class Phase: class=":(aPage == page) and 'currentPage' or ''"> - ::aPageInfo.page.getLabel(zobj) + + ::label + :label + @@ -136,13 +139,14 @@ class Phase: if (field.page.name in self.pages) or \ (field.page.name in self.hiddenPages): return # Add the page only if it must be shown. - showable = field.page.isShowable(obj) - if showable: + showOnView = field.page.isShowable(obj, 'view') + showOnEdit = field.page.isShowable(obj, 'edit') + if showOnView or showOnEdit: # The page must be added self.pages.append(field.page.name) # Create the dict about page information and add it in self.pageInfo - pageInfo = Object(page=field.page, showOnView=bool(showable), - showOnEdit=showable==True, links=None) + pageInfo = Object(page=field.page, showOnView=showOnView, + showOnEdit=showOnEdit, links=None) pageInfo.update(field.page.getInfo(obj, layoutType)) self.pagesInfo[field.page.name] = pageInfo else: @@ -203,4 +207,18 @@ class Phase: return res, nextPhase.pagesInfo[res] else: return None, None + + def getPageInfo(self, page, layoutType): + '''Return the page info corresponding to the given p_page. If this page + cannot be shown on p_layoutType, this method returns page info about + the first showable page on p_layoutType, or None if no page is + showable at all.''' + res = self.pagesInfo[page] + showAttribute = 'showOn%s' % layoutType.capitalize() + if getattr(res, showAttribute): return res + # Find the first showable page in this phase on p_layoutType. + for pageName in self.pages: + if pageName == page: continue + pageInfo = self.pagesInfo[pageName] + if getattr(pageInfo, showAttribute): return pageInfo # ------------------------------------------------------------------------------ diff --git a/fields/workflow.py b/fields/workflow.py index aa80dcd..ab8b6fb 100644 --- a/fields/workflow.py +++ b/fields/workflow.py @@ -17,6 +17,7 @@ import types, string from group import Group from appy.px import Px +from appy.gen.utils import User from appy.gen.mail import sendNotification # Default Appy permissions ----------------------------------------------------- @@ -568,6 +569,11 @@ class WorkflowOwner: active = State({r:(ma, o), w:(ma, o), d:ma}, initial=True) inactive = State({r:(ma, o), w:ma, d:ma}) # Transitions - deactivate = Transition( (active, inactive), condition=ma) + def doDeactivate(self, obj): + '''Prevent user "admin" from being deactivated.''' + if isinstance(obj, User) and (obj.login == 'admin'): + raise Exception('Cannot deactivate admin.') + deactivate = Transition( (active, inactive), condition=ma, + action=doDeactivate) reactivate = Transition( (inactive, active), condition=ma) # ------------------------------------------------------------------------------ diff --git a/gen/__init__.py b/gen/__init__.py index e8b5d06..a971b6e 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -38,20 +38,13 @@ from appy.fields.page import Page from appy.fields.phase import Phase from appy.fields.workflow import * from appy.gen.layout import Table -from appy.gen.utils import No +from appy.gen.utils import No, Tool, User # Make the following classes available here: people may need to override some # of their PXs (defined as static attributes). from appy.gen.wrappers import AbstractWrapper as BaseObject from appy.gen.wrappers.ToolWrapper import ToolWrapper as BaseTool -# ------------------------------------------------------------------------------ -class Model: pass -class Tool(Model): - '''Subclass me to extend or modify the Tool class.''' -class User(Model): - '''Subclass me to extend or modify the User class.''' - # ------------------------------------------------------------------------------ class Config: '''If you want to specify some configuration parameters for appy.gen and diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index ae1cd53..4f09a98 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -21,6 +21,8 @@ except ImportError: # Global JS internationalized messages that will be computed in every page ----- jsMessages = ('no_elem_selected', 'action_confirm', 'save_confirm', 'warn_leave_form') +USER_NOT_FOUND = 'User %s not found. Probably a problem implying several ' \ + 'Appy apps put behind the same domain name or dev machine.' # ------------------------------------------------------------------------------ class ToolMixin(BaseMixin): @@ -67,6 +69,8 @@ class ToolMixin(BaseMixin): if not url: # Bring Managers to the config, lead others to pxHome. user = self.getUser() + if not user: + raise Exception(USER_NOT_FOUND % self.identifyUser()[0]) if user.has_role('Manager'): url = self.goto(self.absolute_url()) else: @@ -1043,22 +1047,6 @@ class ToolMixin(BaseMixin): from AccessControl.User import BasicUserFolder BasicUserFolder.validate = validate - def getUserLine(self): - '''Returns info about the currently logged user as a 2-tuple: first - elem is the one-line user info as shown on every page; second line is - the URL to edit user info.''' - user = self.getUser() - info = [user.title] - showable = [r for r in user.getRoles() if r != 'Authenticated'] - if showable: - info.append(', '.join([self.translate('role_%s' % r) \ - for r in showable])) - # Edit URL for the user. - url = None - if user.o.mayEdit(): - url = user.o.getUrl(mode='edit', page='main', nav='') - return (' | '.join(info), url) - def getUserName(self, login=None, normalized=False): '''Gets the user name corresponding to p_login (or the currently logged user if None), or the p_login itself if the user does not exist diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 6f65124..7ff962a 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -383,23 +383,23 @@ class BaseMixin: errorMessage = self.translate('validation_error') isNew = self.isTemporary() inPopup = rq.get('popup') == '1' - # If this object is created from an initiator, get info about him. + # If this object is created from an initiator, get info about him initiator, initiatorField = self.getInitiatorInfo() initiatorPage = initiatorField and initiatorField.pageName or None - # If the user clicked on 'Cancel', go back to the previous page. + # If the user clicked on 'Cancel', go back to the previous page buttonClicked = rq.get('button') if buttonClicked == 'cancel': - if inPopup: - back = tool.backFromPopup() - elif initiator: - # Go back to the initiator page. + if inPopup: back = tool.backFromPopup() + elif initiator: # Go back to the initiator page urlBack = initiator.getUrl(page=initiatorPage, nav='') else: - if isNew: - # Go back to the root of the site. - urlBack = tool.getSiteUrl() + if isNew: urlBack = tool.getHomePage() # Go back to home page else: - urlBack = self.getUrl() + # Return to the same page, excepted if unshowable on view. + phaseObj = self.getAppyPhases(True, 'view') + pageInfo = phaseObj.getPageInfo(rq['page'], 'view') + if not pageInfo: urlBack = tool.getHomePage() + else: urlBack = self.getUrl(page=pageInfo.page.name) self.say(self.translate('object_canceled')) self.removeLock(rq['page']) if inPopup: return back @@ -452,24 +452,27 @@ class BaseMixin: if inPopup: return tool.backFromPopup() if isNew and initiator: return self.goto(initiator.getUrl(page=initiatorPage, nav='')) - return self.goto(obj.getUrl()) + # Return to the same page, if showable on view + phaseObj = self.getAppyPhases(True, 'view') + pageInfo = phaseObj.getPageInfo(rq['page'], 'view') + if not pageInfo: return self.goto(tool.getHomePage(), msg) + return self.goto(obj.getUrl(page=pageInfo.page.name)) # Get the current page name. We keep it in "pageName" because rq['page'] # can be changed by m_getAppyPhases called below. pageName = rq['page'] - if buttonClicked == 'previous': - # Go to the previous page for this object. - # We recompute the list of phases and pages because things - # may have changed since the object has been updated (ie, - # additional pages may be shown or hidden now, so the next and - # previous pages may have changed). Moreover, previous and next - # pages may not be available in "edit" mode, so we return the edit - # or view pages depending on page.show. - phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit') - pageName, pageInfo = phaseObj.getPreviousPage(pageName) + if buttonClicked in ('previous', 'next'): + # Go to the previous or next page for this object. We recompute the + # list of phases and pages because things may have changed since the + # object has been updated (ie, additional pages may be shown or + # hidden now, so the next and previous pages may have changed). + # Moreover, previous and next pages may not be available in "edit" + # mode, so we return the edit or view pages depending on page.show. + phaseObj = self.getAppyPhases(True, 'edit') + methodName = 'get%sPage' % buttonClicked.capitalize() + pageName, pageInfo = getattr(phaseObj, methodName)(pageName) if pageName: # Return to the edit or view page? if pageInfo.showOnEdit: - rq.set('page', pageName) # I do not use gotoEdit here because I really need to # redirect the user to the edit page. Indeed, the object # edit URL may have moved from temp_folder to another place. @@ -480,21 +483,6 @@ class BaseMixin: else: obj.say(msg) return self.goto(obj.getUrl(inPopup=inPopup)) - if buttonClicked == 'next': - # Go to the next page for this object. - phaseObj = self.getAppyPhases(currentOnly=True, layoutType='edit') - pageName, pageInfo = phaseObj.getNextPage(pageName) - if pageName: - # Return to the edit or view page? - if pageInfo.showOnEdit: - # Same remark as above (click on "previous"). - return self.goto(obj.getUrl(mode='edit', page=pageName, - inPopup=inPopup)) - else: - return self.goto(obj.getUrl(page=pageName, inPopup=inPopup)) - else: - obj.say(msg) - return self.goto(obj.getUrl(inPopup=inPopup)) return obj.gotoEdit() def reindex(self, indexes=None, unindex=False): diff --git a/gen/model.py b/gen/model.py index ab4feda..b5f736f 100644 --- a/gen/model.py +++ b/gen/model.py @@ -123,14 +123,16 @@ class ModelClass: pages[appyType.page.name] = appyType.page res += ' pges = {' for page in pages.itervalues(): - # Determine page show - pageShow = page.show - if isinstance(pageShow, basestring): pageShow='"%s"' % pageShow - elif callable(pageShow): - pageShow = '%s.%s' % (wrapperName, pageShow.__name__) + # Determine page "show" attributes pShow = '' - if pageShow != True: - pShow = ', show=%s' % pageShow + for attr in ('',) + page.subElements: + attrName = 'show%s' % attr.capitalize() + pageShow = getattr(page, attrName) + if isinstance(pageShow, basestring): pageShow='"%s"' % pageShow + elif callable(pageShow): + pageShow = '%s.%s' % (wrapperName, pageShow.__name__) + if pageShow != True: + pShow += ', %s=%s' % (attrName, pageShow) # For translation pages, fixed labels are used. label = '' if className == 'Translation': @@ -148,34 +150,38 @@ class ModelClass: # The User class --------------------------------------------------------------- class User(ModelClass): - _appy_attributes = ['title', 'name', 'firstName', 'login', 'password1', - 'password2', 'email', 'roles', 'source', 'groups', - 'toTool'] + _appy_attributes = ['password1', 'password2', 'title', 'name', 'firstName', + 'login', 'email', 'roles', 'source', 'groups', 'toTool'] + # Passwords are on a specific page. + def showPassword(self): pass + def validatePassword(self): pass + pp = {'page': gen.Page('passwords', showNext=False, show=showPassword), + 'width': 34, 'multiplicity': (1,1), 'format': gen.String.PASSWORD, + 'show': showPassword} + password1 = gen.String(validator=validatePassword, **pp) + password2 = gen.String(**pp) + # All methods defined below are fake. Real versions are in the wrapper. - title = gen.String(show=False, indexed=True) - gm = {'group': 'main', 'width': 34} + pm = {'page': gen.Page('main', showPrevious=False), 'group': 'main', + 'width': 34} + title = gen.String(show=False, indexed=True, **pm) def showName(self): pass - name = gen.String(show=showName, **gm) - firstName = gen.String(show=showName, **gm) + name = gen.String(show=showName, **pm) + firstName = gen.String(show=showName, **pm) def showEmail(self): pass - email = gen.String(show=showEmail, **gm) + email = gen.String(show=showEmail, **pm) # Where is this user stored? By default, in the ZODB. But the user can be # stored in an external LDAP (source='ldap'). - source = gen.String(show=False, default='zodb', layouts='f', **gm) - gm['multiplicity'] = (1,1) + source = gen.String(show=False, default='zodb', layouts='f', **pm) + pm['multiplicity'] = (1,1) def showLogin(self): pass def validateLogin(self): pass login = gen.String(show=showLogin, validator=validateLogin, - indexed=True, **gm) - def showPassword(self): pass - def validatePassword(self): pass - password1 = gen.String(format=gen.String.PASSWORD, show=showPassword, - validator=validatePassword, **gm) - password2 = gen.String(format=gen.String.PASSWORD, show=showPassword, **gm) - gm['multiplicity'] = (0, None) + indexed=True, **pm) + pm['multiplicity'] = (0, None) def showRoles(self): pass roles = gen.String(show=showRoles, indexed=True, - validator=gen.Selection('getGrantableRoles'), **gm) + validator=gen.Selection('getGrantableRoles'), **pm) # The Group class -------------------------------------------------------------- class Group(ModelClass): diff --git a/gen/tr/Appy.pot b/gen/tr/Appy.pot index 6637294..28fb4b0 100644 --- a/gen/tr/Appy.pot +++ b/gen/tr/Appy.pot @@ -379,6 +379,10 @@ msgstr "" msgid "app_home" msgstr "" +#. Default: "Change password" +msgid "change_password" +msgstr "" + #. Default: "This login is reserved." msgid "login_reserved" msgstr "" diff --git a/gen/tr/ar.po b/gen/tr/ar.po index 497d0d9..51dd0de 100644 --- a/gen/tr/ar.po +++ b/gen/tr/ar.po @@ -379,6 +379,10 @@ msgstr "كلمة السر" msgid "app_home" msgstr "" +#. Default: "Change password" +msgid "change_password" +msgstr "" + #. Default: "This login is reserved." msgid "login_reserved" msgstr "" diff --git a/gen/tr/de.po b/gen/tr/de.po index 96c134c..82bd447 100644 --- a/gen/tr/de.po +++ b/gen/tr/de.po @@ -379,6 +379,10 @@ msgstr "Passwort" msgid "app_home" msgstr "" +#. Default: "Change password" +msgid "change_password" +msgstr "" + #. Default: "This login is reserved." msgid "login_reserved" msgstr "Dieses Login ist reserviert" diff --git a/gen/tr/en.po b/gen/tr/en.po index 3075f4d..1bdf0b3 100644 --- a/gen/tr/en.po +++ b/gen/tr/en.po @@ -380,6 +380,10 @@ msgstr "Password" msgid "app_home" msgstr "Home" +#. Default: "Change password" +msgid "change_password" +msgstr "Change password" + #. Default: "This login is reserved." msgid "login_reserved" msgstr "This login is reserved." diff --git a/gen/tr/es.po b/gen/tr/es.po index c35c59b..ebbb1ca 100644 --- a/gen/tr/es.po +++ b/gen/tr/es.po @@ -379,6 +379,10 @@ msgstr "Contraseña" msgid "app_home" msgstr "volver a mi página principal" +#. Default: "Change password" +msgid "change_password" +msgstr "" + #. Default: "This login is reserved." msgid "login_reserved" msgstr "Este login está reservado." diff --git a/gen/tr/fr.po b/gen/tr/fr.po index f18902a..2d3a5e8 100644 --- a/gen/tr/fr.po +++ b/gen/tr/fr.po @@ -380,6 +380,10 @@ msgstr "Mot de passe" msgid "app_home" msgstr "Revenir à ma page principale" +#. Default: "Change password" +msgid "change_password" +msgstr "Changer le mot de passe" + #. Default: "This login is reserved." msgid "login_reserved" msgstr "Ce login est réservé." diff --git a/gen/tr/it.po b/gen/tr/it.po index 1868984..88a376b 100644 --- a/gen/tr/it.po +++ b/gen/tr/it.po @@ -379,6 +379,10 @@ msgstr "Password" msgid "app_home" msgstr "Ritorno alla pagina di partenza" +#. Default: "Change password" +msgid "change_password" +msgstr "" + #. Default: "This login is reserved." msgid "login_reserved" msgstr "" diff --git a/gen/tr/nl.po b/gen/tr/nl.po index d044d9f..a9331ee 100644 --- a/gen/tr/nl.po +++ b/gen/tr/nl.po @@ -379,6 +379,10 @@ msgstr "Wachtwoord" msgid "app_home" msgstr "Keer terug naar de hoofdpagina" +#. Default: "Change password" +msgid "change_password" +msgstr "" + #. Default: "This login is reserved." msgid "login_reserved" msgstr "Dit login is voorbehouden" diff --git a/gen/ui/appy.css b/gen/ui/appy.css index a8077ce..53094c5 100644 --- a/gen/ui/appy.css +++ b/gen/ui/appy.css @@ -55,6 +55,7 @@ img { border: 0; vertical-align: middle } .userStripText { padding: 0 0.3em 0 0.3em; color: whitesmoke } .userStrip a { color: #e7e7e7 } .userStrip a:visited { color: #e7e7e7 } +.changePassword { color: #494949 !important; font-style:italic; font-size: 90% } .breadcrumb { font-size: 11pt; padding-bottom: 6px } .login { margin: 3px; color: black } input.button { color: #666666; height: 20px; margin-bottom: 5px; margin-top:2px; diff --git a/gen/ui/user.png b/gen/ui/user.png new file mode 100644 index 0000000..c638d8f Binary files /dev/null and b/gen/ui/user.png differ diff --git a/gen/utils.py b/gen/utils.py index a424738..4ca40cf 100644 --- a/gen/utils.py +++ b/gen/utils.py @@ -228,4 +228,12 @@ class No: def __init__(self, msg): self.msg = msg def __nonzero__(self): return False def __repr__(self): return '' % self.msg + # ------------------------------------------------------------------------------ +class Model: pass +class Tool(Model): + '''Subclass me to extend or modify the Tool class.''' +class User(Model): + '''Subclass me to extend or modify the User class.''' +# ------------------------------------------------------------------------------ + diff --git a/gen/wrappers/UserWrapper.py b/gen/wrappers/UserWrapper.py index 120ff8d..74324d5 100644 --- a/gen/wrappers/UserWrapper.py +++ b/gen/wrappers/UserWrapper.py @@ -1,4 +1,5 @@ # ------------------------------------------------------------------------------ +from appy.px import Px from appy.fields.string import String from appy.gen import WorkflowOwner from appy.gen.layout import summaryPageLayouts @@ -12,6 +13,16 @@ class UserWrapper(AbstractWrapper): specialUsers = ('system', 'anon', 'admin') layouts = summaryPageLayouts + # Display, in the user strip, links to the User instance of the logged user. + pxUserLinks = Px(''' + + + :user.getTitle() + + :_('change_password') + ''') + def getTitle(self, normalized=False): '''Returns a nice name for this user, based on available information: name/first name or title or login. If p_normalized is True, special diff --git a/gen/wrappers/__init__.py b/gen/wrappers/__init__.py index 2a12f10..edd5208 100644 --- a/gen/wrappers/__init__.py +++ b/gen/wrappers/__init__.py @@ -264,12 +264,7 @@ class AbstractWrapper(object): - - :userInfo[0] - - + :user.pxUserLinks