appy.gen: added new format 'captcha' for a String.

This commit is contained in:
Gaetan Delannay 2012-02-16 18:13:51 +01:00
parent 0d55abb239
commit a80ef513ff
15 changed files with 123 additions and 53 deletions

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
import re, time, copy, sys, types, os, os.path, mimetypes, string, StringIO import re, time, copy, sys, types, os, os.path, mimetypes, string, StringIO, \
random
from appy import Object from appy import Object
from appy.gen.layout import Table from appy.gen.layout import Table
from appy.gen.layout import defaultFieldLayouts from appy.gen.layout import defaultFieldLayouts
@ -1105,6 +1106,7 @@ class String(Type):
TEXT = 1 TEXT = 1
XHTML = 2 XHTML = 2
PASSWORD = 3 PASSWORD = 3
CAPTCHA = 4
def __init__(self, validator=None, multiplicity=(0,1), index=None, def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, format=LINE, default=None, optional=False, editDefault=False, format=LINE,
show=True, page='main', group=None, layouts=None, move=0, show=True, page='main', group=None, layouts=None, move=0,
@ -1115,7 +1117,9 @@ class String(Type):
transform='none', styles=('p','h1','h2','h3','h4'), transform='none', styles=('p','h1','h2','h3','h4'),
allowImageUpload=True): allowImageUpload=True):
# According to format, the widget will be different: input field, # According to format, the widget will be different: input field,
# textarea, inline editor... # textarea, inline editor... Note that there can be only one String
# field of format CAPTCHA by page, because the captcha challenge is
# stored in the session at some global key.
self.format = format self.format = format
# 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.
@ -1285,18 +1289,25 @@ class String(Type):
return res return res
def validateValue(self, obj, value): def validateValue(self, obj, value):
if not self.isSelect: return if self.format == String.CAPTCHA:
# Check that the value is among possible values challenge = obj.REQUEST.SESSION.get('captcha', None)
possibleValues = self.getPossibleValues(obj) # Compute the challenge minus the char to remove
if isinstance(value, basestring): i = challenge['number']-1
error = value not in possibleValues text = challenge['text'][:i] + challenge['text'][i+1:]
else: if value != text:
error = False return obj.translate('bad_captcha')
for v in value: elif self.isSelect:
if v not in possibleValues: # Check that the value is among possible values
error = True possibleValues = self.getPossibleValues(obj)
break if isinstance(value, basestring):
if error: return obj.translate('bad_select_value') error = value not in possibleValues
else:
error = False
for v in value:
if v not in possibleValues:
error = True
break
if error: return obj.translate('bad_select_value')
accents = {'é':'e','è':'e','ê':'e','ë':'e','à':'a','â':'a','ä':'a', accents = {'é':'e','è':'e','ê':'e','ë':'e','à':'a','â':'a','ä':'a',
'ù':'u','û':'u','ü':'u','î':'i','ï':'i','ô':'o','ö':'o', 'ù':'u','û':'u','ü':'u','î':'i','ï':'i','ô':'o','ö':'o',
@ -1348,6 +1359,26 @@ class String(Type):
if (layoutType == 'edit') and (self.format == String.XHTML): if (layoutType == 'edit') and (self.format == String.XHTML):
return ('ckeditor/ckeditor.js',) return ('ckeditor/ckeditor.js',)
def getCaptchaChallenge(self, session):
'''Returns a Captcha challenge in the form of a dict. At key "text",
value is a string that the user will be required to re-type, but
without 1 character whose position is at key "number". The challenge
is stored in the p_session, for the server-side subsequent check.'''
length = random.randint(5, 9) # The length of the challenge to encode
number = random.randint(1, length) # The position of the char to remove
text = '' # The challenge the user needs to type (minus one char)
for i in range(length):
j = random.randint(0, 1)
if j == 0:
chars = string.digits
else:
chars = string.letters
# Choose a char
text += chars[random.randint(0,len(chars)-1)]
res = {'text': text, 'number': number}
session['captcha'] = res
return res
class Boolean(Type): class Boolean(Type):
def __init__(self, validator=None, multiplicity=(0,1), index=None, def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True, default=None, optional=False, editDefault=False, show=True,
@ -2636,14 +2667,17 @@ class No:
class WorkflowAnonymous: class WorkflowAnonymous:
'''One-state workflow allowing anyone to consult and Manager to edit.''' '''One-state workflow allowing anyone to consult and Manager to edit.'''
mgr = 'Manager' mgr = 'Manager'
active = State({r:(mgr, 'Anonymous'), w:mgr, d:mgr}, initial=True) o = 'Owner'
active = State({r:(mgr, 'Anonymous'), w:(mgr,o), d:(mgr,o)}, initial=True)
WorkflowAnonymous.__instance__ = WorkflowAnonymous() WorkflowAnonymous.__instance__ = WorkflowAnonymous()
class WorkflowAuthenticated: class WorkflowAuthenticated:
'''One-state workflow allowing authenticated users to consult and Manager '''One-state workflow allowing authenticated users to consult and Manager
to edit.''' to edit.'''
mgr = 'Manager' mgr = 'Manager'
active = State({r:(mgr, 'Authenticated'), w:mgr, d:mgr}, initial=True) o = 'Owner'
active = State({r:(mgr, 'Authenticated'), w:(mgr,o), d:(mgr,o)},
initial=True)
WorkflowAuthenticated.__instance__ = WorkflowAuthenticated() WorkflowAuthenticated.__instance__ = WorkflowAuthenticated()
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -458,6 +458,8 @@ class ZopeGenerator(Generator):
msg('doc', '', msg.FORMAT_DOC), msg('doc', '', msg.FORMAT_DOC),
msg('rtf', '', msg.FORMAT_RTF), msg('rtf', '', msg.FORMAT_RTF),
msg('front_page_text', '', msg.FRONT_PAGE_TEXT), msg('front_page_text', '', msg.FRONT_PAGE_TEXT),
msg('captcha_text', '', msg.CAPTCHA_TEXT),
msg('bad_captcha', '', msg.BAD_CAPTCHA),
] ]
# Create a label for every role added by this application # Create a label for every role added by this application
for role in self.getAllUsedRoles(): for role in self.getAllUsedRoles():

View file

@ -249,9 +249,11 @@ class ZopeInstaller:
except: except:
# When Plone has installed PAS in acl_users this may fail. Plone # When Plone has installed PAS in acl_users this may fail. Plone
# may still be in the way for migration purposes. # may still be in the way for migration purposes.
users = ('admin') # We suppose there is at least a user. users = ('admin',) # We suppose there is at least a user.
if not users: if not users:
self.app.acl_users._doAddUser('admin', 'admin', ['Manager'], ()) appyTool.create('users', login='admin', firstName='admin',
name='admin', password1='admin', password2='admin',
email='admin@appyframework.org', roles=['Manager'])
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

View file

@ -216,7 +216,7 @@ class Table(LayoutElement):
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
defaultPageLayouts = { defaultPageLayouts = {
'view': Table('s|-n!-w|-b|', align="center"), 'view': Table('n!-w|-b|', align="center"),
'edit': Table('w|-b|', width=None)} 'edit': Table('w|-b|', width=None)}
defaultFieldLayouts = {'edit': 'lrv-f'} defaultFieldLayouts = {'edit': 'lrv-f'}
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -938,14 +938,17 @@ class ToolMixin(BaseMixin):
return [f.__dict__ for f in self.getAllAppyTypes(contentType) \ return [f.__dict__ for f in self.getAllAppyTypes(contentType) \
if (f.type == 'Pod') and (f.show == 'result')] if (f.type == 'Pod') and (f.show == 'result')]
def getUserLine(self, user): def getUserLine(self):
'''Returns a one-line user info as shown on every page.''' '''Returns a info about the currently logged user as a 2-tuple: first
res = [user.getId()] elem is the one-line user info as shown on every page; second line is
rolesToShow = [r for r in user.getRoles() \ the URL to edit user info.'''
appyUser = self.appy().appyUser
res = [appyUser.title, appyUser.login]
rolesToShow = [r for r in appyUser.roles \
if r not in ('Authenticated', 'Member')] if r not in ('Authenticated', 'Member')]
if rolesToShow: if rolesToShow:
res.append(', '.join([self.translate(r) for r in rolesToShow])) res.append(', '.join([self.translate(r) for r in rolesToShow]))
return ' | '.join(res) return (' | '.join(res), appyUser.o.getUrl(mode='edit'))
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

@ -418,7 +418,7 @@ class BaseMixin:
return the result.''' return the result.'''
page = self.REQUEST.get('page', 'main') page = self.REQUEST.get('page', 'main')
for field in self.getAppyTypes('edit', page): for field in self.getAppyTypes('edit', page):
if (field.type == 'String') and (field.format == 3): if (field.type == 'String') and (field.format in (3,4)):
self.REQUEST.set(field.name, '') self.REQUEST.set(field.name, '')
return self.ui.edit(self) return self.ui.edit(self)
@ -1073,6 +1073,9 @@ class BaseMixin:
obj = self obj = self
return appyType.getPossibleValues(obj, withTranslations, withBlankValue) return appyType.getPossibleValues(obj, withTranslations, withBlankValue)
def getCaptchaChallenge(self, name):
return self.getAppyType(name).getCaptchaChallenge(self.REQUEST.SESSION)
def appy(self): def appy(self):
'''Returns a wrapper object allowing to manipulate p_self the Appy '''Returns a wrapper object allowing to manipulate p_self the Appy
way.''' way.'''

View file

@ -114,6 +114,10 @@ class PoMessage:
FORMAT_PDF = 'PDF' FORMAT_PDF = 'PDF'
FORMAT_DOC = 'DOC' FORMAT_DOC = 'DOC'
FORMAT_RTF = 'RTF' FORMAT_RTF = 'RTF'
CAPTCHA_TEXT = 'Please type "${text}" (without the double quotes) in the ' \
'field besides, but without the character at position ' \
'${number}.'
BAD_CAPTCHA = 'The code was not correct. Please try again.'
def __init__(self, id, msg, default, fuzzy=False, comments=[], def __init__(self, id, msg, default, fuzzy=False, comments=[],
niceDefault=False): niceDefault=False):

View file

@ -30,24 +30,24 @@
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>
<img style="cursor:pointer" <a tal:condition="python: ('form' in createMeans) and userMayAdd"
tal:condition="python: ('form' in createMeans) and userMayAdd" tal:attributes="href python: '%s/do?action=Create&className=%s' % (toolUrl, rootClass);
tal:attributes="onClick python: 'href: window.location=\'%s/do?action=Create&className=%s\'' % (toolUrl, rootClass); title python: _('query_create')">
src string: $appUrl/ui/plus.png; <img tal:attributes="src string: $appUrl/ui/plus.png"/>
title python: _('query_create')"/> </a>
<tal:comment replace="nothing">Create (a) new object(s) by importing data</tal:comment> <tal:comment replace="nothing">Create (a) new object(s) by importing data</tal:comment>
<img style="cursor:pointer" <a tal:condition="python: ('import' in createMeans) and userMayAdd"
tal:condition="python: ('import' in createMeans) and userMayAdd" tal:attributes="href python: '%s/ui/import?className=%s' % (toolUrl, rootClass);
tal:attributes="onClick python: 'href: window.location=\'%s/ui/import?className=%s\'' % (toolUrl, rootClass); title python: _('query_import')">
src string: $appUrl/ui/import.png; <img tal:attributes="src string: $appUrl/ui/import.png"/>
title python: _('query_import')"/> </a>
<tal:comment replace="nothing">Search objects of this type</tal:comment> <tal:comment replace="nothing">Search objects of this type</tal:comment>
<img style="cursor:pointer" <a tal:define="showSearch python: tool.getAttr('enableAdvancedSearchFor%s' % rootClass)"
tal:define="showSearch python: tool.getAttr('enableAdvancedSearchFor%s' % rootClass)" tal:condition="showSearch"
tal:condition="showSearch" tal:attributes="href python: '%s/ui/search?className=%s' % (toolUrl, rootClass);
tal:attributes="onClick python: 'href: window.location=\'%s/ui/search?className=%s\'' % (toolUrl, rootClass); title python: _('search_objects')">
src string: $appUrl/ui/search.gif; <img tal:attributes="src string: $appUrl/ui/search.gif"/>
title python: _('search_objects')"/> </a>
</td> </td>
</tr> </tr>
</table> </table>

View file

@ -88,7 +88,8 @@
<tr> <tr>
<td> <td>
<tal:comment replace="nothing">The user login form for anonymous users</tal:comment> <tal:comment replace="nothing">The user login form for anonymous users</tal:comment>
<table align="center" tal:condition="isAnon" class="login"> <table align="center" tal:condition="python: isAnon and ('/temp_folder/' not in req['ACTUAL_URL'])"
class="login">
<tr><td> <tr><td>
<form name="loginform" method="post" <form name="loginform" method="post"
tal:attributes="action python: tool.absolute_url() + '/performLogin'"> tal:attributes="action python: tool.absolute_url() + '/performLogin'">
@ -116,17 +117,23 @@
<img tal:attributes="src string: $appUrl/ui/home.gif"/> <img tal:attributes="src string: $appUrl/ui/home.gif"/>
</a> </a>
<!-- Config --> <!-- Config -->
<img style="cursor:pointer" tal:condition="python: user.has_role('Manager')" <a tal:condition="python: user.has_role('Manager')"
tal:attributes="onClick python: 'href: window.location=\'%s\'' % tool.getUrl(page='main', nav=''); tal:attributes="href python: tool.getUrl(page='main', nav='');
title python: _('%sTool' % appName); title python: _('%sTool' % appName)">
src string:$appUrl/ui/appyConfig.gif"/> <img tal:attributes="src string:$appUrl/ui/appyConfig.gif"/>
</a>
<!-- Logout --> <!-- Logout -->
<a tal:attributes="href python: tool.absolute_url() + '/performLogout'; <a tal:attributes="href python: tool.absolute_url() + '/performLogout';
title python: _('logout')"> title python: _('logout')">
<img tal:attributes="src string: $appUrl/ui/logout.gif"/> <img tal:attributes="src string: $appUrl/ui/logout.gif"/>
</a> </a>
</td> </td>
<td align="right" tal:content="python: tool.getUserLine(user)"></td> <td align="right" tal:define="userInfo tool/getUserLine">
<span tal:content="python: userInfo[0]"></span>
<a tal:attributes="href python: userInfo[1]">
<img tal:attributes="src string: $appUrl/ui/edit.gif"/>
</a>
</td>
</tr> </tr>
</table> </table>
</td> </td>

View file

@ -26,7 +26,7 @@
tal:define="fmt widget/format; tal:define="fmt widget/format;
isSelect widget/isSelect; isSelect widget/isSelect;
isMaster widget/slaves; isMaster widget/slaves;
isOneLine python: fmt in (0,3); isOneLine python: fmt in (0,3,4);
maxChars python: test(widget['maxChars'], widget['maxChars'], '')"> maxChars python: test(widget['maxChars'], widget['maxChars'], '')">
<tal:choice condition="isSelect"> <tal:choice condition="isSelect">
@ -49,6 +49,12 @@
value python: test(inRequest, requestValue, value); value python: test(inRequest, requestValue, value);
style python: 'text-transform:%s' % widget['transform']; style python: 'text-transform:%s' % widget['transform'];
type python: (widget['format'] == 3) and 'password' or 'text'"/> type python: (widget['format'] == 3) and 'password' or 'text'"/>
<tal:comment replace="nothing">Display a captcha if required</tal:comment>
<tal:captcha condition="python: widget['format'] == 4">
<span tal:define="challenge python: contextObj.getCaptchaChallenge(name)"
tal:content="python: _('captcha_text', mapping=challenge)">
</span>
</tal:captcha>
</tal:line> </tal:line>
<tal:textarea condition="python: fmt in (1,2)"> <tal:textarea condition="python: fmt in (1,2)">
<textarea tal:attributes="id name; name name; <textarea tal:attributes="id name; name name;

View file

@ -19,10 +19,11 @@ def createObject(folder, id, className, appName, wf=True):
obj.id = id obj.id = id
obj._at_uid = id obj._at_uid = id
userId = obj.getUser().getId() userId = obj.getUser().getId()
obj.creator = userId # If user is anonymous, userIs is None
obj.creator = userId or 'Anonymous User'
from DateTime import DateTime from DateTime import DateTime
obj.created = DateTime() obj.created = DateTime()
obj.__ac_local_roles__ = { userId: ['Owner'] } obj.__ac_local_roles__ = { userId: ['Owner'] } # userId can be None (anon).
if wf: obj.notifyWorkflowCreated() if wf: obj.notifyWorkflowCreated()
return obj return obj

View file

@ -70,5 +70,9 @@ class GroupWrapper(AbstractWrapper):
self.log('User "%s" added to group "%s".' % \ self.log('User "%s" added to group "%s".' % \
(user.login, self.login)) (user.login, self.login))
if hasattr(self.o.aq_base, '_oldUsers'): del self.o._oldUsers if hasattr(self.o.aq_base, '_oldUsers'): del self.o._oldUsers
# If the group was created by an Anonymous, Anonymous can't stay Owner
# of the object.
if None in self.o.__ac_local_roles__:
del self.o.__ac_local_roles__[None]
return self._callCustom('onEdit', created) return self._callCustom('onEdit', created)
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -67,9 +67,13 @@ class UserWrapper(AbstractWrapper):
# Updates roles at the Zope level. # Updates roles at the Zope level.
zopeUser = aclUsers.getUserById(login) zopeUser = aclUsers.getUserById(login)
zopeUser.roles = self.roles zopeUser.roles = self.roles
# "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',))
# If the user was created by an Anonymous, Anonymous can't stay Owner
# of the object.
if None in self.o.__ac_local_roles__:
del self.o.__ac_local_roles__[None]
return self._callCustom('onEdit', created) return self._callCustom('onEdit', created)
def getZopeUser(self): def getZopeUser(self):

View file

@ -56,7 +56,7 @@ 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.search('User', login=self.o.getUser().getId())[0] return self.search1('User', 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

@ -198,6 +198,7 @@ class Converter:
# Loads the document to convert in a new hidden frame # Loads the document to convert in a new hidden frame
prop = PropertyValue(); prop.Name = 'Hidden'; prop.Value = True prop = PropertyValue(); prop.Name = 'Hidden'; prop.Value = True
if self.inputType == 'csv': if self.inputType == 'csv':
# Give some additional params if we need to open a CSV file
prop2 = PropertyValue() prop2 = PropertyValue()
prop2.Name = 'FilterFlags' prop2.Name = 'FilterFlags'
prop2.Value = '59,34,76,1' prop2.Value = '59,34,76,1'
@ -206,7 +207,6 @@ class Converter:
props = (prop, prop2) props = (prop, prop2)
else: else:
props = (prop,) props = (prop,)
# Give some additional params if we need to open a CSV file
self.doc = self.oo.loadComponentFromURL(self.docUrl, "_blank", 0, self.doc = self.oo.loadComponentFromURL(self.docUrl, "_blank", 0,
props) props)
if self.inputType == 'odt': if self.inputType == 'odt':