appy.gen: improvements in user management.

This commit is contained in:
Gaetan Delannay 2012-02-21 12:09:42 +01:00
parent 9394490d33
commit 9c5f92337b
10 changed files with 85 additions and 23 deletions

View file

@ -335,8 +335,7 @@ class ZopeInstaller:
(translation.id, poName)) (translation.id, poName))
# Execute custom installation code if any # Execute custom installation code if any
if hasattr(appyTool, 'install'): if hasattr(appyTool, 'onInstall'): appyTool.onInstall()
tool.executeAppyAction('install', reindex=False)
def configureSessions(self): def configureSessions(self):
'''Configure the session machinery.''' '''Configure the session machinery.'''

View file

@ -102,6 +102,16 @@ class ToolMixin(BaseMixin):
for elem in path.split('/'): res = res._getOb(elem) for elem in path.split('/'): res = res._getOb(elem)
return res return res
def showLanguageSelector(self):
'''We must show the language selector if the app config requires it and
it there is more than 2 supported languages. Moreover, on some pages,
switching the language is not allowed.'''
cfg = self.getProductConfig()
if not cfg.languageSelector: return
if len(cfg.languages) < 2: return
page = self.REQUEST.get('ACTUAL_URL').split('/')[-1]
return page not in ('edit', 'query', 'search')
def getLanguages(self): def getLanguages(self):
'''Returns the supported languages. First one is the default.''' '''Returns the supported languages. First one is the default.'''
return self.getProductConfig().languages return self.getProductConfig().languages
@ -111,6 +121,14 @@ class ToolMixin(BaseMixin):
p_code.''' p_code.'''
return languages.get(code)[2] return languages.get(code)[2]
def getCssJs(self):
'''Returns the list of CSS and JS files to include in the main template.
The method ensures that appy.css and appy.js come first.'''
names = self.getPhysicalRoot().ui.objectIds('File')
names.remove('appy.js'); names.insert(0, 'appy.js')
names.remove('appy.css'); names.insert(0, 'appy.css')
return names
def consumeMessages(self): def consumeMessages(self):
'''Returns the list of messages to show to a web page and clean it in '''Returns the list of messages to show to a web page and clean it in
the session.''' the session.'''
@ -825,6 +843,11 @@ class ToolMixin(BaseMixin):
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
# Authentication-related methods # Authentication-related methods
# -------------------------------------------------------------------------- # --------------------------------------------------------------------------
def _updateCookie(self, login, password):
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
cookieValue = urllib.quote(cookieValue)
self.REQUEST.RESPONSE.setCookie('__ac', cookieValue, path='/')
def performLogin(self): def performLogin(self):
'''Logs the user in.''' '''Logs the user in.'''
rq = self.REQUEST rq = self.REQUEST
@ -837,10 +860,7 @@ class ToolMixin(BaseMixin):
return self.goto(urlBack, msg) return self.goto(urlBack, msg)
# Perform the Zope-level authentication # Perform the Zope-level authentication
login = rq.get('__ac_name', '') login = rq.get('__ac_name', '')
password = rq.get('__ac_password', '') self._updateCookie(login, rq.get('__ac_password', ''))
cookieValue = base64.encodestring('%s:%s' % (login, password)).rstrip()
cookieValue = urllib.quote(cookieValue)
rq.RESPONSE.setCookie('__ac', cookieValue, path='/')
user = self.acl_users.validate(rq) user = self.acl_users.validate(rq)
if self.userIsAnon(): if self.userIsAnon():
rq.RESPONSE.expireCookie('__ac', path='/') rq.RESPONSE.expireCookie('__ac', path='/')

View file

@ -85,7 +85,10 @@ class ModelClass:
elif isinstance(value, gen.Page): elif isinstance(value, gen.Page):
value = 'pages["%s"]' % value.name value = 'pages["%s"]' % value.name
elif callable(value): elif callable(value):
value = '%s.%s' % (wrapperName, value.__name__) className = wrapperName
if (appyType.type == 'Ref') and appyType.isBack:
className = appyType.back.klass.__name__
value = '%s.%s' % (className, value.__name__)
typeArgs += '%s=%s,' % (name, value) typeArgs += '%s=%s,' % (name, value)
return '%s(%s)' % (appyType.__class__.__name__, typeArgs) return '%s(%s)' % (appyType.__class__.__name__, typeArgs)
@ -144,10 +147,12 @@ class User(ModelClass):
password1 = gen.String(format=gen.String.PASSWORD, show=showPassword, password1 = gen.String(format=gen.String.PASSWORD, show=showPassword,
validator=validatePassword, **gm) validator=validatePassword, **gm)
password2 = gen.String(format=gen.String.PASSWORD, show=showPassword, **gm) password2 = gen.String(format=gen.String.PASSWORD, show=showPassword, **gm)
email = gen.String(group='main', width=25) def showEmail(self): pass
email = gen.String(show=showEmail, group='main', width=25)
gm['multiplicity'] = (0, None) gm['multiplicity'] = (0, None)
roles = gen.String(validator=gen.Selection('getGrantableRoles'), def showRoles(self): pass
indexed=True, **gm) roles = gen.String(show=showRoles, indexed=True,
validator=gen.Selection('getGrantableRoles'), **gm)
# The Group class -------------------------------------------------------------- # The Group class --------------------------------------------------------------
class Group(ModelClass): class Group(ModelClass):
@ -163,7 +168,7 @@ class Group(ModelClass):
roles = gen.String(validator=gen.Selection('getGrantableRoles'), roles = gen.String(validator=gen.Selection('getGrantableRoles'),
multiplicity=(0,None), **m) multiplicity=(0,None), **m)
users = gen.Ref(User, multiplicity=(0,None), add=False, link=True, users = gen.Ref(User, multiplicity=(0,None), add=False, link=True,
back=gen.Ref(attribute='groups', show=True), back=gen.Ref(attribute='groups', show=User.showRoles),
showHeaders=True, shownInfo=('title', 'login')) showHeaders=True, shownInfo=('title', 'login'))
# The Translation class -------------------------------------------------------- # The Translation class --------------------------------------------------------

View file

@ -40,7 +40,7 @@ img {border: 0}
border-style: solid; border-width: 1px; border-color: grey; } border-style: solid; border-width: 1px; border-color: grey; }
.top { height: 75px; margin-left: 3em; vertical-align: top;} .top { height: 75px; margin-left: 3em; vertical-align: top;}
.lang { margin-right: 3px; } .lang { margin-right: 3px; }
.userStrip { background-color: #89A6B1; height: 30px; .userStrip { background-color: #89A6B1; height: 40px;
border-top: 3px solid #405A64; border-bottom: 2px solid #5F7983; } border-top: 3px solid #405A64; border-bottom: 2px solid #5F7983; }
.login { margin-top: 2px; margin-bottom: 2px; color: white;} .login { margin-top: 2px; margin-bottom: 2px; color: white;}
.buttons { margin-left: 4px;} .buttons { margin-left: 4px;}

BIN
gen/ui/banner.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

7
gen/ui/home.pt Normal file
View file

@ -0,0 +1,7 @@
<tal:main define="tool python: context.config">
<html metal:use-macro="context/ui/template/macros/main">
<div metal:fill-slot="content">
<span tal:replace="structure python: tool.translate('front_page_text')"/>
</div>
</html>
</tal:main>

View file

@ -14,7 +14,7 @@
<head> <head>
<title tal:content="tool/getAppName"></title> <title tal:content="tool/getAppName"></title>
<tal:link repeat="name python: app.ui.objectIds('File')"> <tal:link repeat="name tool/getCssJs">
<link tal:condition="python: name.endswith('.css')" <link tal:condition="python: name.endswith('.css')"
rel="stylesheet" type="text/css" tal:attributes="href string:$appUrl/ui/$name"/> rel="stylesheet" type="text/css" tal:attributes="href string:$appUrl/ui/$name"/>
<script tal:condition="python: name.endswith('.js')" <script tal:condition="python: name.endswith('.js')"
@ -31,12 +31,10 @@
<tr valign="top"> <tr valign="top">
<td></td> <td></td>
<tal:comment replace="nothing">Language selector (links or listbox)</tal:comment> <tal:comment replace="nothing">Language selector (links or listbox)</tal:comment>
<td align="right" <td align="right" tal:condition="tool/showLanguageSelector">
tal:define="languages tool/getLanguages; <tal:lgs define="languages tool/getLanguages;
defaultLanguage python: languages[0]; defaultLanguage python: languages[0];
suffix python: req.get('ACTUAL_URL').split('/')[-1]; asLinks python: len(languages) &lt;= 5">
asLinks python: len(languages) &lt;= 5"
tal:condition="python: len(languages) &gt;= 2 and (suffix not in ('edit', 'query', 'search'))">
<table tal:condition="asLinks"> <table tal:condition="asLinks">
<tr> <tr>
<td tal:repeat="lang languages"> <td tal:repeat="lang languages">
@ -54,6 +52,7 @@
tal:attributes="selected python:defaultLanguage == lang; value lang"> tal:attributes="selected python:defaultLanguage == lang; value lang">
</option> </option>
</select> </select>
</tal:lgs>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -9,6 +9,10 @@ class GroupWrapper(AbstractWrapper):
if self.o.isTemporary(): return 'edit' if self.o.isTemporary(): return 'edit'
return 'view' return 'view'
def showGroups(self):
'''Only the admin can view or edit roles.'''
return self.user.has_role('Manager')
def validateLogin(self, login): def validateLogin(self, login):
'''Is this p_login valid?''' '''Is this p_login valid?'''
return True return True

View file

@ -9,6 +9,16 @@ class UserWrapper(AbstractWrapper):
if self.o.isTemporary(): return 'edit' if self.o.isTemporary(): return 'edit'
return 'view' return 'view'
def showEmail(self):
'''In most cases, email is the login. Show the field only if it is not
the case.'''
email = self.email
return email and (email != self.login)
def showRoles(self):
'''Only the admin can view or edit roles.'''
return self.user.has_role('Manager')
def validateLogin(self, login): def validateLogin(self, login):
'''Is this p_login valid?''' '''Is this p_login valid?'''
# The login can't be the id of the whole site or "admin" # The login can't be the id of the whole site or "admin"
@ -28,8 +38,11 @@ class UserWrapper(AbstractWrapper):
def showPassword(self): def showPassword(self):
'''When must we show the 2 fields for entering a password ?''' '''When must we show the 2 fields for entering a password ?'''
# When someone creates the user
if self.o.isTemporary(): return 'edit' if self.o.isTemporary(): return 'edit'
return False # When the user itself (which is Owner of the object representing him)
# wants to edit information about himself.
if self.user.has_role('Owner', self): return 'edit'
def getGrantableRoles(self): def getGrantableRoles(self):
'''Returns the list of roles that the admin can grant to a user.''' '''Returns the list of roles that the admin can grant to a user.'''
@ -64,9 +77,16 @@ class UserWrapper(AbstractWrapper):
# granted to it. # granted to it.
zopeUser.groups = PersistentMapping() zopeUser.groups = PersistentMapping()
else: else:
# Updates roles at the Zope level. # Update roles at the Zope level.
zopeUser = aclUsers.getUserById(login) zopeUser = aclUsers.getUserById(login)
zopeUser.roles = self.roles zopeUser.roles = self.roles
# Update the password if the user has entered new ones.
rq = self.request
if rq.has_key('password1'):
zopeUser.__ = aclUsers._encryptPassword(rq['password1'])
# Update the cookie value
self.tool.o._updateCookie(login, rq['password1'])
self.password1 = self.password2 = ''
# "self" must be owned by its Zope user. # "self" must be owned by its Zope user.
if 'Owner' not in self.o.get_local_roles_for_userid(login): if 'Owner' not in self.o.get_local_roles_for_userid(login):
self.o.manage_addLocalRoles(login, ('Owner',)) self.o.manage_addLocalRoles(login, ('Owner',))

View file

@ -108,7 +108,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=('zope2.12', 'openoffice.org', 'imagemagick'),
sign=True): 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)
@ -372,7 +372,15 @@ definitionJsonConf = '''{
class Cortexer: class Cortexer:
'''This class allows to produce a Cortex application definition for '''This class allows to produce a Cortex application definition for
a Debianized Python/Appy application.''' a Debianized Python/Appy application.
Once the "cortex.admin" folder and its content has been generated, in
order to push the app definition into Cortex, go in the folder where
"cortex.admin" lies and type (command-line tool "cortex-client" must
be installed):
cortex-client sync push --api http://<cortex-host-ip>/api
'''
def __init__(self, app, pythonVersions=('2.6',)): def __init__(self, app, pythonVersions=('2.6',)):
self.appName = os.path.basename(app) self.appName = os.path.basename(app)
self.pythonVersions = pythonVersions self.pythonVersions = pythonVersions