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:
|
This proposal supersedes a proposal from 2009-10-09.
Commit message
Description of the change
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
Forrest Aldridge (forrest-aldridge) wrote : Posted in a previous version of this proposal | # |
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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'
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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.
![](/+icing/build/overlay/assets/skins/sam/images/close.gif)
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 | 168 | * Set the input node's value to C{value} and passes the result of | 168 | * Set the input node's value to C{value} and passes the result of |
6 | 169 | * C{control.getValue} to C{control.baseValidator}. | 169 | * C{control.getValue} to C{control.baseValidator}. |
7 | 170 | */ | 170 | */ |
10 | 171 | function assertValidInput(self, control, value) { | 171 | function assertValidInput(self, control, value, msg) { |
9 | 172 | var oldValue = control.inputNode.value; | ||
11 | 173 | control.inputNode.value = value; | 172 | control.inputNode.value = value; |
14 | 174 | var msg = (Methanal.Util.repr(value) + ' is NOT valid input for ' + | 173 | if (msg === undefined) { |
15 | 175 | Methanal.Util.repr(control)); | 174 | msg = (Methanal.Util.repr(value) + ' is NOT valid input for ' + |
16 | 175 | Methanal.Util.repr(control)); | ||
17 | 176 | } | ||
18 | 176 | self.assertIdentical( | 177 | self.assertIdentical( |
19 | 177 | control.baseValidator(control.getValue()), undefined, msg); | 178 | control.baseValidator(control.getValue()), undefined, msg); |
20 | 178 | control.inputNode.value = oldValue; | ||
21 | 179 | }, | 179 | }, |
22 | 180 | 180 | ||
23 | 181 | 181 | ||
24 | @@ -754,6 +754,120 @@ | |||
25 | 754 | 754 | ||
26 | 755 | 755 | ||
27 | 756 | /** | 756 | /** |
28 | 757 | * Tests for L{Methanal.View.VerifiedPasswordInput}. | ||
29 | 758 | */ | ||
30 | 759 | Methanal.Tests.TestView.BaseTestTextInput.subclass(Methanal.Tests.TestView, 'TestVerifiedPasswordInput').methods( | ||
31 | 760 | function setUp(self) { | ||
32 | 761 | self.controlType = Methanal.View.VerifiedPasswordInput; | ||
33 | 762 | }, | ||
34 | 763 | |||
35 | 764 | |||
36 | 765 | /** | ||
37 | 766 | * Assert that C{password}, and optionally C{confirmPassword}, are a good | ||
38 | 767 | * input. | ||
39 | 768 | */ | ||
40 | 769 | function assertGoodPassword(self, control, password, confirmPassword) { | ||
41 | 770 | if (confirmPassword === undefined) { | ||
42 | 771 | confirmPassword = password; | ||
43 | 772 | } | ||
44 | 773 | control._confirmPasswordNode.value = confirmPassword; | ||
45 | 774 | self.assertValidInput( | ||
46 | 775 | control, password, | ||
47 | 776 | Methanal.Util.repr(password) + ' is NOT a good password'); | ||
48 | 777 | }, | ||
49 | 778 | |||
50 | 779 | |||
51 | 780 | /** | ||
52 | 781 | * Assert that C{password}, and optionally C{confirmPassword}, are a bad | ||
53 | 782 | * input. | ||
54 | 783 | */ | ||
55 | 784 | function assertBadPassword(self, control, password, confirmPassword) { | ||
56 | 785 | if (confirmPassword === undefined) { | ||
57 | 786 | confirmPassword = password; | ||
58 | 787 | } | ||
59 | 788 | control._confirmPasswordNode.value = confirmPassword; | ||
60 | 789 | self.assertInvalidInput( | ||
61 | 790 | control, password, | ||
62 | 791 | Methanal.Util.repr(password) + ' IS a good password'); | ||
63 | 792 | }, | ||
64 | 793 | |||
65 | 794 | |||
66 | 795 | function createControl(self, args) { | ||
67 | 796 | var control = Methanal.Tests.TestView.TestVerifiedPasswordInput.upcall( | ||
68 | 797 | self, 'createControl', args); | ||
69 | 798 | Methanal.Tests.TestView.makeWidgetChildNode( | ||
70 | 799 | control, 'input', 'confirmPassword'); | ||
71 | 800 | return control; | ||
72 | 801 | }, | ||
73 | 802 | |||
74 | 803 | |||
75 | 804 | /** | ||
76 | 805 | * Validation will fail under the following conditions: | ||
77 | 806 | * 1. The input and confirmPasswordNode node values don't match. | ||
78 | 807 | * 2. If either of the above node values have no length (are blank). | ||
79 | 808 | */ | ||
80 | 809 | function test_inputValidation(self) { | ||
81 | 810 | self.testControl({value: null}, | ||
82 | 811 | function (control) { | ||
83 | 812 | // Test condition 1 | ||
84 | 813 | self.assertBadPassword(control, 'match', 'no match'); | ||
85 | 814 | self.assertGoodPassword(control, 'match', 'match'); | ||
86 | 815 | // Test condition 2 | ||
87 | 816 | self.assertBadPassword(control, '', ''); | ||
88 | 817 | }); | ||
89 | 818 | }, | ||
90 | 819 | |||
91 | 820 | |||
92 | 821 | /** | ||
93 | 822 | * Changing the password strength criteria results in different validation | ||
94 | 823 | * criteria for the control. | ||
95 | 824 | */ | ||
96 | 825 | function test_strengthCriteria(self) { | ||
97 | 826 | // Override the default criteria of 5 or more characters. | ||
98 | 827 | self.testControl({value: null, minPasswordLength: 3}, | ||
99 | 828 | function (control) { | ||
100 | 829 | self.assertBadPassword(control, '12'); | ||
101 | 830 | self.assertGoodPassword(control, '123'); | ||
102 | 831 | |||
103 | 832 | control.setStrengthCriteria(['ALPHA']); | ||
104 | 833 | self.assertGoodPassword(control, 'Abc'); | ||
105 | 834 | self.assertBadPassword(control, '123'); | ||
106 | 835 | |||
107 | 836 | control.setStrengthCriteria(['NUMERIC']); | ||
108 | 837 | self.assertBadPassword(control, 'Abc'); | ||
109 | 838 | self.assertGoodPassword(control, '123'); | ||
110 | 839 | |||
111 | 840 | control.setStrengthCriteria(['ALPHA', 'NUMERIC']); | ||
112 | 841 | self.assertBadPassword(control, 'Abc'); | ||
113 | 842 | self.assertBadPassword(control, '123'); | ||
114 | 843 | self.assertGoodPassword(control, 'Abc123'); | ||
115 | 844 | |||
116 | 845 | control.setStrengthCriteria(['MIXEDCASE']); | ||
117 | 846 | self.assertGoodPassword(control, 'Abc'); | ||
118 | 847 | self.assertGoodPassword(control, 'abC'); | ||
119 | 848 | self.assertBadPassword(control, 'abc'); | ||
120 | 849 | self.assertBadPassword(control, '123'); | ||
121 | 850 | |||
122 | 851 | control.setStrengthCriteria(['SYMBOLS']); | ||
123 | 852 | self.assertGoodPassword(control, '!@#_'); | ||
124 | 853 | self.assertBadPassword(control, ' '); | ||
125 | 854 | self.assertBadPassword(control, 'abc'); | ||
126 | 855 | |||
127 | 856 | control.setStrengthCriteria([]); | ||
128 | 857 | self.assertGoodPassword(control, '!@#_'); | ||
129 | 858 | self.assertGoodPassword(control, 'abc'); | ||
130 | 859 | self.assertGoodPassword(control, '123'); | ||
131 | 860 | |||
132 | 861 | self.assertThrows(Methanal.View.InvalidStrengthCriterion, | ||
133 | 862 | function () { | ||
134 | 863 | control.setStrengthCriteria(['DANGERWILLROBINSON']); | ||
135 | 864 | }); | ||
136 | 865 | }); | ||
137 | 866 | }); | ||
138 | 867 | |||
139 | 868 | |||
140 | 869 | |||
141 | 870 | /** | ||
142 | 757 | * Tests for L{Methanal.View.InputContainer}. | 871 | * Tests for L{Methanal.View.InputContainer}. |
143 | 758 | */ | 872 | */ |
144 | 759 | Methanal.Tests.TestView.BaseTestTextInput.subclass(Methanal.Tests.TestView, 'TestFormGroup').methods( | 873 | Methanal.Tests.TestView.BaseTestTextInput.subclass(Methanal.Tests.TestView, 'TestFormGroup').methods( |
145 | 760 | 874 | ||
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 | 1793 | return 'Percentage values must be between 0% and 100%' | 1793 | return 'Percentage values must be between 0% and 100%' |
151 | 1794 | } | 1794 | } |
152 | 1795 | }); | 1795 | }); |
153 | 1796 | |||
154 | 1797 | |||
155 | 1798 | |||
156 | 1799 | /** | ||
157 | 1800 | * An invalid password strength criterion was specified. | ||
158 | 1801 | */ | ||
159 | 1802 | Divmod.Error.subclass(Methanal.View, 'InvalidStrengthCriterion'); | ||
160 | 1803 | |||
161 | 1804 | |||
162 | 1805 | |||
163 | 1806 | /** | ||
164 | 1807 | * Password input with a verification field and strength checking. | ||
165 | 1808 | */ | ||
166 | 1809 | Methanal.View.TextInput.subclass(Methanal.View, 'VerifiedPasswordInput').methods( | ||
167 | 1810 | function __init__(self, node, args) { | ||
168 | 1811 | Methanal.View.VerifiedPasswordInput.upcall( | ||
169 | 1812 | self, '__init__', node, args); | ||
170 | 1813 | self._minPasswordLength = args.minPasswordLength || 5; | ||
171 | 1814 | self.setStrengthCriteria(args.strengthCriteria || []); | ||
172 | 1815 | }, | ||
173 | 1816 | |||
174 | 1817 | |||
175 | 1818 | function nodeInserted(self) { | ||
176 | 1819 | self._confirmPasswordNode = self.nodeById('confirmPassword'); | ||
177 | 1820 | Methanal.View.VerifiedPasswordInput.upcall(self, 'nodeInserted'); | ||
178 | 1821 | }, | ||
179 | 1822 | |||
180 | 1823 | |||
181 | 1824 | /** | ||
182 | 1825 | * Set the password strength criteria. | ||
183 | 1826 | * | ||
184 | 1827 | * @type criteria: C{Array} of C{String} | ||
185 | 1828 | * @param criteria: An array of names, matching those found in | ||
186 | 1829 | * L{Methanal.View.VerifiedPasswordInput.STRENGTH_CRITERIA}, indicating | ||
187 | 1830 | * the password strength criteria | ||
188 | 1831 | */ | ||
189 | 1832 | function setStrengthCriteria(self, criteria) { | ||
190 | 1833 | var fns = Methanal.View.VerifiedPasswordInput.STRENGTH_CRITERIA; | ||
191 | 1834 | for (var i = 0; i < criteria.length; ++i) { | ||
192 | 1835 | var criterion = criteria[i]; | ||
193 | 1836 | if (fns[criterion] === undefined) { | ||
194 | 1837 | criterion = Methanal.Util.repr(criterion); | ||
195 | 1838 | throw Methanal.View.InvalidStrengthCriterion(criterion); | ||
196 | 1839 | } | ||
197 | 1840 | } | ||
198 | 1841 | self._strengthCriteria = criteria; | ||
199 | 1842 | }, | ||
200 | 1843 | |||
201 | 1844 | |||
202 | 1845 | /** | ||
203 | 1846 | * Override this method to change the definition of a 'strong' password. | ||
204 | 1847 | */ | ||
205 | 1848 | function passwordIsStrong(self, password) { | ||
206 | 1849 | if (password.length < self._minPasswordLength) { | ||
207 | 1850 | return false; | ||
208 | 1851 | } | ||
209 | 1852 | |||
210 | 1853 | var fns = Methanal.View.VerifiedPasswordInput.STRENGTH_CRITERIA; | ||
211 | 1854 | for (var i = 0; i < self._strengthCriteria.length; ++i) { | ||
212 | 1855 | var fn = fns[self._strengthCriteria[i]]; | ||
213 | 1856 | if (!fn(password)) { | ||
214 | 1857 | return false; | ||
215 | 1858 | } | ||
216 | 1859 | } | ||
217 | 1860 | return true; | ||
218 | 1861 | }, | ||
219 | 1862 | |||
220 | 1863 | |||
221 | 1864 | /** | ||
222 | 1865 | * This default validator ensures that the password is strong and that | ||
223 | 1866 | * the password given in both fields have length > 0 and match exactly. | ||
224 | 1867 | */ | ||
225 | 1868 | function baseValidator(self, value) { | ||
226 | 1869 | if (value !== self._confirmPasswordNode.value || value === null || | ||
227 | 1870 | self._confirmPasswordNode.value === null) { | ||
228 | 1871 | return 'Passwords do not match.'; | ||
229 | 1872 | } | ||
230 | 1873 | |||
231 | 1874 | if (!self.passwordIsStrong(value)) { | ||
232 | 1875 | return 'Password is too weak.'; | ||
233 | 1876 | } | ||
234 | 1877 | }); | ||
235 | 1878 | |||
236 | 1879 | |||
237 | 1880 | |||
238 | 1881 | Methanal.View.VerifiedPasswordInput.STRENGTH_CRITERIA = { | ||
239 | 1882 | 'ALPHA': function (value) { return /[a-zA-Z]/.test(value); }, | ||
240 | 1883 | 'NUMERIC': function (value) { return /[0-9]/.test(value); }, | ||
241 | 1884 | 'MIXEDCASE': function (value) { | ||
242 | 1885 | return /[a-z]/.test(value) && /[A-Z]/.test(value); }, | ||
243 | 1886 | 'SYMBOLS': function (value) { return /[^A-Za-z0-9\s]/.test(value); }}; | ||
244 | 1796 | 1887 | ||
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 | 1 | <div xmlns:nevow="http://nevow.com/ns/nevow/0.1" xmlns:athena="http://divmod.org/ns/athena/0.7" nevow:render="liveElement"> | ||
250 | 2 | <input class="methanal-input" type="password"> | ||
251 | 3 | <nevow:attr name="value" nevow:render="value" /> | ||
252 | 4 | <athena:handler event="onchange" handler="onChange" /> | ||
253 | 5 | <athena:handler event="onkeyup" handler="onKeyUp" /> | ||
254 | 6 | <athena:handler event="onblur" handler="onBlur" /> | ||
255 | 7 | <athena:handler event="onfocus" handler="onFocus" /> | ||
256 | 8 | </input> | ||
257 | 9 | <input class="methanal-input" type="password" id="confirmPassword"> | ||
258 | 10 | <nevow:attr name="value" nevow:render="value" /> | ||
259 | 11 | <athena:handler event="onchange" handler="onChange" /> | ||
260 | 12 | </input> | ||
261 | 13 | <span style="display: none;" class="friendly-representation" id="displayValue" /> | ||
262 | 14 | <span class="methanal-error" id="error" /> | ||
263 | 15 | </div> | ||
264 | 0 | 16 | ||
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 | 511 | 511 | ||
270 | 512 | 512 | ||
271 | 513 | 513 | ||
272 | 514 | class VerifiedPasswordInput(TextInput): | ||
273 | 515 | """ | ||
274 | 516 | Password input with verification and strength checking. | ||
275 | 517 | |||
276 | 518 | @type minPasswordLength: C{int} | ||
277 | 519 | @ivar minPasswordLength: Minimum acceptable password length, or C{None} | ||
278 | 520 | to use the default client-side value | ||
279 | 521 | |||
280 | 522 | @type strengthCriteria: C{list} of C{unicode} | ||
281 | 523 | @ivar strengthCriteria: A list of criteria names for password strength | ||
282 | 524 | testing, or C{None} for no additional strength criteria. See | ||
283 | 525 | L{Methanal.View.VerifiedPasswordInput.STRENGTH_CRITERIA} in the | ||
284 | 526 | Javascript source for possible values | ||
285 | 527 | """ | ||
286 | 528 | fragmentName = 'methanal-verified-password-input' | ||
287 | 529 | jsClass = u'Methanal.View.VerifiedPasswordInput' | ||
288 | 530 | |||
289 | 531 | |||
290 | 532 | def __init__(self, minPasswordLength=None, strengthCriteria=None, **kw): | ||
291 | 533 | super(VerifiedPasswordInput, self).__init__(**kw) | ||
292 | 534 | self.minPasswordLength = minPasswordLength | ||
293 | 535 | if strengthCriteria is None: | ||
294 | 536 | strengthCriteria = [] | ||
295 | 537 | self.strengthCriteria = strengthCriteria | ||
296 | 538 | |||
297 | 539 | |||
298 | 540 | def getArgs(self): | ||
299 | 541 | return {u'minPasswordLength': self.minPasswordLength, | ||
300 | 542 | u'strengthCriteria': self.strengthCriteria} | ||
301 | 543 | |||
302 | 544 | |||
303 | 545 | |||
304 | 514 | class ChoiceInput(FormInput): | 546 | class ChoiceInput(FormInput): |
305 | 515 | """ | 547 | """ |
306 | 516 | Abstract input with multiple options. | 548 | 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'