From a80ef513ff8dfaf0a2e9c6bb67a7c7115d44a8df Mon Sep 17 00:00:00 2001 From: Gaetan Delannay Date: Thu, 16 Feb 2012 18:13:51 +0100 Subject: [PATCH] appy.gen: added new format 'captcha' for a String. --- gen/__init__.py | 66 +++++++++++++++++++++++++++--------- gen/generator.py | 2 ++ gen/installer.py | 6 ++-- gen/layout.py | 2 +- gen/mixins/ToolMixin.py | 13 ++++--- gen/mixins/__init__.py | 5 ++- gen/po.py | 4 +++ gen/ui/portlet.pt | 32 ++++++++--------- gen/ui/template.pt | 19 +++++++---- gen/ui/widgets/string.pt | 8 ++++- gen/utils.py | 5 +-- gen/wrappers/GroupWrapper.py | 4 +++ gen/wrappers/UserWrapper.py | 6 +++- gen/wrappers/__init__.py | 2 +- pod/converter.py | 2 +- 15 files changed, 123 insertions(+), 53 deletions(-) diff --git a/gen/__init__.py b/gen/__init__.py index a431967..b4c4711 100644 --- a/gen/__init__.py +++ b/gen/__init__.py @@ -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,18 +1289,25 @@ class String(Type): return res def validateValue(self, obj, value): - if not self.isSelect: return - # Check that the value is among possible values - possibleValues = self.getPossibleValues(obj) - if isinstance(value, basestring): - 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') + 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): + 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', 'ù':'u','û':'u','ü':'u','î':'i','ï':'i','ô':'o','ö':'o', @@ -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() # ------------------------------------------------------------------------------ diff --git a/gen/generator.py b/gen/generator.py index 970df46..04568ee 100644 --- a/gen/generator.py +++ b/gen/generator.py @@ -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(): diff --git a/gen/installer.py b/gen/installer.py index ef43db8..a7530eb 100644 --- a/gen/installer.py +++ b/gen/installer.py @@ -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 diff --git a/gen/layout.py b/gen/layout.py index 51f8d6e..f5a8af0 100644 --- a/gen/layout.py +++ b/gen/layout.py @@ -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'} # ------------------------------------------------------------------------------ diff --git a/gen/mixins/ToolMixin.py b/gen/mixins/ToolMixin.py index 266e350..6c28d3b 100644 --- a/gen/mixins/ToolMixin.py +++ b/gen/mixins/ToolMixin.py @@ -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.''' diff --git a/gen/mixins/__init__.py b/gen/mixins/__init__.py index 04f0fe9..6049ae8 100644 --- a/gen/mixins/__init__.py +++ b/gen/mixins/__init__.py @@ -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.''' diff --git a/gen/po.py b/gen/po.py index 037086d..a0493a0 100644 --- a/gen/po.py +++ b/gen/po.py @@ -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): diff --git a/gen/ui/portlet.pt b/gen/ui/portlet.pt index 5784d18..26db2e1 100644 --- a/gen/ui/portlet.pt +++ b/gen/ui/portlet.pt @@ -30,24 +30,24 @@ userMayAdd python: user.has_permission(addPermission, appFolder); createMeans python: tool.getCreateMeans(rootClass)"> Create a new object from a web form - + + + Create (a) new object(s) by importing data - + + + Search objects of this type - + + + diff --git a/gen/ui/template.pt b/gen/ui/template.pt index 14d569e..317d6e0 100644 --- a/gen/ui/template.pt +++ b/gen/ui/template.pt @@ -88,7 +88,8 @@ The user login form for anonymous users - +
- +
@@ -116,17 +117,23 @@ - + + +
+ + + + +
diff --git a/gen/ui/widgets/string.pt b/gen/ui/widgets/string.pt index 5107f00..e2493f5 100644 --- a/gen/ui/widgets/string.pt +++ b/gen/ui/widgets/string.pt @@ -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'], '')"> @@ -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'"/> + Display a captcha if required + + + +