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 -*-
# ------------------------------------------------------------------------------
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.gen.layout import Table
from appy.gen.layout import defaultFieldLayouts
@ -1105,6 +1106,7 @@ class String(Type):
TEXT = 1
XHTML = 2
PASSWORD = 3
CAPTCHA = 4
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, format=LINE,
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'),
allowImageUpload=True):
# 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
# When format is XHTML, the list of styles that the user will be able to
# select in the styles dropdown is defined hereafter.
@ -1285,7 +1289,14 @@ class String(Type):
return res
def validateValue(self, obj, value):
if not self.isSelect: return
if self.format == String.CAPTCHA:
challenge = obj.REQUEST.SESSION.get('captcha', None)
# Compute the challenge minus the char to remove
i = challenge['number']-1
text = challenge['text'][:i] + challenge['text'][i+1:]
if value != text:
return obj.translate('bad_captcha')
elif self.isSelect:
# Check that the value is among possible values
possibleValues = self.getPossibleValues(obj)
if isinstance(value, basestring):
@ -1348,6 +1359,26 @@ class String(Type):
if (layoutType == 'edit') and (self.format == String.XHTML):
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):
def __init__(self, validator=None, multiplicity=(0,1), index=None,
default=None, optional=False, editDefault=False, show=True,
@ -2636,14 +2667,17 @@ class No:
class WorkflowAnonymous:
'''One-state workflow allowing anyone to consult and Manager to edit.'''
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()
class WorkflowAuthenticated:
'''One-state workflow allowing authenticated users to consult and Manager
to edit.'''
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()
# ------------------------------------------------------------------------------

View file

@ -458,6 +458,8 @@ class ZopeGenerator(Generator):
msg('doc', '', msg.FORMAT_DOC),
msg('rtf', '', msg.FORMAT_RTF),
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
for role in self.getAllUsedRoles():

View file

@ -249,9 +249,11 @@ class ZopeInstaller:
except:
# When Plone has installed PAS in acl_users this may fail. Plone
# 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:
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.')
# Create group "admins" if it does not exist

View file

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

View file

@ -938,14 +938,17 @@ class ToolMixin(BaseMixin):
return [f.__dict__ for f in self.getAllAppyTypes(contentType) \
if (f.type == 'Pod') and (f.show == 'result')]
def getUserLine(self, user):
'''Returns a one-line user info as shown on every page.'''
res = [user.getId()]
rolesToShow = [r for r in user.getRoles() \
def getUserLine(self):
'''Returns a 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.'''
appyUser = self.appy().appyUser
res = [appyUser.title, appyUser.login]
rolesToShow = [r for r in appyUser.roles \
if r not in ('Authenticated', 'Member')]
if 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):
'''Generates a UID for an instance of p_className.'''

View file

@ -418,7 +418,7 @@ class BaseMixin:
return the result.'''
page = self.REQUEST.get('page', 'main')
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, '')
return self.ui.edit(self)
@ -1073,6 +1073,9 @@ class BaseMixin:
obj = self
return appyType.getPossibleValues(obj, withTranslations, withBlankValue)
def getCaptchaChallenge(self, name):
return self.getAppyType(name).getCaptchaChallenge(self.REQUEST.SESSION)
def appy(self):
'''Returns a wrapper object allowing to manipulate p_self the Appy
way.'''

View file

@ -114,6 +114,10 @@ class PoMessage:
FORMAT_PDF = 'PDF'
FORMAT_DOC = 'DOC'
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=[],
niceDefault=False):

View file

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

View file

@ -88,7 +88,8 @@
<tr>
<td>
<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>
<form name="loginform" method="post"
tal:attributes="action python: tool.absolute_url() + '/performLogin'">
@ -116,17 +117,23 @@
<img tal:attributes="src string: $appUrl/ui/home.gif"/>
</a>
<!-- Config -->
<img style="cursor:pointer" tal:condition="python: user.has_role('Manager')"
tal:attributes="onClick python: 'href: window.location=\'%s\'' % tool.getUrl(page='main', nav='');
title python: _('%sTool' % appName);
src string:$appUrl/ui/appyConfig.gif"/>
<a tal:condition="python: user.has_role('Manager')"
tal:attributes="href python: tool.getUrl(page='main', nav='');
title python: _('%sTool' % appName)">
<img tal:attributes="src string:$appUrl/ui/appyConfig.gif"/>
</a>
<!-- Logout -->
<a tal:attributes="href python: tool.absolute_url() + '/performLogout';
title python: _('logout')">
<img tal:attributes="src string: $appUrl/ui/logout.gif"/>
</a>
</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>
</table>
</td>

View file

@ -26,7 +26,7 @@
tal:define="fmt widget/format;
isSelect widget/isSelect;
isMaster widget/slaves;
isOneLine python: fmt in (0,3);
isOneLine python: fmt in (0,3,4);
maxChars python: test(widget['maxChars'], widget['maxChars'], '')">
<tal:choice condition="isSelect">
@ -49,6 +49,12 @@
value python: test(inRequest, requestValue, value);
style python: 'text-transform:%s' % widget['transform'];
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:textarea condition="python: fmt in (1,2)">
<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._at_uid = id
userId = obj.getUser().getId()
obj.creator = userId
# If user is anonymous, userIs is None
obj.creator = userId or 'Anonymous User'
from DateTime import 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()
return obj

View file

@ -70,5 +70,9 @@ class GroupWrapper(AbstractWrapper):
self.log('User "%s" added to group "%s".' % \
(user.login, self.login))
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)
# ------------------------------------------------------------------------------

View file

@ -67,9 +67,13 @@ class UserWrapper(AbstractWrapper):
# Updates roles at the Zope level.
zopeUser = aclUsers.getUserById(login)
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):
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)
def getZopeUser(self):

View file

@ -56,7 +56,7 @@ class AbstractWrapper(object):
elif name == 'user':
return self.o.getUser()
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()
# Now, let's try to return a real attribute.
res = object.__getattribute__(self, name)

View file

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