Merge lp:~jjacobs/methanal/verified-password-input into lp:methanal
- verified-password-input
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Tristan Seligmann |
Approved revision: | 114 |
Merged at revision: | not available |
Proposed branch: | lp:~jjacobs/methanal/verified-password-input |
Merge into: | lp:methanal |
Diff against target: |
306 lines 4 files modified
methanal/js/Methanal/Tests/TestView.js (+119/-5) methanal/js/Methanal/View.js (+91/-0) methanal/themes/methanal-base/methanal-verified-password-input.html (+15/-0) methanal/view.py (+32/-0) |
To merge this branch: | bzr merge lp:~jjacobs/methanal/verified-password-input |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tristan Seligmann | Approve | ||
Forrest Aldridge (community) | Needs Fixing | ||
Review via email: mp+13138@code.launchpad.net |
This proposal supersedes a proposal from 2009-10-09.
Commit message
Description of the change
Forrest Aldridge (forrest-aldridge) wrote : Posted in a previous version of this proposal | # |
Tristan Seligmann (mithrandi) wrote : | # |
1. Saving and restoring control.
2. In general, the pattern "throw new Error('Foo bar:' + c);" is better written as "throw new FooBar(c);"; embedding the exception "class" in text makes it challenging to catch the specific exception without doing something yucky like substring matching. In this specific case, setStrengthCriteria should probably throw UnknownStrength
Forrest Aldridge (forrest-aldridge) wrote : | # |
1. The docstring in TestView.js on line 834 is an incomplete sentence and is slightly unclear.
2. This is a matter of preference really, but I think it would be clearer if the variable c in View.js in lines 1828-1831 were renamed to 'criterion'
Jonathan Jacobs (jjacobs) wrote : | # |
> 1. The docstring in TestView.js on line 834 is an incomplete sentence and is
> slightly unclear.
Oops, sorry.
> 2. This is a matter of preference really, but I think it would be clearer if
> the variable c in View.js in lines 1828-1831 were renamed to 'criterion'
It was probably renamed to reduce it's width and then something else happened that stopped that from mattering.
Jonathan Jacobs (jjacobs) wrote : | # |
> 1. Saving and restoring control.
> relies on this behaviour, and the first password input isn't saved and
> restored, so it's somewhat inconsistent.
This behaviour has been removed, it didn't seem to matter to anything, I guess if you need a clean slate, write another test case.
> 2. In general, the pattern "throw new Error('Foo bar:' + c);" is better
> written as "throw new FooBar(c);"; embedding the exception "class" in text
> makes it challenging to catch the specific exception without doing something
> yucky like substring matching. In this specific case, setStrengthCriteria
> should probably throw UnknownStrength
> something like that.
I was being lazy.
Tristan Seligmann (mithrandi) : | # |
Preview Diff
1 | === modified file 'methanal/js/Methanal/Tests/TestView.js' |
2 | --- methanal/js/Methanal/Tests/TestView.js 2009-10-04 11:18:09 +0000 |
3 | +++ methanal/js/Methanal/Tests/TestView.js 2009-10-09 22:42:12 +0000 |
4 | @@ -168,14 +168,14 @@ |
5 | * Set the input node's value to C{value} and passes the result of |
6 | * C{control.getValue} to C{control.baseValidator}. |
7 | */ |
8 | - function assertValidInput(self, control, value) { |
9 | - var oldValue = control.inputNode.value; |
10 | + function assertValidInput(self, control, value, msg) { |
11 | control.inputNode.value = value; |
12 | - var msg = (Methanal.Util.repr(value) + ' is NOT valid input for ' + |
13 | - Methanal.Util.repr(control)); |
14 | + if (msg === undefined) { |
15 | + msg = (Methanal.Util.repr(value) + ' is NOT valid input for ' + |
16 | + Methanal.Util.repr(control)); |
17 | + } |
18 | self.assertIdentical( |
19 | control.baseValidator(control.getValue()), undefined, msg); |
20 | - control.inputNode.value = oldValue; |
21 | }, |
22 | |
23 | |
24 | @@ -754,6 +754,120 @@ |
25 | |
26 | |
27 | /** |
28 | + * Tests for L{Methanal.View.VerifiedPasswordInput}. |
29 | + */ |
30 | +Methanal.Tests.TestView.BaseTestTextInput.subclass(Methanal.Tests.TestView, 'TestVerifiedPasswordInput').methods( |
31 | + function setUp(self) { |
32 | + self.controlType = Methanal.View.VerifiedPasswordInput; |
33 | + }, |
34 | + |
35 | + |
36 | + /** |
37 | + * Assert that C{password}, and optionally C{confirmPassword}, are a good |
38 | + * input. |
39 | + */ |
40 | + function assertGoodPassword(self, control, password, confirmPassword) { |
41 | + if (confirmPassword === undefined) { |
42 | + confirmPassword = password; |
43 | + } |
44 | + control._confirmPasswordNode.value = confirmPassword; |
45 | + self.assertValidInput( |
46 | + control, password, |
47 | + Methanal.Util.repr(password) + ' is NOT a good password'); |
48 | + }, |
49 | + |
50 | + |
51 | + /** |
52 | + * Assert that C{password}, and optionally C{confirmPassword}, are a bad |
53 | + * input. |
54 | + */ |
55 | + function assertBadPassword(self, control, password, confirmPassword) { |
56 | + if (confirmPassword === undefined) { |
57 | + confirmPassword = password; |
58 | + } |
59 | + control._confirmPasswordNode.value = confirmPassword; |
60 | + self.assertInvalidInput( |
61 | + control, password, |
62 | + Methanal.Util.repr(password) + ' IS a good password'); |
63 | + }, |
64 | + |
65 | + |
66 | + function createControl(self, args) { |
67 | + var control = Methanal.Tests.TestView.TestVerifiedPasswordInput.upcall( |
68 | + self, 'createControl', args); |
69 | + Methanal.Tests.TestView.makeWidgetChildNode( |
70 | + control, 'input', 'confirmPassword'); |
71 | + return control; |
72 | + }, |
73 | + |
74 | + |
75 | + /** |
76 | + * Validation will fail under the following conditions: |
77 | + * 1. The input and confirmPasswordNode node values don't match. |
78 | + * 2. If either of the above node values have no length (are blank). |
79 | + */ |
80 | + function test_inputValidation(self) { |
81 | + self.testControl({value: null}, |
82 | + function (control) { |
83 | + // Test condition 1 |
84 | + self.assertBadPassword(control, 'match', 'no match'); |
85 | + self.assertGoodPassword(control, 'match', 'match'); |
86 | + // Test condition 2 |
87 | + self.assertBadPassword(control, '', ''); |
88 | + }); |
89 | + }, |
90 | + |
91 | + |
92 | + /** |
93 | + * Changing the password strength criteria results in different validation |
94 | + * criteria for the control. |
95 | + */ |
96 | + function test_strengthCriteria(self) { |
97 | + // Override the default criteria of 5 or more characters. |
98 | + self.testControl({value: null, minPasswordLength: 3}, |
99 | + function (control) { |
100 | + self.assertBadPassword(control, '12'); |
101 | + self.assertGoodPassword(control, '123'); |
102 | + |
103 | + control.setStrengthCriteria(['ALPHA']); |
104 | + self.assertGoodPassword(control, 'Abc'); |
105 | + self.assertBadPassword(control, '123'); |
106 | + |
107 | + control.setStrengthCriteria(['NUMERIC']); |
108 | + self.assertBadPassword(control, 'Abc'); |
109 | + self.assertGoodPassword(control, '123'); |
110 | + |
111 | + control.setStrengthCriteria(['ALPHA', 'NUMERIC']); |
112 | + self.assertBadPassword(control, 'Abc'); |
113 | + self.assertBadPassword(control, '123'); |
114 | + self.assertGoodPassword(control, 'Abc123'); |
115 | + |
116 | + control.setStrengthCriteria(['MIXEDCASE']); |
117 | + self.assertGoodPassword(control, 'Abc'); |
118 | + self.assertGoodPassword(control, 'abC'); |
119 | + self.assertBadPassword(control, 'abc'); |
120 | + self.assertBadPassword(control, '123'); |
121 | + |
122 | + control.setStrengthCriteria(['SYMBOLS']); |
123 | + self.assertGoodPassword(control, '!@#_'); |
124 | + self.assertBadPassword(control, ' '); |
125 | + self.assertBadPassword(control, 'abc'); |
126 | + |
127 | + control.setStrengthCriteria([]); |
128 | + self.assertGoodPassword(control, '!@#_'); |
129 | + self.assertGoodPassword(control, 'abc'); |
130 | + self.assertGoodPassword(control, '123'); |
131 | + |
132 | + self.assertThrows(Methanal.View.InvalidStrengthCriterion, |
133 | + function () { |
134 | + control.setStrengthCriteria(['DANGERWILLROBINSON']); |
135 | + }); |
136 | + }); |
137 | + }); |
138 | + |
139 | + |
140 | + |
141 | +/** |
142 | * Tests for L{Methanal.View.InputContainer}. |
143 | */ |
144 | Methanal.Tests.TestView.BaseTestTextInput.subclass(Methanal.Tests.TestView, 'TestFormGroup').methods( |
145 | |
146 | === modified file 'methanal/js/Methanal/View.js' |
147 | --- methanal/js/Methanal/View.js 2009-10-05 00:15:43 +0000 |
148 | +++ methanal/js/Methanal/View.js 2009-10-09 22:42:12 +0000 |
149 | @@ -1793,3 +1793,94 @@ |
150 | return 'Percentage values must be between 0% and 100%' |
151 | } |
152 | }); |
153 | + |
154 | + |
155 | + |
156 | +/** |
157 | + * An invalid password strength criterion was specified. |
158 | + */ |
159 | +Divmod.Error.subclass(Methanal.View, 'InvalidStrengthCriterion'); |
160 | + |
161 | + |
162 | + |
163 | +/** |
164 | + * Password input with a verification field and strength checking. |
165 | + */ |
166 | +Methanal.View.TextInput.subclass(Methanal.View, 'VerifiedPasswordInput').methods( |
167 | + function __init__(self, node, args) { |
168 | + Methanal.View.VerifiedPasswordInput.upcall( |
169 | + self, '__init__', node, args); |
170 | + self._minPasswordLength = args.minPasswordLength || 5; |
171 | + self.setStrengthCriteria(args.strengthCriteria || []); |
172 | + }, |
173 | + |
174 | + |
175 | + function nodeInserted(self) { |
176 | + self._confirmPasswordNode = self.nodeById('confirmPassword'); |
177 | + Methanal.View.VerifiedPasswordInput.upcall(self, 'nodeInserted'); |
178 | + }, |
179 | + |
180 | + |
181 | + /** |
182 | + * Set the password strength criteria. |
183 | + * |
184 | + * @type criteria: C{Array} of C{String} |
185 | + * @param criteria: An array of names, matching those found in |
186 | + * L{Methanal.View.VerifiedPasswordInput.STRENGTH_CRITERIA}, indicating |
187 | + * the password strength criteria |
188 | + */ |
189 | + function setStrengthCriteria(self, criteria) { |
190 | + var fns = Methanal.View.VerifiedPasswordInput.STRENGTH_CRITERIA; |
191 | + for (var i = 0; i < criteria.length; ++i) { |
192 | + var criterion = criteria[i]; |
193 | + if (fns[criterion] === undefined) { |
194 | + criterion = Methanal.Util.repr(criterion); |
195 | + throw Methanal.View.InvalidStrengthCriterion(criterion); |
196 | + } |
197 | + } |
198 | + self._strengthCriteria = criteria; |
199 | + }, |
200 | + |
201 | + |
202 | + /** |
203 | + * Override this method to change the definition of a 'strong' password. |
204 | + */ |
205 | + function passwordIsStrong(self, password) { |
206 | + if (password.length < self._minPasswordLength) { |
207 | + return false; |
208 | + } |
209 | + |
210 | + var fns = Methanal.View.VerifiedPasswordInput.STRENGTH_CRITERIA; |
211 | + for (var i = 0; i < self._strengthCriteria.length; ++i) { |
212 | + var fn = fns[self._strengthCriteria[i]]; |
213 | + if (!fn(password)) { |
214 | + return false; |
215 | + } |
216 | + } |
217 | + return true; |
218 | + }, |
219 | + |
220 | + |
221 | + /** |
222 | + * This default validator ensures that the password is strong and that |
223 | + * the password given in both fields have length > 0 and match exactly. |
224 | + */ |
225 | + function baseValidator(self, value) { |
226 | + if (value !== self._confirmPasswordNode.value || value === null || |
227 | + self._confirmPasswordNode.value === null) { |
228 | + return 'Passwords do not match.'; |
229 | + } |
230 | + |
231 | + if (!self.passwordIsStrong(value)) { |
232 | + return 'Password is too weak.'; |
233 | + } |
234 | + }); |
235 | + |
236 | + |
237 | + |
238 | +Methanal.View.VerifiedPasswordInput.STRENGTH_CRITERIA = { |
239 | + 'ALPHA': function (value) { return /[a-zA-Z]/.test(value); }, |
240 | + 'NUMERIC': function (value) { return /[0-9]/.test(value); }, |
241 | + 'MIXEDCASE': function (value) { |
242 | + return /[a-z]/.test(value) && /[A-Z]/.test(value); }, |
243 | + 'SYMBOLS': function (value) { return /[^A-Za-z0-9\s]/.test(value); }}; |
244 | |
245 | === added file 'methanal/themes/methanal-base/methanal-verified-password-input.html' |
246 | --- methanal/themes/methanal-base/methanal-verified-password-input.html 1970-01-01 00:00:00 +0000 |
247 | +++ methanal/themes/methanal-base/methanal-verified-password-input.html 2009-10-09 22:42:12 +0000 |
248 | @@ -0,0 +1,15 @@ |
249 | +<div xmlns:nevow="http://nevow.com/ns/nevow/0.1" xmlns:athena="http://divmod.org/ns/athena/0.7" nevow:render="liveElement"> |
250 | + <input class="methanal-input" type="password"> |
251 | + <nevow:attr name="value" nevow:render="value" /> |
252 | + <athena:handler event="onchange" handler="onChange" /> |
253 | + <athena:handler event="onkeyup" handler="onKeyUp" /> |
254 | + <athena:handler event="onblur" handler="onBlur" /> |
255 | + <athena:handler event="onfocus" handler="onFocus" /> |
256 | + </input> |
257 | + <input class="methanal-input" type="password" id="confirmPassword"> |
258 | + <nevow:attr name="value" nevow:render="value" /> |
259 | + <athena:handler event="onchange" handler="onChange" /> |
260 | + </input> |
261 | + <span style="display: none;" class="friendly-representation" id="displayValue" /> |
262 | + <span class="methanal-error" id="error" /> |
263 | +</div> |
264 | |
265 | === modified file 'methanal/view.py' |
266 | --- methanal/view.py 2009-10-04 10:17:26 +0000 |
267 | +++ methanal/view.py 2009-10-09 22:42:12 +0000 |
268 | @@ -511,6 +511,38 @@ |
269 | |
270 | |
271 | |
272 | +class VerifiedPasswordInput(TextInput): |
273 | + """ |
274 | + Password input with verification and strength checking. |
275 | + |
276 | + @type minPasswordLength: C{int} |
277 | + @ivar minPasswordLength: Minimum acceptable password length, or C{None} |
278 | + to use the default client-side value |
279 | + |
280 | + @type strengthCriteria: C{list} of C{unicode} |
281 | + @ivar strengthCriteria: A list of criteria names for password strength |
282 | + testing, or C{None} for no additional strength criteria. See |
283 | + L{Methanal.View.VerifiedPasswordInput.STRENGTH_CRITERIA} in the |
284 | + Javascript source for possible values |
285 | + """ |
286 | + fragmentName = 'methanal-verified-password-input' |
287 | + jsClass = u'Methanal.View.VerifiedPasswordInput' |
288 | + |
289 | + |
290 | + def __init__(self, minPasswordLength=None, strengthCriteria=None, **kw): |
291 | + super(VerifiedPasswordInput, self).__init__(**kw) |
292 | + self.minPasswordLength = minPasswordLength |
293 | + if strengthCriteria is None: |
294 | + strengthCriteria = [] |
295 | + self.strengthCriteria = strengthCriteria |
296 | + |
297 | + |
298 | + def getArgs(self): |
299 | + return {u'minPasswordLength': self.minPasswordLength, |
300 | + u'strengthCriteria': self.strengthCriteria} |
301 | + |
302 | + |
303 | + |
304 | class ChoiceInput(FormInput): |
305 | """ |
306 | Abstract input with multiple options. |
1. The docstring in TestView.js on line 834 is an incomplete sentence and is slightly unclear.
2. This is a matter of preference really, but I think it would be clearer if the variable c in View.js in lines 1828-1831 were renamed to 'criterion'