Merge lp:~edwin-grubbs/launchpad/bug-482176-add-team-member-ajax-part1 into lp:launchpad
- bug-482176-add-team-member-ajax-part1
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Gavin Panella |
Approved revision: | no longer in the source branch. |
Merged at revision: | not available |
Proposed branch: | lp:~edwin-grubbs/launchpad/bug-482176-add-team-member-ajax-part1 |
Merge into: | lp:launchpad |
Prerequisite: | lp:launchpad/db-devel |
Diff against target: |
830 lines (+294/-82) 17 files modified
lib/canonical/launchpad/javascript/bugs/bugtask-index.js (+4/-37) lib/canonical/launchpad/javascript/code/codereview.js (+9/-1) lib/canonical/launchpad/javascript/lp/picker.js (+5/-2) lib/canonical/launchpad/javascript/registry/team.js (+102/-0) lib/lp/app/templates/base-layout-macros.pt (+2/-0) lib/lp/bugs/tests/test_bugs_webservice.py (+0/-10) lib/lp/registry/browser/configure.zcml (+3/-0) lib/lp/registry/browser/person.py (+43/-12) lib/lp/registry/browser/tests/teammembership-views.txt (+1/-0) lib/lp/registry/browser/tests/test_person_webservice.py (+38/-0) lib/lp/registry/doc/teammembership-email-notification.txt (+6/-0) lib/lp/registry/doc/teammembership.txt (+39/-2) lib/lp/registry/interfaces/teammembership.py (+2/-0) lib/lp/registry/model/person.py (+3/-1) lib/lp/registry/model/teammembership.py (+4/-5) lib/lp/registry/templates/team-index.pt (+10/-0) lib/lp/registry/templates/team-portlet-membership.pt (+23/-12) |
To merge this branch: | bzr merge lp:~edwin-grubbs/launchpad/bug-482176-add-team-member-ajax-part1 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Gavin Panella (community) | code | Approve | |
Review via email: mp+16226@code.launchpad.net |
Commit message
Description of the change
Edwin Grubbs (edwin-grubbs) wrote : | # |
Gavin Panella (allenap) wrote : | # |
Hi Edwin,
I've put a lot of comments in the diff below, but none of them are
critical, and a lot of them are suggestions. This is a nice new
feature that works well :)
review approve code
merge approve
Gavin.
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -239,21 +239,6 @@
>
>
> /*
> - * Clear the subscribe someone else picker.
> - *
> - * @method clear_picker
> - * @param e {Object} The event object.
> - */
> -function clear_picker(e) {
> - var input = Y.one('
> - input.set('value', '');
> - this.set('error', '');
> - this.set('results', [{}]);
> - this._results_
> - this.set('batches', []);
> -}
> -
> -/*
> * Initialize click handler for the subscribe someone else link.
> *
> * @method setup_subscribe
> @@ -262,23 +247,14 @@
> function setup_subscribe
> var config = {
> header: 'Subscribe someone else',
> - step_title: 'Search'
> + step_title: 'Search',
> + picker_activator: '.menu-
> };
>
> var picker = Y.lp.picker.create(
> 'ValidPersonOrT
> function(result) { subscribe_
> config);
> - // Clear results and search terms on cancel or save.
> - picker.on('save', clear_picker, picker);
> - picker.on('cancel', clear_picker, picker);
> -
> - var subscription_
> - subscription_
> - e.halt();
> - picker.show();
> - });
> - subscription_
> }
>
> /*
> @@ -626,21 +602,12 @@
> if (Y.Lang.
> var config = {
> header: 'Link a related branch',
> - step_title: 'Search'
> + step_title: 'Search',
> + picker_activator: '.menu-
> };
>
> var picker = Y.lp.picker.create(
> 'Branch', get_branch_
> -
> - // Clear results and search terms on cancel or save.
> - picker.on('save', clear_picker, picker);
> - picker.on('cancel', clear_picker, picker);
> -
> - link_branch_
> - e.halt();
> - picker.show();
> - });
> - link_branch_
> }
> }
Thanks for fixing this up.
>
>
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -22,6 +22,14 @@
> var link = Y.one('
> if (link !== null) {
> link.addClass(
> + /* XXX: salgado, 2009-11-11: This will cause the picker to be
> + * rec...
Preview Diff
1 | === modified file 'lib/canonical/launchpad/javascript/bugs/bugtask-index.js' | |||
2 | --- lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-12-10 20:59:49 +0000 | |||
3 | +++ lib/canonical/launchpad/javascript/bugs/bugtask-index.js 2009-12-16 02:39:23 +0000 | |||
4 | @@ -239,21 +239,6 @@ | |||
5 | 239 | 239 | ||
6 | 240 | 240 | ||
7 | 241 | /* | 241 | /* |
8 | 242 | * Clear the subscribe someone else picker. | ||
9 | 243 | * | ||
10 | 244 | * @method clear_picker | ||
11 | 245 | * @param e {Object} The event object. | ||
12 | 246 | */ | ||
13 | 247 | function clear_picker(e) { | ||
14 | 248 | var input = Y.one('.yui-picker-search-box input'); | ||
15 | 249 | input.set('value', ''); | ||
16 | 250 | this.set('error', ''); | ||
17 | 251 | this.set('results', [{}]); | ||
18 | 252 | this._results_box.set('innerHTML', ''); | ||
19 | 253 | this.set('batches', []); | ||
20 | 254 | } | ||
21 | 255 | |||
22 | 256 | /* | ||
23 | 257 | * Initialize click handler for the subscribe someone else link. | 242 | * Initialize click handler for the subscribe someone else link. |
24 | 258 | * | 243 | * |
25 | 259 | * @method setup_subscribe_someone_else_handler | 244 | * @method setup_subscribe_someone_else_handler |
26 | @@ -262,23 +247,14 @@ | |||
27 | 262 | function setup_subscribe_someone_else_handler(subscription) { | 247 | function setup_subscribe_someone_else_handler(subscription) { |
28 | 263 | var config = { | 248 | var config = { |
29 | 264 | header: 'Subscribe someone else', | 249 | header: 'Subscribe someone else', |
31 | 265 | step_title: 'Search' | 250 | step_title: 'Search', |
32 | 251 | picker_activator: '.menu-link-addsubscriber' | ||
33 | 266 | }; | 252 | }; |
34 | 267 | 253 | ||
35 | 268 | var picker = Y.lp.picker.create( | 254 | var picker = Y.lp.picker.create( |
36 | 269 | 'ValidPersonOrTeam', | 255 | 'ValidPersonOrTeam', |
37 | 270 | function(result) { subscribe_someone_else(result, subscription); }, | 256 | function(result) { subscribe_someone_else(result, subscription); }, |
38 | 271 | config); | 257 | config); |
39 | 272 | // Clear results and search terms on cancel or save. | ||
40 | 273 | picker.on('save', clear_picker, picker); | ||
41 | 274 | picker.on('cancel', clear_picker, picker); | ||
42 | 275 | |||
43 | 276 | var subscription_link_someone_else = Y.one('.menu-link-addsubscriber'); | ||
44 | 277 | subscription_link_someone_else.on('click', function(e) { | ||
45 | 278 | e.halt(); | ||
46 | 279 | picker.show(); | ||
47 | 280 | }); | ||
48 | 281 | subscription_link_someone_else.addClass('js-action'); | ||
49 | 282 | } | 258 | } |
50 | 283 | 259 | ||
51 | 284 | /* | 260 | /* |
52 | @@ -626,21 +602,12 @@ | |||
53 | 626 | if (Y.Lang.isValue(link_branch_link)) { | 602 | if (Y.Lang.isValue(link_branch_link)) { |
54 | 627 | var config = { | 603 | var config = { |
55 | 628 | header: 'Link a related branch', | 604 | header: 'Link a related branch', |
57 | 629 | step_title: 'Search' | 605 | step_title: 'Search', |
58 | 606 | picker_activator: '.menu-link-addbranch' | ||
59 | 630 | }; | 607 | }; |
60 | 631 | 608 | ||
61 | 632 | var picker = Y.lp.picker.create( | 609 | var picker = Y.lp.picker.create( |
62 | 633 | 'Branch', get_branch_and_link_to_bug, config); | 610 | 'Branch', get_branch_and_link_to_bug, config); |
63 | 634 | |||
64 | 635 | // Clear results and search terms on cancel or save. | ||
65 | 636 | picker.on('save', clear_picker, picker); | ||
66 | 637 | picker.on('cancel', clear_picker, picker); | ||
67 | 638 | |||
68 | 639 | link_branch_link.on('click', function(e) { | ||
69 | 640 | e.halt(); | ||
70 | 641 | picker.show(); | ||
71 | 642 | }); | ||
72 | 643 | link_branch_link.addClass('js-action'); | ||
73 | 644 | } | 611 | } |
74 | 645 | } | 612 | } |
75 | 646 | 613 | ||
76 | 647 | 614 | ||
77 | === modified file 'lib/canonical/launchpad/javascript/code/codereview.js' | |||
78 | --- lib/canonical/launchpad/javascript/code/codereview.js 2009-11-30 23:51:34 +0000 | |||
79 | +++ lib/canonical/launchpad/javascript/code/codereview.js 2009-12-16 02:39:23 +0000 | |||
80 | @@ -22,6 +22,14 @@ | |||
81 | 22 | var link = Y.one('#request-review'); | 22 | var link = Y.one('#request-review'); |
82 | 23 | if (link !== null) { | 23 | if (link !== null) { |
83 | 24 | link.addClass('js-action'); | 24 | link.addClass('js-action'); |
84 | 25 | /* XXX: salgado, 2009-11-11: This will cause the picker to be | ||
85 | 26 | * recreated every time the user clicks on the link. Although that | ||
86 | 27 | * makes it unnecessary to have the widget cleared, it makes it | ||
87 | 28 | * impossible to persist the state of the picker between clicks on the | ||
88 | 29 | * link. We should probably have a policy to enforce that we | ||
89 | 30 | * just hide/show widgets when a link is clicked more than once, | ||
90 | 31 | * instead of recreating the widgets every time. | ||
91 | 32 | */ | ||
92 | 25 | link.on('click', show_request_review_form); | 33 | link.on('click', show_request_review_form); |
93 | 26 | } | 34 | } |
94 | 27 | link = Y.one('.menu-link-set_commit_message'); | 35 | link = Y.one('.menu-link-set_commit_message'); |
95 | @@ -51,7 +59,7 @@ | |||
96 | 51 | */ | 59 | */ |
97 | 52 | function commit_message_listener(message, saved) | 60 | function commit_message_listener(message, saved) |
98 | 53 | { | 61 | { |
100 | 54 | if (message == '') { | 62 | if (message === '') { |
101 | 55 | // Hide the multiline editor | 63 | // Hide the multiline editor |
102 | 56 | Y.one('#edit-commit-message').addClass('unseen'); | 64 | Y.one('#edit-commit-message').addClass('unseen'); |
103 | 57 | // Show the link again | 65 | // Show the link again |
104 | 58 | 66 | ||
105 | === modified file 'lib/canonical/launchpad/javascript/lp/picker.js' | |||
106 | --- lib/canonical/launchpad/javascript/lp/picker.js 2009-12-01 15:11:51 +0000 | |||
107 | +++ lib/canonical/launchpad/javascript/lp/picker.js 2009-12-16 02:39:23 +0000 | |||
108 | @@ -15,6 +15,8 @@ | |||
109 | 15 | * @param {String} resource_uri The object being modified. | 15 | * @param {String} resource_uri The object being modified. |
110 | 16 | * @param {String} attribute_name The attribute on the resource being | 16 | * @param {String} attribute_name The attribute on the resource being |
111 | 17 | * modified. | 17 | * modified. |
112 | 18 | * @param {Bool} show_remove_button Should the remove button be shown? | ||
113 | 19 | * @param {Bool} show_assign_me_button Should the 'assign me' button be shown? | ||
114 | 18 | * @param {String} content_box_id | 20 | * @param {String} content_box_id |
115 | 19 | * @param {Object} config Object literal of config name/value pairs. | 21 | * @param {Object} config Object literal of config name/value pairs. |
116 | 20 | * config.header is a line of text at the top of | 22 | * config.header is a line of text at the top of |
117 | @@ -219,10 +221,10 @@ | |||
118 | 219 | "string: " + vocabulary); | 221 | "string: " + vocabulary); |
119 | 220 | } | 222 | } |
120 | 221 | 223 | ||
122 | 222 | var picker = new Y.Picker({ | 224 | var new_config = Y.merge(config, { |
123 | 223 | align: { | 225 | align: { |
124 | 224 | points: [Y.WidgetPositionExt.CC, | 226 | points: [Y.WidgetPositionExt.CC, |
126 | 225 | Y.WidgetPositionExt.CC] | 227 | Y.WidgetPositionExt.CC] |
127 | 226 | }, | 228 | }, |
128 | 227 | progressbar: true, | 229 | progressbar: true, |
129 | 228 | progress: 100, | 230 | progress: 100, |
130 | @@ -231,6 +233,7 @@ | |||
131 | 231 | zIndex: 1000, | 233 | zIndex: 1000, |
132 | 232 | visible: false | 234 | visible: false |
133 | 233 | }); | 235 | }); |
134 | 236 | var picker = new Y.Picker(new_config); | ||
135 | 234 | 237 | ||
136 | 235 | picker.subscribe('save', function (e) { | 238 | picker.subscribe('save', function (e) { |
137 | 236 | Y.log('Got save event.'); | 239 | Y.log('Got save event.'); |
138 | 237 | 240 | ||
139 | === added file 'lib/canonical/launchpad/javascript/registry/team.js' | |||
140 | --- lib/canonical/launchpad/javascript/registry/team.js 1970-01-01 00:00:00 +0000 | |||
141 | +++ lib/canonical/launchpad/javascript/registry/team.js 2009-12-16 02:39:23 +0000 | |||
142 | @@ -0,0 +1,102 @@ | |||
143 | 1 | /** Copyright (c) 2009, Canonical Ltd. All rights reserved. | ||
144 | 2 | * | ||
145 | 3 | * Objects for subscription handling. | ||
146 | 4 | * | ||
147 | 5 | * @module lp.subscriber | ||
148 | 6 | */ | ||
149 | 7 | |||
150 | 8 | YUI.add('registry.team', function(Y) { | ||
151 | 9 | |||
152 | 10 | var module = Y.namespace('registry.team'); | ||
153 | 11 | |||
154 | 12 | /* | ||
155 | 13 | * Initialize click handler for the add member link | ||
156 | 14 | * | ||
157 | 15 | * @method setup_add_member_handler | ||
158 | 16 | */ | ||
159 | 17 | module.setup_add_member_handler = function() { | ||
160 | 18 | var config = { | ||
161 | 19 | header: 'Add a member', | ||
162 | 20 | step_title: 'Search', | ||
163 | 21 | picker_activator: '.menu-link-add_member' | ||
164 | 22 | }; | ||
165 | 23 | |||
166 | 24 | var picker = Y.lp.picker.create( | ||
167 | 25 | 'ValidTeamMember', | ||
168 | 26 | function(result) { _add_member(result); }, | ||
169 | 27 | config); | ||
170 | 28 | }; | ||
171 | 29 | |||
172 | 30 | var _add_member = function(result) { | ||
173 | 31 | var spinner = Y.one('#add-member-spinner'); | ||
174 | 32 | var addmember_link = Y.one('.menu-link-add_member'); | ||
175 | 33 | addmember_link.setStyle('display', 'none'); | ||
176 | 34 | spinner.setStyle('display', 'inline'); | ||
177 | 35 | function disable_spinner() { | ||
178 | 36 | addmember_link.setStyle('display', 'inline'); | ||
179 | 37 | spinner.setStyle('display', 'none'); | ||
180 | 38 | } | ||
181 | 39 | lp_client = new LP.client.Launchpad(); | ||
182 | 40 | |||
183 | 41 | var error_handler = new LP.client.ErrorHandler(); | ||
184 | 42 | error_handler.clearProgressUI = disable_spinner; | ||
185 | 43 | error_handler.showError = function(error_msg) { | ||
186 | 44 | Y.lp.display_error(Y.one('.menu-link-add_member'), error_msg); | ||
187 | 45 | }; | ||
188 | 46 | |||
189 | 47 | config = { | ||
190 | 48 | on: { | ||
191 | 49 | success: function(member_added) { | ||
192 | 50 | if (!member_added) { | ||
193 | 51 | disable_spinner(); | ||
194 | 52 | alert('Already a member.'); | ||
195 | 53 | return; | ||
196 | 54 | } | ||
197 | 55 | if (result.css.match("team")) { | ||
198 | 56 | disable_spinner(); | ||
199 | 57 | alert('This is a team'); | ||
200 | 58 | return; | ||
201 | 59 | } | ||
202 | 60 | var members_section = Y.one('#recently-approved'); | ||
203 | 61 | var members_ul = Y.one('#recently-approved-ul'); | ||
204 | 62 | var first_node = members_ul.get('firstChild'); | ||
205 | 63 | config = { | ||
206 | 64 | on: { | ||
207 | 65 | success: function(person_html) { | ||
208 | 66 | var total_members = Y.one( | ||
209 | 67 | '#member-count').get('innerHTML'); | ||
210 | 68 | total_members = parseInt(total_members, 10) + 1; | ||
211 | 69 | Y.one('#member-count').set( | ||
212 | 70 | 'innerHTML', total_members); | ||
213 | 71 | person_repr = Y.Node.create( | ||
214 | 72 | '<li>' + person_html + '</li>'); | ||
215 | 73 | members_section.setStyle('visibility', 'visible'); | ||
216 | 74 | members_ul.insertBefore( | ||
217 | 75 | person_repr, first_node); | ||
218 | 76 | anim = Y.lazr.anim.green_flash( | ||
219 | 77 | {node: person_repr}); | ||
220 | 78 | anim.run(); | ||
221 | 79 | disable_spinner(); | ||
222 | 80 | }, | ||
223 | 81 | failure: error_handler.getFailureHandler() | ||
224 | 82 | }, | ||
225 | 83 | accept: LP.client.XHTML | ||
226 | 84 | }; | ||
227 | 85 | lp_client.get(result.api_uri, config); | ||
228 | 86 | }, | ||
229 | 87 | failure: error_handler.getFailureHandler() | ||
230 | 88 | }, | ||
231 | 89 | parameters: { | ||
232 | 90 | // XXX: Why do I always have to get absolute URIs out of the URIs | ||
233 | 91 | // in the picker's result/client.links? | ||
234 | 92 | reviewer: LP.client.get_absolute_uri(LP.client.links.me), | ||
235 | 93 | person: LP.client.get_absolute_uri(result.api_uri) | ||
236 | 94 | } | ||
237 | 95 | }; | ||
238 | 96 | |||
239 | 97 | lp_client.named_post( | ||
240 | 98 | LP.client.cache.context.self_link, 'addMember', config); | ||
241 | 99 | }; | ||
242 | 100 | |||
243 | 101 | }, '0.1', {requires: [ | ||
244 | 102 | 'node', 'lazr.anim', 'lp.picker', 'lp.errors', 'lp.client.plugins']}); | ||
245 | 0 | 103 | ||
246 | === modified file 'lib/lp/app/templates/base-layout-macros.pt' | |||
247 | --- lib/lp/app/templates/base-layout-macros.pt 2009-12-08 16:43:44 +0000 | |||
248 | +++ lib/lp/app/templates/base-layout-macros.pt 2009-12-16 02:39:23 +0000 | |||
249 | @@ -181,6 +181,8 @@ | |||
250 | 181 | tal:attributes="src string:${lp_js}/lp/comment.js"></script> | 181 | tal:attributes="src string:${lp_js}/lp/comment.js"></script> |
251 | 182 | <script type="text/javascript" | 182 | <script type="text/javascript" |
252 | 183 | tal:attributes="src string:${lp_js}/lp/errors.js"></script> | 183 | tal:attributes="src string:${lp_js}/lp/errors.js"></script> |
253 | 184 | <script type="text/javascript" | ||
254 | 185 | tal:attributes="src string:${lp_js}/registry/team.js"></script> | ||
255 | 184 | 186 | ||
256 | 185 | </tal:devmode> | 187 | </tal:devmode> |
257 | 186 | <tal:production condition="not:devmode"> | 188 | <tal:production condition="not:devmode"> |
258 | 187 | 189 | ||
259 | === modified file 'lib/lp/bugs/tests/test_bugs_webservice.py' | |||
260 | --- lib/lp/bugs/tests/test_bugs_webservice.py 2009-12-01 12:21:56 +0000 | |||
261 | +++ lib/lp/bugs/tests/test_bugs_webservice.py 2009-12-16 02:39:22 +0000 | |||
262 | @@ -120,16 +120,6 @@ | |||
263 | 120 | self.assertEqual(response.status, 200) | 120 | self.assertEqual(response.status, 200) |
264 | 121 | 121 | ||
265 | 122 | rendered_comment = response.body | 122 | rendered_comment = response.body |
266 | 123 | # XXX Bjorn Tillenius 2009-05-15 bug=377003 | ||
267 | 124 | # The current request is a web service request when rendering | ||
268 | 125 | # the HTML, causing canonical_url to produce links pointing to the | ||
269 | 126 | # web service. Adjust the test to compensate for this, and accept | ||
270 | 127 | # that the links will be incorrect for now. We should fix this | ||
271 | 128 | # before using it for anything useful. | ||
272 | 129 | rendered_comment = rendered_comment.replace( | ||
273 | 130 | 'http://api.launchpad.dev/beta/', | ||
274 | 131 | 'http://launchpad.dev/') | ||
275 | 132 | |||
276 | 133 | self.assertRenderedCommentsEqual( | 123 | self.assertRenderedCommentsEqual( |
277 | 134 | rendered_comment, self.expected_comment_html) | 124 | rendered_comment, self.expected_comment_html) |
278 | 135 | 125 | ||
279 | 136 | 126 | ||
280 | === modified file 'lib/lp/registry/browser/configure.zcml' | |||
281 | --- lib/lp/registry/browser/configure.zcml 2009-12-05 18:37:28 +0000 | |||
282 | +++ lib/lp/registry/browser/configure.zcml 2009-12-16 02:39:22 +0000 | |||
283 | @@ -751,6 +751,9 @@ | |||
284 | 751 | for="lp.registry.interfaces.person.IPerson" | 751 | for="lp.registry.interfaces.person.IPerson" |
285 | 752 | layer="canonical.launchpad.layers.BugsLayer" | 752 | layer="canonical.launchpad.layers.BugsLayer" |
286 | 753 | name="+bugs"/> | 753 | name="+bugs"/> |
287 | 754 | <adapter | ||
288 | 755 | factory="lp.registry.browser.person.PersonXHTMLRepresentation" | ||
289 | 756 | name="lazr.restful.EntryResource" /> | ||
290 | 754 | <browser:page | 757 | <browser:page |
291 | 755 | name="+review" | 758 | name="+review" |
292 | 756 | for="lp.registry.interfaces.person.IPerson" | 759 | for="lp.registry.interfaces.person.IPerson" |
293 | 757 | 760 | ||
294 | === modified file 'lib/lp/registry/browser/person.py' | |||
295 | --- lib/lp/registry/browser/person.py 2009-12-08 10:20:37 +0000 | |||
296 | +++ lib/lp/registry/browser/person.py 2009-12-16 02:39:23 +0000 | |||
297 | @@ -102,7 +102,7 @@ | |||
298 | 102 | from zope.interface import classImplements, implements, Interface | 102 | from zope.interface import classImplements, implements, Interface |
299 | 103 | from zope.interface.exceptions import Invalid | 103 | from zope.interface.exceptions import Invalid |
300 | 104 | from zope.interface.interface import invariant | 104 | from zope.interface.interface import invariant |
302 | 105 | from zope.component import getUtility | 105 | from zope.component import adapts, getUtility |
303 | 106 | from zope.publisher.interfaces import NotFound | 106 | from zope.publisher.interfaces import NotFound |
304 | 107 | from zope.publisher.interfaces.browser import IBrowserPublisher | 107 | from zope.publisher.interfaces.browser import IBrowserPublisher |
305 | 108 | from zope.schema import Bool, Choice, List, Text, TextLine | 108 | from zope.schema import Bool, Choice, List, Text, TextLine |
306 | @@ -117,6 +117,7 @@ | |||
307 | 117 | from lazr.delegates import delegates | 117 | from lazr.delegates import delegates |
308 | 118 | from lazr.config import as_timedelta | 118 | from lazr.config import as_timedelta |
309 | 119 | from lazr.restful.interface import copy_field, use_template | 119 | from lazr.restful.interface import copy_field, use_template |
310 | 120 | from lazr.restful.interfaces import IWebServiceClientRequest | ||
311 | 120 | from canonical.lazr.utils import safe_hasattr | 121 | from canonical.lazr.utils import safe_hasattr |
312 | 121 | from canonical.database.sqlbase import flush_database_updates | 122 | from canonical.database.sqlbase import flush_database_updates |
313 | 122 | 123 | ||
314 | @@ -226,7 +227,8 @@ | |||
315 | 226 | logoutPerson, allowUnauthenticatedSession) | 227 | logoutPerson, allowUnauthenticatedSession) |
316 | 227 | from canonical.launchpad.webapp.menu import get_current_view | 228 | from canonical.launchpad.webapp.menu import get_current_view |
317 | 228 | from canonical.launchpad.webapp.publisher import LaunchpadView | 229 | from canonical.launchpad.webapp.publisher import LaunchpadView |
319 | 229 | from canonical.launchpad.webapp.tales import DateTimeFormatterAPI | 230 | from canonical.launchpad.webapp.tales import ( |
320 | 231 | DateTimeFormatterAPI, PersonFormatterAPI) | ||
321 | 230 | from lazr.uri import URI, InvalidURIError | 232 | from lazr.uri import URI, InvalidURIError |
322 | 231 | 233 | ||
323 | 232 | from canonical.launchpad import _ | 234 | from canonical.launchpad import _ |
324 | @@ -2460,7 +2462,7 @@ | |||
325 | 2460 | 2462 | ||
326 | 2461 | @property | 2463 | @property |
327 | 2462 | def next_url(self): | 2464 | def next_url(self): |
329 | 2463 | """Redirect back to the +languages page if request originated there.""" | 2465 | """Redirect back to the originating page.""" |
330 | 2464 | redirection_url = self.request.get('redirection_url') | 2466 | redirection_url = self.request.get('redirection_url') |
331 | 2465 | if redirection_url: | 2467 | if redirection_url: |
332 | 2466 | return redirection_url | 2468 | return redirection_url |
333 | @@ -2468,7 +2470,7 @@ | |||
334 | 2468 | 2470 | ||
335 | 2469 | @property | 2471 | @property |
336 | 2470 | def cancel_url(self): | 2472 | def cancel_url(self): |
338 | 2471 | """Redirect back to the +languages page if request originated there.""" | 2473 | """Redirect back to the originating page.""" |
339 | 2472 | redirection_url = self.getRedirectionURL() | 2474 | redirection_url = self.getRedirectionURL() |
340 | 2473 | if redirection_url: | 2475 | if redirection_url: |
341 | 2474 | return redirection_url | 2476 | return redirection_url |
342 | @@ -2676,12 +2678,29 @@ | |||
343 | 2676 | orderBy='-TeamMembership.date_proposed') | 2678 | orderBy='-TeamMembership.date_proposed') |
344 | 2677 | return members[:5] | 2679 | return members[:5] |
345 | 2678 | 2680 | ||
352 | 2679 | @cachedproperty | 2681 | @property |
353 | 2680 | def has_recent_approved_or_proposed_members(self): | 2682 | def recently_approved_hidden(self): |
354 | 2681 | """Does the team have recently approved or proposed members?""" | 2683 | """Optionally hide the div. |
355 | 2682 | approved = self.recently_approved_members.count() > 0 | 2684 | |
356 | 2683 | proposed = self.recently_proposed_members.count() > 0 | 2685 | The AJAX on the page needs the elements to be present |
357 | 2684 | return approved or proposed | 2686 | but hidden in case it adds a member to the list. |
358 | 2687 | """ | ||
359 | 2688 | if self.recently_approved_members.count() == 0: | ||
360 | 2689 | return 'visibility: collapse' | ||
361 | 2690 | else: | ||
362 | 2691 | return '' | ||
363 | 2692 | |||
364 | 2693 | @property | ||
365 | 2694 | def recently_proposed_hidden(self): | ||
366 | 2695 | """Optionally hide the div. | ||
367 | 2696 | |||
368 | 2697 | The AJAX on the page needs the elements to be present | ||
369 | 2698 | but hidden in case it adds a member to the list. | ||
370 | 2699 | """ | ||
371 | 2700 | if self.recently_proposed_members.count() == 0: | ||
372 | 2701 | return 'visibility: collapse' | ||
373 | 2702 | else: | ||
374 | 2703 | return '' | ||
375 | 2685 | 2704 | ||
376 | 2686 | @cachedproperty | 2705 | @cachedproperty |
377 | 2687 | def openpolls(self): | 2706 | def openpolls(self): |
378 | @@ -5826,8 +5845,7 @@ | |||
379 | 5826 | usedfor = ITeamIndexMenu | 5845 | usedfor = ITeamIndexMenu |
380 | 5827 | facet = 'overview' | 5846 | facet = 'overview' |
381 | 5828 | title = 'Change team' | 5847 | title = 'Change team' |
384 | 5829 | links = ('edit', 'join', 'add_member', 'add_my_teams', | 5848 | links = ('edit', 'join', 'add_my_teams', 'leave') |
383 | 5830 | 'leave') | ||
385 | 5831 | 5849 | ||
386 | 5832 | 5850 | ||
387 | 5833 | class TeamEditMenu(TeamNavigationMenuBase): | 5851 | class TeamEditMenu(TeamNavigationMenuBase): |
388 | @@ -5843,3 +5861,16 @@ | |||
389 | 5843 | classImplements(TeamIndexView, ITeamIndexMenu) | 5861 | classImplements(TeamIndexView, ITeamIndexMenu) |
390 | 5844 | classImplements(TeamEditView, ITeamEditMenu) | 5862 | classImplements(TeamEditView, ITeamEditMenu) |
391 | 5845 | classImplements(PersonIndexView, IPersonIndexMenu) | 5863 | classImplements(PersonIndexView, IPersonIndexMenu) |
392 | 5864 | |||
393 | 5865 | |||
394 | 5866 | class PersonXHTMLRepresentation: | ||
395 | 5867 | adapts(IPerson, IWebServiceClientRequest) | ||
396 | 5868 | implements(Interface) | ||
397 | 5869 | |||
398 | 5870 | def __init__(self, person, request): | ||
399 | 5871 | self.person = person | ||
400 | 5872 | self.request = request | ||
401 | 5873 | |||
402 | 5874 | def __call__(self): | ||
403 | 5875 | """Render `Person` as XHTML using the webservice.""" | ||
404 | 5876 | return PersonFormatterAPI(self.person).link(None) | ||
405 | 5846 | 5877 | ||
406 | === modified file 'lib/lp/registry/browser/tests/teammembership-views.txt' | |||
407 | --- lib/lp/registry/browser/tests/teammembership-views.txt 2009-11-13 13:06:50 +0000 | |||
408 | +++ lib/lp/registry/browser/tests/teammembership-views.txt 2009-12-16 02:39:23 +0000 | |||
409 | @@ -63,6 +63,7 @@ | |||
410 | 63 | 63 | ||
411 | 64 | >>> login_person(team_owner) | 64 | >>> login_person(team_owner) |
412 | 65 | >>> super_team.addMember(team, team_owner) | 65 | >>> super_team.addMember(team, team_owner) |
413 | 66 | True | ||
414 | 66 | >>> membership = membership_set.getByPersonAndTeam(team, super_team) | 67 | >>> membership = membership_set.getByPersonAndTeam(team, super_team) |
415 | 67 | >>> login_person(team.teamowner) | 68 | >>> login_person(team.teamowner) |
416 | 68 | >>> view = TeamInvitationView(membership, request) | 69 | >>> view = TeamInvitationView(membership, request) |
417 | 69 | 70 | ||
418 | === added file 'lib/lp/registry/browser/tests/test_person_webservice.py' | |||
419 | --- lib/lp/registry/browser/tests/test_person_webservice.py 1970-01-01 00:00:00 +0000 | |||
420 | +++ lib/lp/registry/browser/tests/test_person_webservice.py 2009-12-16 02:39:22 +0000 | |||
421 | @@ -0,0 +1,38 @@ | |||
422 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | ||
423 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
424 | 3 | |||
425 | 4 | __metaclass__ = type | ||
426 | 5 | |||
427 | 6 | import unittest | ||
428 | 7 | |||
429 | 8 | from canonical.launchpad.ftests import login | ||
430 | 9 | from lp.testing import TestCaseWithFactory | ||
431 | 10 | from canonical.launchpad.testing.pages import LaunchpadWebServiceCaller | ||
432 | 11 | from canonical.testing import DatabaseFunctionalLayer | ||
433 | 12 | |||
434 | 13 | |||
435 | 14 | class TestPersonRepresentation(TestCaseWithFactory): | ||
436 | 15 | layer = DatabaseFunctionalLayer | ||
437 | 16 | |||
438 | 17 | def setUp(self): | ||
439 | 18 | TestCaseWithFactory.setUp(self) | ||
440 | 19 | login('guilherme.salgado@canonical.com ') | ||
441 | 20 | self.person = self.factory.makePerson( | ||
442 | 21 | name='test-person', displayname='Test Person') | ||
443 | 22 | self.webservice = LaunchpadWebServiceCaller( | ||
444 | 23 | 'launchpad-library', 'salgado-change-anything') | ||
445 | 24 | |||
446 | 25 | def test_GET_xhtml_representation(self): | ||
447 | 26 | response = self.webservice.get( | ||
448 | 27 | '/~%s' % self.person.name, 'application/xhtml+xml') | ||
449 | 28 | |||
450 | 29 | self.assertEqual(response.status, 200) | ||
451 | 30 | |||
452 | 31 | rendered_comment = response.body | ||
453 | 32 | self.assertEquals( | ||
454 | 33 | rendered_comment, | ||
455 | 34 | '<a href="/~test-person" class="sprite person">Test Person</a>') | ||
456 | 35 | |||
457 | 36 | |||
458 | 37 | def test_suite(): | ||
459 | 38 | return unittest.TestLoader().loadTestsFromName(__name__) | ||
460 | 0 | 39 | ||
461 | === modified file 'lib/lp/registry/doc/teammembership-email-notification.txt' | |||
462 | --- lib/lp/registry/doc/teammembership-email-notification.txt 2009-08-24 03:01:12 +0000 | |||
463 | +++ lib/lp/registry/doc/teammembership-email-notification.txt 2009-12-16 02:39:23 +0000 | |||
464 | @@ -226,6 +226,7 @@ | |||
465 | 226 | >>> cprov = personset.getByName('cprov') | 226 | >>> cprov = personset.getByName('cprov') |
466 | 227 | >>> marilize = personset.getByName('marilize') | 227 | >>> marilize = personset.getByName('marilize') |
467 | 228 | >>> ubuntu_team.addMember(marilize, reviewer=cprov) | 228 | >>> ubuntu_team.addMember(marilize, reviewer=cprov) |
468 | 229 | True | ||
469 | 229 | >>> transaction.commit() | 230 | >>> transaction.commit() |
470 | 230 | >>> len(stub.test_emails) | 231 | >>> len(stub.test_emails) |
471 | 231 | 6 | 232 | 6 |
472 | @@ -275,6 +276,7 @@ | |||
473 | 275 | >>> mirror_admins.getTeamAdminsEmailAddresses() | 276 | >>> mirror_admins.getTeamAdminsEmailAddresses() |
474 | 276 | ['mark@example.com'] | 277 | ['mark@example.com'] |
475 | 277 | >>> ubuntu_team.addMember(mirror_admins, reviewer=cprov) | 278 | >>> ubuntu_team.addMember(mirror_admins, reviewer=cprov) |
476 | 279 | True | ||
477 | 278 | >>> transaction.commit() | 280 | >>> transaction.commit() |
478 | 279 | >>> len(stub.test_emails) | 281 | >>> len(stub.test_emails) |
479 | 280 | 1 | 282 | 1 |
480 | @@ -326,6 +328,7 @@ | |||
481 | 326 | 328 | ||
482 | 327 | >>> landscape = personset.getByName('landscape-developers') | 329 | >>> landscape = personset.getByName('landscape-developers') |
483 | 328 | >>> ubuntu_team.addMember(landscape, reviewer=cprov) | 330 | >>> ubuntu_team.addMember(landscape, reviewer=cprov) |
484 | 331 | True | ||
485 | 329 | 332 | ||
486 | 330 | # Reset stub.test_emails as we don't care about the notification triggered | 333 | # Reset stub.test_emails as we don't care about the notification triggered |
487 | 331 | # by the addMember() call. | 334 | # by the addMember() call. |
488 | @@ -359,6 +362,7 @@ | |||
489 | 359 | 362 | ||
490 | 360 | >>> launchpad = personset.getByName('launchpad') | 363 | >>> launchpad = personset.getByName('launchpad') |
491 | 361 | >>> ubuntu_team.addMember(launchpad, reviewer=cprov, force_team_add=True) | 364 | >>> ubuntu_team.addMember(launchpad, reviewer=cprov, force_team_add=True) |
492 | 365 | True | ||
493 | 362 | >>> flush_database_updates() | 366 | >>> flush_database_updates() |
494 | 363 | >>> transaction.commit() | 367 | >>> transaction.commit() |
495 | 364 | >>> len(stub.test_emails) | 368 | >>> len(stub.test_emails) |
496 | @@ -812,6 +816,7 @@ | |||
497 | 812 | >>> member = factory.makePerson( | 816 | >>> member = factory.makePerson( |
498 | 813 | ... name='team-member', email='team-member@example.com') | 817 | ... name='team-member', email='team-member@example.com') |
499 | 814 | >>> team_one.addMember(member, owner) | 818 | >>> team_one.addMember(member, owner) |
500 | 819 | True | ||
501 | 815 | >>> print_distinct_emails() | 820 | >>> print_distinct_emails() |
502 | 816 | From: Team One ... | 821 | From: Team One ... |
503 | 817 | ---------------------------------------- | 822 | ---------------------------------------- |
504 | @@ -838,6 +843,7 @@ | |||
505 | 838 | >>> team_two = factory.makeTeam( | 843 | >>> team_two = factory.makeTeam( |
506 | 839 | ... name='team-two', email='team-two@example.com', owner=owner) | 844 | ... name='team-two', email='team-two@example.com', owner=owner) |
507 | 840 | >>> team_one.addMember(team_two, owner, force_team_add=True) | 845 | >>> team_one.addMember(team_two, owner, force_team_add=True) |
508 | 846 | True | ||
509 | 841 | >>> print_distinct_emails() | 847 | >>> print_distinct_emails() |
510 | 842 | From: Team One ... | 848 | From: Team One ... |
511 | 843 | ---------------------------------------- | 849 | ---------------------------------------- |
512 | 844 | 850 | ||
513 | === modified file 'lib/lp/registry/doc/teammembership.txt' | |||
514 | --- lib/lp/registry/doc/teammembership.txt 2009-11-30 20:18:42 +0000 | |||
515 | +++ lib/lp/registry/doc/teammembership.txt 2009-12-16 02:39:22 +0000 | |||
516 | @@ -132,7 +132,6 @@ | |||
517 | 132 | Other users must use the join method if they are going to add themselves | 132 | Other users must use the join method if they are going to add themselves |
518 | 133 | to a team. | 133 | to a team. |
519 | 134 | 134 | ||
520 | 135 | >>> from zope.security.interfaces import Unauthorized | ||
521 | 136 | >>> mark = personset.getByName('mark') | 135 | >>> mark = personset.getByName('mark') |
522 | 137 | >>> t3.addMember(salgado, reviewer=mark, status=TeamMembershipStatus.ADMIN) | 136 | >>> t3.addMember(salgado, reviewer=mark, status=TeamMembershipStatus.ADMIN) |
523 | 138 | Traceback (most recent call last): | 137 | Traceback (most recent call last): |
524 | @@ -142,8 +141,12 @@ | |||
525 | 142 | # Log in as the team owner. | 141 | # Log in as the team owner. |
526 | 143 | >>> login_person(t3.teamowner) | 142 | >>> login_person(t3.teamowner) |
527 | 144 | 143 | ||
528 | 144 | addMember returns True if the member got added (i.e. he wasn't already a | ||
529 | 145 | member of the team). | ||
530 | 146 | |||
531 | 145 | >>> t3.addMember( | 147 | >>> t3.addMember( |
532 | 146 | ... salgado, reviewer=mark, status=TeamMembershipStatus.ADMIN) | 148 | ... salgado, reviewer=mark, status=TeamMembershipStatus.ADMIN) |
533 | 149 | True | ||
534 | 147 | >>> from canonical.launchpad.interfaces import ITeamMembershipSet | 150 | >>> from canonical.launchpad.interfaces import ITeamMembershipSet |
535 | 148 | >>> membershipset = getUtility(ITeamMembershipSet) | 151 | >>> membershipset = getUtility(ITeamMembershipSet) |
536 | 149 | >>> flush_database_updates() | 152 | >>> flush_database_updates() |
537 | @@ -155,13 +158,27 @@ | |||
538 | 155 | >>> salgado in t3.activemembers | 158 | >>> salgado in t3.activemembers |
539 | 156 | True | 159 | True |
540 | 157 | 160 | ||
541 | 161 | addMember returns True also when the member is added as a proposed | ||
542 | 162 | member. | ||
543 | 163 | |||
544 | 158 | >>> marilize = personset.getByName('marilize') | 164 | >>> marilize = personset.getByName('marilize') |
545 | 159 | >>> t3.addMember( | 165 | >>> t3.addMember( |
546 | 160 | ... marilize, reviewer=mark, status=TeamMembershipStatus.PROPOSED) | 166 | ... marilize, reviewer=mark, status=TeamMembershipStatus.PROPOSED) |
547 | 167 | True | ||
548 | 161 | >>> flush_database_updates() | 168 | >>> flush_database_updates() |
549 | 162 | >>> marilize in t3.activemembers | 169 | >>> marilize in t3.activemembers |
550 | 163 | False | 170 | False |
551 | 164 | 171 | ||
552 | 172 | If addMember is called with a person that is already a member, it | ||
553 | 173 | returns False. | ||
554 | 174 | |||
555 | 175 | >>> t3.addMember( | ||
556 | 176 | ... salgado, reviewer=mark, status=TeamMembershipStatus.ADMIN) | ||
557 | 177 | False | ||
558 | 178 | >>> t3.addMember( | ||
559 | 179 | ... marilize, reviewer=mark, status=TeamMembershipStatus.PROPOSED) | ||
560 | 180 | False | ||
561 | 181 | |||
562 | 165 | As expected, the membership object implements ITeamMembership. | 182 | As expected, the membership object implements ITeamMembership. |
563 | 166 | 183 | ||
564 | 167 | >>> from canonical.launchpad.webapp.testing import verifyObject | 184 | >>> from canonical.launchpad.webapp.testing import verifyObject |
565 | @@ -175,6 +192,7 @@ | |||
566 | 175 | invitation before the team is made a member. | 192 | invitation before the team is made a member. |
567 | 176 | 193 | ||
568 | 177 | >>> t1.addMember(t2, reviewer) | 194 | >>> t1.addMember(t2, reviewer) |
569 | 195 | True | ||
570 | 178 | >>> membership = membershipset.getByPersonAndTeam(t2, t1) | 196 | >>> membership = membershipset.getByPersonAndTeam(t2, t1) |
571 | 179 | >>> membership.status == TeamMembershipStatus.INVITED | 197 | >>> membership.status == TeamMembershipStatus.INVITED |
572 | 180 | True | 198 | True |
573 | @@ -192,6 +210,7 @@ | |||
574 | 192 | A team admin can also decline an invitation made to his team. | 210 | A team admin can also decline an invitation made to his team. |
575 | 193 | 211 | ||
576 | 194 | >>> t2.addMember(t3, reviewer=mark) | 212 | >>> t2.addMember(t3, reviewer=mark) |
577 | 213 | True | ||
578 | 195 | >>> login_person(t3.teamowner) | 214 | >>> login_person(t3.teamowner) |
579 | 196 | >>> t3.declineInvitationToBeMemberOf(t2, comment='something') | 215 | >>> t3.declineInvitationToBeMemberOf(t2, comment='something') |
580 | 197 | >>> membership = membershipset.getByPersonAndTeam(t3, t2) | 216 | >>> membership = membershipset.getByPersonAndTeam(t3, t2) |
581 | @@ -205,6 +224,7 @@ | |||
582 | 205 | 224 | ||
583 | 206 | >>> login_person(t3.teamowner) | 225 | >>> login_person(t3.teamowner) |
584 | 207 | >>> t2.addMember(t3, reviewer=mark, force_team_add=True) | 226 | >>> t2.addMember(t3, reviewer=mark, force_team_add=True) |
585 | 227 | True | ||
586 | 208 | >>> [m.displayname for m in t2.allmembers] | 228 | >>> [m.displayname for m in t2.allmembers] |
587 | 209 | [u'Foo Bar', u'Guilherme Salgado', u't3'] | 229 | [u'Foo Bar', u'Guilherme Salgado', u't3'] |
588 | 210 | 230 | ||
589 | @@ -226,6 +246,7 @@ | |||
590 | 226 | Adding t2 as a member of t5 will add all t2 members as t5 members too. | 246 | Adding t2 as a member of t5 will add all t2 members as t5 members too. |
591 | 227 | 247 | ||
592 | 228 | >>> t5.addMember(t2, reviewer, force_team_add=True) | 248 | >>> t5.addMember(t2, reviewer, force_team_add=True) |
593 | 249 | True | ||
594 | 229 | >>> [m.displayname for m in t5.allmembers] | 250 | >>> [m.displayname for m in t5.allmembers] |
595 | 230 | [u'Foo Bar', u'Guilherme Salgado', u't2', u't3'] | 251 | [u'Foo Bar', u'Guilherme Salgado', u't2', u't3'] |
596 | 231 | 252 | ||
597 | @@ -233,7 +254,9 @@ | |||
598 | 233 | members too. | 254 | members too. |
599 | 234 | 255 | ||
600 | 235 | >>> t4.addMember(t5, reviewer, force_team_add=True) | 256 | >>> t4.addMember(t5, reviewer, force_team_add=True) |
601 | 257 | True | ||
602 | 236 | >>> t4.addMember(t1, reviewer, force_team_add=True) | 258 | >>> t4.addMember(t1, reviewer, force_team_add=True) |
603 | 259 | True | ||
604 | 237 | >>> [m.displayname for m in t4.allmembers] | 260 | >>> [m.displayname for m in t4.allmembers] |
605 | 238 | [u'Foo Bar', u'Guilherme Salgado', u't1', u't2', u't3', u't5'] | 261 | [u'Foo Bar', u'Guilherme Salgado', u't1', u't2', u't3', u't5'] |
606 | 239 | 262 | ||
607 | @@ -327,6 +350,7 @@ | |||
608 | 327 | 350 | ||
609 | 328 | >>> cprov = getUtility(IPersonSet).getByName('cprov') | 351 | >>> cprov = getUtility(IPersonSet).getByName('cprov') |
610 | 329 | >>> t3.addMember(cprov, reviewer) | 352 | >>> t3.addMember(cprov, reviewer) |
611 | 353 | True | ||
612 | 330 | >>> [m.displayname for m in t3.allmembers] | 354 | >>> [m.displayname for m in t3.allmembers] |
613 | 331 | [u'Celso Providelo', u'Foo Bar'] | 355 | [u'Celso Providelo', u'Foo Bar'] |
614 | 332 | 356 | ||
615 | @@ -391,15 +415,22 @@ | |||
616 | 391 | None | 415 | None |
617 | 392 | 416 | ||
618 | 393 | When we approve his membership, the datejoined will contain the date that it | 417 | When we approve his membership, the datejoined will contain the date that it |
620 | 394 | was approved. | 418 | was approved. It returns True to indicate that the status was changed. |
621 | 395 | 419 | ||
622 | 396 | >>> membership.setStatus(TeamMembershipStatus.APPROVED, foobar) | 420 | >>> membership.setStatus(TeamMembershipStatus.APPROVED, foobar) |
623 | 421 | True | ||
624 | 397 | >>> print membership.status.title | 422 | >>> print membership.status.title |
625 | 398 | Approved | 423 | Approved |
626 | 399 | >>> utc_now = datetime.now(pytz.timezone('UTC')) | 424 | >>> utc_now = datetime.now(pytz.timezone('UTC')) |
627 | 400 | >>> membership.datejoined.date() == utc_now.date() | 425 | >>> membership.datejoined.date() == utc_now.date() |
628 | 401 | True | 426 | True |
629 | 402 | 427 | ||
630 | 428 | If setStatus is called again with the same status, it returns False, | ||
631 | 429 | to indicate that the status didn't change. | ||
632 | 430 | |||
633 | 431 | >>> membership.setStatus(TeamMembershipStatus.APPROVED, foobar) | ||
634 | 432 | False | ||
635 | 433 | |||
636 | 403 | Other status updates won't change datejoined, regardless of the status. | 434 | Other status updates won't change datejoined, regardless of the status. |
637 | 404 | That's because datejoined stores the date in which the membership was first | 435 | That's because datejoined stores the date in which the membership was first |
638 | 405 | made active. | 436 | made active. |
639 | @@ -415,6 +446,7 @@ | |||
640 | 415 | 446 | ||
641 | 416 | >>> foobar_on_buildd.setStatus( | 447 | >>> foobar_on_buildd.setStatus( |
642 | 417 | ... TeamMembershipStatus.DEACTIVATED, foobar) | 448 | ... TeamMembershipStatus.DEACTIVATED, foobar) |
643 | 449 | True | ||
644 | 418 | >>> print foobar_on_buildd.status.title | 450 | >>> print foobar_on_buildd.status.title |
645 | 419 | Deactivated | 451 | Deactivated |
646 | 420 | >>> foobar_on_buildd.datejoined <= utc_now | 452 | >>> foobar_on_buildd.datejoined <= utc_now |
647 | @@ -422,6 +454,7 @@ | |||
648 | 422 | 454 | ||
649 | 423 | >>> foobar_on_buildd.setStatus( | 455 | >>> foobar_on_buildd.setStatus( |
650 | 424 | ... TeamMembershipStatus.APPROVED, foobar) | 456 | ... TeamMembershipStatus.APPROVED, foobar) |
651 | 457 | True | ||
652 | 425 | >>> print foobar_on_buildd.status.title | 458 | >>> print foobar_on_buildd.status.title |
653 | 426 | Approved | 459 | Approved |
654 | 427 | >>> foobar_on_buildd.datejoined <= utc_now | 460 | >>> foobar_on_buildd.datejoined <= utc_now |
655 | @@ -790,6 +823,7 @@ | |||
656 | 790 | >>> admins = getUtility(IPersonSet).getByName('admins') | 823 | >>> admins = getUtility(IPersonSet).getByName('admins') |
657 | 791 | >>> login_person(t1.teamowner) | 824 | >>> login_person(t1.teamowner) |
658 | 792 | >>> t1.addMember(admins, reviewer=t1.teamowner, force_team_add=True) | 825 | >>> t1.addMember(admins, reviewer=t1.teamowner, force_team_add=True) |
659 | 826 | True | ||
660 | 793 | >>> flush_database_updates() | 827 | >>> flush_database_updates() |
661 | 794 | >>> print '\n'.join(sorted( | 828 | >>> print '\n'.join(sorted( |
662 | 795 | ... team.name for team in salgado.teams_participated_in)) | 829 | ... team.name for team in salgado.teams_participated_in)) |
663 | @@ -804,6 +838,7 @@ | |||
664 | 804 | for Salgado. | 838 | for Salgado. |
665 | 805 | 839 | ||
666 | 806 | >>> admins.addMember(t2, reviewer=admins.teamowner, force_team_add=True) | 840 | >>> admins.addMember(t2, reviewer=admins.teamowner, force_team_add=True) |
667 | 841 | True | ||
668 | 807 | >>> flush_database_updates() | 842 | >>> flush_database_updates() |
669 | 808 | >>> print '\n'.join(sorted( | 843 | >>> print '\n'.join(sorted( |
670 | 809 | ... team.name for team in salgado.teams_participated_in)) | 844 | ... team.name for team in salgado.teams_participated_in)) |
671 | @@ -845,6 +880,7 @@ | |||
672 | 845 | Or changed: | 880 | Or changed: |
673 | 846 | 881 | ||
674 | 847 | >>> membership.setStatus(TeamMembershipStatus.DEACTIVATED, mark) | 882 | >>> membership.setStatus(TeamMembershipStatus.DEACTIVATED, mark) |
675 | 883 | True | ||
676 | 848 | >>> no_priv._inTeam_cache | 884 | >>> no_priv._inTeam_cache |
677 | 849 | {} | 885 | {} |
678 | 850 | >>> no_priv.inTeam(admins) | 886 | >>> no_priv.inTeam(admins) |
679 | @@ -902,3 +938,4 @@ | |||
680 | 902 | >>> bad_membership.sendAutoRenewalNotification() | 938 | >>> bad_membership.sendAutoRenewalNotification() |
681 | 903 | 939 | ||
682 | 904 | >>> bad_membership.setStatus(TeamMembershipStatus.EXPIRED, bad_user) | 940 | >>> bad_membership.setStatus(TeamMembershipStatus.EXPIRED, bad_user) |
683 | 941 | True | ||
684 | 905 | 942 | ||
685 | === modified file 'lib/lp/registry/interfaces/teammembership.py' | |||
686 | --- lib/lp/registry/interfaces/teammembership.py 2009-06-25 04:06:00 +0000 | |||
687 | +++ lib/lp/registry/interfaces/teammembership.py 2009-12-16 02:39:23 +0000 | |||
688 | @@ -224,6 +224,8 @@ | |||
689 | 224 | transition. | 224 | transition. |
690 | 225 | 225 | ||
691 | 226 | The given status must be different than the current status. | 226 | The given status must be different than the current status. |
692 | 227 | |||
693 | 228 | Return True if the status got changed, otherwise False. | ||
694 | 227 | """ | 229 | """ |
695 | 228 | 230 | ||
696 | 229 | 231 | ||
697 | 230 | 232 | ||
698 | === modified file 'lib/lp/registry/model/person.py' | |||
699 | --- lib/lp/registry/model/person.py 2009-12-05 18:37:28 +0000 | |||
700 | +++ lib/lp/registry/model/person.py 2009-12-16 02:39:23 +0000 | |||
701 | @@ -1236,6 +1236,7 @@ | |||
702 | 1236 | status = TeamMembershipStatus.INVITED | 1236 | status = TeamMembershipStatus.INVITED |
703 | 1237 | event = TeamInvitationEvent | 1237 | event = TeamInvitationEvent |
704 | 1238 | 1238 | ||
705 | 1239 | member_added = True | ||
706 | 1239 | expires = self.defaultexpirationdate | 1240 | expires = self.defaultexpirationdate |
707 | 1240 | tm = TeamMembership.selectOneBy(person=person, team=self) | 1241 | tm = TeamMembership.selectOneBy(person=person, team=self) |
708 | 1241 | if tm is None: | 1242 | if tm is None: |
709 | @@ -1250,11 +1251,12 @@ | |||
710 | 1250 | # We can't use tm.setExpirationDate() here because the reviewer | 1251 | # We can't use tm.setExpirationDate() here because the reviewer |
711 | 1251 | # here will be the member themselves when they join an OPEN team. | 1252 | # here will be the member themselves when they join an OPEN team. |
712 | 1252 | tm.dateexpires = expires | 1253 | tm.dateexpires = expires |
714 | 1253 | tm.setStatus(status, reviewer, comment) | 1254 | member_added = tm.setStatus(status, reviewer, comment) |
715 | 1254 | 1255 | ||
716 | 1255 | if not person.is_team and may_subscribe_to_list: | 1256 | if not person.is_team and may_subscribe_to_list: |
717 | 1256 | person.autoSubscribeToMailingList(self.mailing_list, | 1257 | person.autoSubscribeToMailingList(self.mailing_list, |
718 | 1257 | requester=reviewer) | 1258 | requester=reviewer) |
719 | 1259 | return member_added | ||
720 | 1258 | 1260 | ||
721 | 1259 | # The three methods below are not in the IPerson interface because we want | 1261 | # The three methods below are not in the IPerson interface because we want |
722 | 1260 | # to protect them with a launchpad.Edit permission. We could do that by | 1262 | # to protect them with a launchpad.Edit permission. We could do that by |
723 | 1261 | 1263 | ||
724 | === modified file 'lib/lp/registry/model/teammembership.py' | |||
725 | --- lib/lp/registry/model/teammembership.py 2009-06-25 04:06:00 +0000 | |||
726 | +++ lib/lp/registry/model/teammembership.py 2009-12-16 02:39:23 +0000 | |||
727 | @@ -272,7 +272,7 @@ | |||
728 | 272 | def setStatus(self, status, user, comment=None): | 272 | def setStatus(self, status, user, comment=None): |
729 | 273 | """See `ITeamMembership`.""" | 273 | """See `ITeamMembership`.""" |
730 | 274 | if status == self.status: | 274 | if status == self.status: |
732 | 275 | return | 275 | return False |
733 | 276 | 276 | ||
734 | 277 | approved = TeamMembershipStatus.APPROVED | 277 | approved = TeamMembershipStatus.APPROVED |
735 | 278 | admin = TeamMembershipStatus.ADMIN | 278 | admin = TeamMembershipStatus.ADMIN |
736 | @@ -357,10 +357,9 @@ | |||
737 | 357 | # When a member proposes himself, a more detailed notification is | 357 | # When a member proposes himself, a more detailed notification is |
738 | 358 | # sent to the team admins by a subscriber of JoinTeamEvent; that's | 358 | # sent to the team admins by a subscriber of JoinTeamEvent; that's |
739 | 359 | # why we don't send anything here. | 359 | # why we don't send anything here. |
744 | 360 | if self.person == self.last_changed_by and self.status == proposed: | 360 | if self.person != self.last_changed_by or self.status != proposed: |
745 | 361 | return | 361 | self._sendStatusChangeNotification(old_status) |
746 | 362 | 362 | return True | |
743 | 363 | self._sendStatusChangeNotification(old_status) | ||
747 | 364 | 363 | ||
748 | 365 | def _sendStatusChangeNotification(self, old_status): | 364 | def _sendStatusChangeNotification(self, old_status): |
749 | 366 | """Send a status change notification to all team admins and the | 365 | """Send a status change notification to all team admins and the |
750 | 367 | 366 | ||
751 | === modified file 'lib/lp/registry/templates/team-index.pt' | |||
752 | --- lib/lp/registry/templates/team-index.pt 2009-10-26 21:12:49 +0000 | |||
753 | +++ lib/lp/registry/templates/team-index.pt 2009-12-16 02:39:22 +0000 | |||
754 | @@ -17,6 +17,16 @@ | |||
755 | 17 | rel="meta" type="application/rdf+xml" | 17 | rel="meta" type="application/rdf+xml" |
756 | 18 | title="FOAF" href="+rdf" | 18 | title="FOAF" href="+rdf" |
757 | 19 | /> | 19 | /> |
758 | 20 | <script type="text/javascript" | ||
759 | 21 | tal:content="string: | ||
760 | 22 | YUI().use('registry.team', function(Y) { | ||
761 | 23 | Y.on('load', | ||
762 | 24 | function(e) { | ||
763 | 25 | Y.registry.team.setup_add_member_handler(); | ||
764 | 26 | }, | ||
765 | 27 | window); | ||
766 | 28 | }); | ||
767 | 29 | "/> | ||
768 | 20 | </tal:block> | 30 | </tal:block> |
769 | 21 | </head> | 31 | </head> |
770 | 22 | 32 | ||
771 | 23 | 33 | ||
772 | === modified file 'lib/lp/registry/templates/team-portlet-membership.pt' | |||
773 | --- lib/lp/registry/templates/team-portlet-membership.pt 2009-10-23 21:11:12 +0000 | |||
774 | +++ lib/lp/registry/templates/team-portlet-membership.pt 2009-12-16 02:39:23 +0000 | |||
775 | @@ -18,7 +18,9 @@ | |||
776 | 18 | <div id="membership-summary"> | 18 | <div id="membership-summary"> |
777 | 19 | <div> | 19 | <div> |
778 | 20 | <img src="/@@/team" alt="team" /> | 20 | <img src="/@@/team" alt="team" /> |
780 | 21 | <strong><tal:active content="context/all_member_count" /></strong> | 21 | <strong id="member-count"> |
781 | 22 | <tal:active content="context/all_member_count" /> | ||
782 | 23 | </strong> | ||
783 | 22 | <a tal:attributes="href string:${context/fmt:url/+members}#active" | 24 | <a tal:attributes="href string:${context/fmt:url/+members}#active" |
784 | 23 | >active members</a><tal:invited | 25 | >active members</a><tal:invited |
785 | 24 | define="invited_member_count context/invited_member_count" | 26 | define="invited_member_count context/invited_member_count" |
786 | @@ -90,24 +92,33 @@ | |||
787 | 90 | <tal:can-view | 92 | <tal:can-view |
788 | 91 | condition="context/@@+restricted-membership/userCanViewMembership" | 93 | condition="context/@@+restricted-membership/userCanViewMembership" |
789 | 92 | define="overview_menu context/menu:overview"> | 94 | define="overview_menu context/menu:overview"> |
792 | 93 | <table style="margin: 0px 0px .5em 0px;" | 95 | <table style="margin: 0px 0px .5em 0px;"> |
791 | 94 | tal:condition="view/has_recent_approved_or_proposed_members"> | ||
793 | 95 | <tr> | 96 | <tr> |
794 | 97 | <td style="padding: 0px 1em 1em 0px;" | ||
795 | 98 | tal:define="link context/menu:overview/add_member"> | ||
796 | 99 | <span id="add-member-spinner" class="update-in-progress-message" | ||
797 | 100 | style="display: none"> | ||
798 | 101 | Saving... | ||
799 | 102 | </span> | ||
800 | 103 | <tal:add-member replace="structure link/fmt:link-icon" /> | ||
801 | 104 | </td> | ||
802 | 96 | <td style="padding: 0px 0px 1em 0px;" | 105 | <td style="padding: 0px 0px 1em 0px;" |
803 | 97 | tal:define="link context/menu:overview/mugshots"> | 106 | tal:define="link context/menu:overview/mugshots"> |
804 | 98 | <tal:mugshots replace="structure link/fmt:link-icon" /> | 107 | <tal:mugshots replace="structure link/fmt:link-icon" /> |
805 | 99 | </td> | 108 | </td> |
806 | 100 | </tr> | 109 | </tr> |
807 | 101 | <tr> | 110 | <tr> |
817 | 102 | <td style="padding: 3px 3em 0px 0px;" id="recently-approved" | 111 | <td style="padding: 3px 3em 0px 0px;"> |
818 | 103 | tal:condition="view/recently_approved_members"> | 112 | <div id="recently-approved" |
819 | 104 | <h3 style="color:black; font-weight:bold; margin: 0px"> | 113 | tal:attributes="style view/recently_approved_hidden"> |
820 | 105 | Recently approved | 114 | <h3 style="color:black; font-weight:bold; margin: 0px"> |
821 | 106 | </h3> | 115 | Latest members |
822 | 107 | <ul tal:condition="view/recently_approved_members"> | 116 | </h3> |
823 | 108 | <li tal:repeat="person view/recently_approved_members" | 117 | <ul id="recently-approved-ul"> |
824 | 109 | tal:content="structure person/fmt:link" /> | 118 | <li tal:repeat="person view/recently_approved_members" |
825 | 110 | </ul> | 119 | tal:content="structure person/fmt:link" /> |
826 | 120 | </ul> | ||
827 | 121 | </div> | ||
828 | 111 | </td> | 122 | </td> |
829 | 112 | <td style="padding: 0px;" id="recently-applied" | 123 | <td style="padding: 0px;" id="recently-applied" |
830 | 113 | tal:condition="view/recently_proposed_members"> | 124 | tal:condition="view/recently_proposed_members"> |
Summary
-------
This is the first branch on the way to adding a picker
to add members to a team with a link on its index page.
Salgado started this branch. I did a little bit of cleanup, but
the following things should be done in a followup branch.
* Add windmill test.
* Handle adding teams, which will go into a PROPOSED or INVITED status.
Implementation details ------- ------- -
-------
The picker widget now clears the search term by default when canonical/ launchpad/ javascript/ bugs/bugtask- index.js canonical/ launchpad/ javascript/ code/codereview .js
the SAVE event is processed (when an item is selected from the results).
The picker widget will also add an onclick handler for you.
lib/
lib/
The main part of this feature: canonical/ launchpad/ javascript/ registry/ team.js lp/app/ templates/ base-layout- macros. pt lp/registry/ browser/ configure. zcml lp/registry/ browser/ person. py lp/registry/ templates/ team-index. pt lp/registry/ browser/ tests/test_ person_ webservice. py lp/registry/ templates/ team-portlet- membership. pt
lib/
lib/
lib/
lib/
lib/
lib/
lib/
Change affecting existing tests: lp/registry/ model/person. py lp/registry/ model/teammembe rship.py lp/registry/ browser/ tests/teammembe rship-views. txt lp/registry/ doc/teammembers hip-email- notification. txt lp/registry/ doc/teammembers hip.txt
lib/
lib/
lib/
lib/
lib/
Tests
-----
./bin/test -vv -t 'test_person_ webservice| teammembership- views.txt| teammembership- email-notificat ion.txt| /teammembership .txt'
Demo and Q/A
------------
* Open http:// launchpad. dev/~guadamen
* The "Add member" link should be green, click on it to show the picker.
* When you click on a person in the picker, the progress spinner
should display where the (+) icon was.
* Then there should be a green flash where the person is added to the
"Latest members" list.