Merge lp:~edwin-grubbs/launchpad/bug-482176-add-team-member-ajax-part2 into lp:launchpad
- bug-482176-add-team-member-ajax-part2
- Merge into devel
Status: | Merged |
---|---|
Approved by: | Guilherme Salgado |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp:~edwin-grubbs/launchpad/bug-482176-add-team-member-ajax-part2 |
Merge into: | lp:launchpad |
Prerequisite: | lp:~edwin-grubbs/launchpad/bug-482176-add-team-member-ajax-part1 |
Diff against target: |
962 lines (+399/-202) 16 files modified
lib/canonical/launchpad/icing/style-3-0.css (+3/-0) lib/canonical/launchpad/icing/style.css (+0/-2) lib/canonical/launchpad/javascript/code/codereview.js (+7/-6) lib/canonical/launchpad/javascript/lp/errors.js (+33/-3) lib/canonical/launchpad/javascript/registry/team.js (+83/-49) lib/lp/registry/browser/person.py (+33/-21) lib/lp/registry/browser/tests/mailinglist-views.txt (+5/-4) lib/lp/registry/interfaces/person.py (+27/-17) lib/lp/registry/model/person.py (+8/-5) lib/lp/registry/model/teammembership.py (+2/-10) lib/lp/registry/stories/team/xx-team-home.txt (+21/-10) lib/lp/registry/stories/team/xx-team-membership.txt (+13/-0) lib/lp/registry/templates/team-index.pt (+1/-1) lib/lp/registry/templates/team-members.pt (+53/-51) lib/lp/registry/templates/team-portlet-membership.pt (+36/-23) lib/lp/registry/windmill/tests/test_team_index.py (+74/-0) |
To merge this branch: | bzr merge lp:~edwin-grubbs/launchpad/bug-482176-add-team-member-ajax-part2 |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Martin Albisetti (community) | ui | Approve | |
Curtis Hovey (community) | code and js | Approve | |
Guilherme Salgado (community) | Needs Fixing | ||
Canonical Launchpad Engineering | code ui | Pending | |
Review via email: mp+16325@code.launchpad.net |
Commit message
Description of the change
Edwin Grubbs (edwin-grubbs) wrote : | # |
Guilherme Salgado (salgado) wrote : | # |
Hi Edwin,
This is a nice branch but I think we're missing some test coverage for
some changes you introduced.
review needsfixing
On Fri, 2009-12-18 at 06:36 +0000, Edwin Grubbs wrote:
> Summary
> -------
>
> This branch is dependent on
> lp:~edwin-grubbs/launchpad/bug-482176-add-team-member-ajax-part1
>
> I made the following changes:
> * Added a windmill test
> * Changed the return value from IPerson.addMember() so
> the UI knows what status it decided to use.
> * If the user is allowed to invite a member because they can admin
> the owning team and if the user is allowed to accept the invitation
> because they can admin the member team, addMember() now just fully
> approves the membership.
>
> Over 50 tests broke because I changed the return value for
> addMember() and setStatus(). I only fixed one of them as a sample.
> The rest will have to wait for a followup branch, since I'm interested
> in getting feedback on the changes I made already.
I think most of these tests are probably not interested in the return
value of addMember(), and in that case I suggest you change these ones
to assign the return value of addMember() to a dummy variable so that
they don't break again when we need to change the return value once
again.
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -15,87 +15,122 @@
> * @method setup_add_
> */
> module.
> + if (Y.UA.ie) {
> + return;
> + }
> +
> var config = {
> header: 'Add a member',
> step_title: 'Search',
> picker_activator: '.menu-
> };
>
> - var picker = Y.lp.picker.create(
> - 'ValidTeamMember',
> - function(result) { _add_member(
> - config);
> + Y.lp.picker.
> };
>
> -var _add_member = function(result) {
> - var spinner = Y.one('
> - var addmember_link = Y.one('
> - addmember_
> - spinner.
> - function disable_spinner() {
> - addmember_
> - spinner.
> - }
> +var _add_member = function(
> + var box = Y.one('
> + var spinner = box.query(
> + var addmember_link = box.query(
> + addmember_
> + spinner.
> + var disable_spinner = function() {
> + addmember_
> + spinner.
> + };
> lp_client = new LP.client.
>
> var error_handler = new LP.client.
> error_handler.
> error_handler.
> - Y.lp.display_
> + Y.lp.displ...
Edwin Grubbs (edwin-grubbs) wrote : | # |
Here are screenshots of this feature that show how adding a person or a team as a member updates the appropriate list and updates the counts at the top of the portlet. If the user adds a team that he does not admin, then the team is invited. If the user does admin the team being added, it skips the invitation status and goes straight to approved.
Martin Albisetti (beuno) wrote : | # |
Hi Edwin,
The branch in general looks great, you solved the bulk of the challenges already.
As per our conversation on IRC, the actual fix for the "user is already approved" message is not getting in the situation in the first place, so filing a bug about is a good start.
If we're going to keep the dialog around for a while, I would do one of two things:
1) Add buttons to dismiss the dialog or go back to the results (OK and BACK?)
2) Show the message instead of the results (ie, keep the search fields visible to re-search)
I would also re-phrase the message to something like "$user is already part of the team"
Edwin Grubbs (edwin-grubbs) wrote : | # |
Hi Salgado,
Thanks for the review. Comments below.
> Hi Edwin,
>
> This is a nice branch but I think we're missing some test coverage for
> some changes you introduced.
>
> review needsfixing
>
> On Fri, 2009-12-18 at 06:36 +0000, Edwin Grubbs wrote:
> > Summary
> > -------
> >
> > This branch is dependent on
> > lp:~edwin-grubbs/launchpad/bug-482176-add-team-member-ajax-part1
> >
> > I made the following changes:
> > * Added a windmill test
> > * Changed the return value from IPerson.addMember() so
> > the UI knows what status it decided to use.
> > * If the user is allowed to invite a member because they can admin
> > the owning team and if the user is allowed to accept the invitation
> > because they can admin the member team, addMember() now just fully
> > approves the membership.
> >
> > Over 50 tests broke because I changed the return value for
> > addMember() and setStatus(). I only fixed one of them as a sample.
> > The rest will have to wait for a followup branch, since I'm interested
> > in getting feedback on the changes I made already.
>
> I think most of these tests are probably not interested in the return
> value of addMember(), and in that case I suggest you change these ones
> to assign the return value of addMember() to a dummy variable so that
> they don't break again when we need to change the return value once
> again.
I added the dummy variable in the mailinglist-
do that for the others.
> > === modified file 'lib/canonical/
> > --- lib/canonical/
> +0000
> > +++ lib/canonical/
> +0000
> > @@ -15,87 +15,122 @@
> > * @method setup_add_
> > */
> > module.
> > + if (Y.UA.ie) {
> > + return;
> > + }
> > +
> > var config = {
> > header: 'Add a member',
> > step_title: 'Search',
> > picker_activator: '.menu-
> > };
> >
> > - var picker = Y.lp.picker.create(
> > - 'ValidTeamMember',
> > - function(result) { _add_member(
> > - config);
> > + Y.lp.picker.
> > };
> >
> > -var _add_member = function(result) {
> > - var spinner = Y.one('
> > - var addmember_link = Y.one('
> > - addmember_
> > - spinner.
> > - function disable_spinner() {
> > - addmember_
> > - spinner.
> > - }
> > +var _add_member = function(
> > + var box = Y.one('
> > + var spinner = box.query(
> > + var addmember_link = box.query(
> > + addmember_
> > + spinner.
> > + var disable_spinner = function() {
> > + addmember_
> > + spinner.
> > + };
> > ...
Edwin Grubbs (edwin-grubbs) wrote : | # |
Here is the incremental diff that addresses both Salgado's and Martin's comments.
{{{
=== modified file 'lib/canonical/
--- lib/canonical/
+++ lib/canonical/
@@ -69,7 +69,7 @@
* @param flash_node {Node} The node to red flash.
* @param msg {String} The message to display.
*/
-var display_error = function(
+Y.lp.display_error = function(
create_
maybe_
@@ -77,5 +77,36 @@
});
};
-Y.lp.display_error = display_error;
-}, "0.1", {"requires"
+
+var info_overlay;
+/*
+ * Display the form overlay for non-error informational messages.
+ *
+ * @method display_info
+ * @param msg {String} The message to display.
+*/
+Y.lp.display_info = function(msg) {
+ if (info_overlay === undefined) {
+ info_overlay = new Y.lazr.
+ //headerContent: '<h2><img src="/@
+ centered: true,
+ visible: false
+ });
+ info_overlay.
+ }
+ var content = Y.Node.create(
+ '<div style="background: url(/@@/info-large) no-repeat; ' +
+ 'min-height: 32px; padding-left: 40px; padding-top: 16px"/></div>');
+ content.
+ var button_div = Y.Node.create('<div style="text-align: right"></div>');
+ var ok_button = Y.Node.
+ ok_button.
+ info_overlay.
+ });
+ button_
+ content.
+ info_overlay.
+ info_overlay.
+};
+
+}, "0.1", {"requires"
=== modified file 'lib/canonical/
--- lib/canonical/
+++ lib/canonical/
@@ -54,13 +54,14 @@
if (did_status_change === false) {
- Y.lp.display_error(
- addmember_link,
+ Y.lp.display_info(
- current_status + '.');
+ current_
+ ' as a member of the team.');
}
+ current_status = 'foo';
if (current_status == 'Invited') {
@@ -79,6 +80,7 @@
+ return;
}
Edwin Grubbs (edwin-grubbs) wrote : | # |
Hi Martin,
Thanks for the review. For some reason, your comment didn't get added to the merge proposal, so I pasted it here again.
On Mon, Dec 21, 2009 at 1:35 PM, Martin Albisetti <email address hidden> wrote:
> Review: Needs Fixing ui
> Hi Edwin,
>
> The branch in general looks great, you solved the bulk of the challenges already.
> As per our conversation on IRC, the actual fix for the "user is already approved" message is not getting in the situation in the first place, so filing a bug about is a good start.
Opened bug 499210 for this.
> If we're going to keep the dialog around for a while, I would do one of two things:
>
> 1) Add buttons to dismiss the dialog or go back to the results (OK and BACK?)
> 2) Show the message instead of the results (ie, keep the search fields visible to re-search)
I added an "OK" button to dismiss the dialog.
> I would also re-phrase the message to something like "$user is already part of the team"
There actually are two potential errors, since a team could already be in an invited status, so it would be unclear to say that they are already part of the team. Here are screenshots of both error messages.
https:/
https:/
Curtis Hovey (sinzui) wrote : | # |
Hi Edwin.
This is a nice addition. I have no remarks about new test. I have a suggestion about the team story
> === modified file 'lib/lp/
> --- lib/lp/
> +++ lib/lp/
...
> @@ -88,14 +93,14 @@
> Show received invitations
>
> If the team does not have any recently approved or proposed members,
> -the recent members sections are not displayed:
> +the recent members sections are hidden using the "unseen" css class:
>
> >>> browser.open('http://
> >>> print find_tag_
> - None
> + <td id="recently-
>
> - >>> print find_tag_
> - None
> + >>> print find_tag_
> + <td id="recently-
These tests will break if we reimplement this as a list. We should not print
markup in user stories because they do no read the source. The test is parsing
the tree many times. I think this can be expressed like this so that it runs
faster and is robust.
>>> browser.open('http://
>>> content = find_main_
>>> tag = find_tag_
>>> print tag['class']
unseen
>>> tag = find_tag_
>>> print tag['class']
unseen
.
Martin Albisetti (beuno) wrote : | # |
Thanks Edwin. The dialog now looks better, minus an alignment issue between the text and the icon. If you could top-align the text with the icon, it would be super great.
The only remaining issue for me is the fact that the persons' name doesn't appear on the page grayed out while it's being saved, like everywhere else.
Could you factor in that behavior?
Edwin Grubbs (edwin-grubbs) wrote : | # |
> Thanks Edwin. The dialog now looks better, minus an alignment issue between
> the text and the icon. If you could top-align the text with the icon, it would
> be super great.
>
> The only remaining issue for me is the fact that the persons' name doesn't
> appear on the page grayed out while it's being saved, like everywhere else.
>
> Could you factor in that behavior?
Martin, I've run into a snag adding the grayed out team/person with a spinner. A team may be added to the "Latest members" or the "Latest invited" list depending on whether the user is also an admin on the team being added as a member. Also, the "Latest members" and "Latest invited" labels may need to be unhidden and presumably re-hidden if they are empty at the end of the process. It seems like a bad idea to make another REST request to gather the information just to put the spinner in the right spot.
Guilherme Salgado (salgado) wrote : | # |
On Mon, 2009-12-21 at 20:54 +0000, Edwin Grubbs wrote:
> Here is the incremental diff that addresses both Salgado's and
> Martin's comments.
Looks good to me, but there's some code that was apparently written to
test corner cases and left behind accidentally. See below
>
> {{{
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -69,7 +69,7 @@
> * @param flash_node {Node} The node to red flash.
> * @param msg {String} The message to display.
> */
> -var display_error = function(
> +Y.lp.display_error = function(
> create_
> maybe_red_
> error_overlay.
> @@ -77,5 +77,36 @@
> });
> };
>
> -Y.lp.display_error = display_error;
> -}, "0.1", {"requires"
> +
> +var info_overlay;
> +/*
> + * Display the form overlay for non-error informational messages.
> + *
> + * @method display_info
> + * @param msg {String} The message to display.
> +*/
> +Y.lp.display_info = function(msg) {
> + if (info_overlay === undefined) {
> + info_overlay = new Y.lazr.
> + //headerContent: '<h2><img src="/@
Why is this commented out?
> + centered: true,
> + visible: false
> + });
> + info_overlay.
> + }
> + var content = Y.Node.create(
> + '<div style="background: url(/@@/info-large) no-repeat; ' +
> + 'min-height: 32px; padding-left: 40px; padding-top: 16px"/></div>');
> + content.
> + var button_div = Y.Node.create('<div style="text-align: right"></div>');
> + var ok_button = Y.Node.
> + ok_button.
> + info_overlay.
> + });
> + button_
> + content.
> + info_overlay.
> + info_overlay.
> +};
> +
> +}, "0.1", {"requires"
>
> === modified file 'lib/canonical/
> --- lib/canonical/
> +++ lib/canonical/
> @@ -54,13 +54,14 @@
> var members_section, members_ul, count_elem;
> if (did_status_change === false) {
> disable_spinner();
> - Y.lp.display_error(
> - addmember_link,
> + Y.lp.display_info(
> selected_
> - current_status + '.');
> + current_
> + ' as a member of the team.');
> return;
> }
> var is_team = false;
> + current_status = 'foo';
!?
I really hope your windmill te...
Martin Albisetti (beuno) wrote : | # |
This is a pattern we will need to solve many times, so lets land this now, and start a discussion on how to fix it in a more general manner.
Thanks Edwin, this seems good to land for me.
Edwin Grubbs (edwin-grubbs) wrote : | # |
I removed the commented out headerContent, which was unnecessary. I also removed the debugging code that set current_status to 'foo'.
Preview Diff
1 | === modified file 'lib/canonical/launchpad/icing/style-3-0.css' |
2 | --- lib/canonical/launchpad/icing/style-3-0.css 2009-12-21 15:44:34 +0000 |
3 | +++ lib/canonical/launchpad/icing/style-3-0.css 2010-01-04 18:54:21 +0000 |
4 | @@ -215,6 +215,9 @@ |
5 | font-weight: bold; |
6 | } |
7 | |
8 | +.unseen {display: none;} |
9 | + |
10 | + |
11 | /* Common presentations */ |
12 | div.see-all { |
13 | font-size: 123.1%; /* Same as a h2 */ |
14 | |
15 | === modified file 'lib/canonical/launchpad/icing/style.css' |
16 | --- lib/canonical/launchpad/icing/style.css 2009-12-19 19:00:37 +0000 |
17 | +++ lib/canonical/launchpad/icing/style.css 2010-01-04 18:54:21 +0000 |
18 | @@ -428,8 +428,6 @@ |
19 | fieldset .expanded {display: block;} |
20 | fieldset.collapsible legend {font-weight: normal;} |
21 | |
22 | -.unseen {display: none;} |
23 | - |
24 | form.primary.search { |
25 | margin-bottom: 2em; |
26 | } |
27 | |
28 | === modified file 'lib/canonical/launchpad/javascript/code/codereview.js' |
29 | --- lib/canonical/launchpad/javascript/code/codereview.js 2010-01-04 18:54:20 +0000 |
30 | +++ lib/canonical/launchpad/javascript/code/codereview.js 2010-01-04 18:54:21 +0000 |
31 | @@ -22,12 +22,13 @@ |
32 | var link = Y.one('#request-review'); |
33 | if (link !== null) { |
34 | link.addClass('js-action'); |
35 | - /* XXX: salgado, 2009-11-11: This will cause the picker to be |
36 | - * recreated every time the user clicks on the link. Although that |
37 | - * makes it unnecessary to have the widget cleared, it makes it |
38 | - * impossible to persist the state of the picker between clicks on the |
39 | - * link. We should probably have a policy to enforce that we |
40 | - * just hide/show widgets when a link is clicked more than once, |
41 | + /* XXX: salgado 2009-11-11 bug=497603 |
42 | + * This will cause the picker to be recreated every time the |
43 | + * user clicks on the link. Although that makes it unnecessary |
44 | + * to have the widget cleared, it makes it impossible to persist |
45 | + * the state of the picker between clicks on the link. We |
46 | + * should probably have a policy to enforce that we just |
47 | + * hide/show widgets when a link is clicked more than once, |
48 | * instead of recreating the widgets every time. |
49 | */ |
50 | link.on('click', show_request_review_form); |
51 | |
52 | === modified file 'lib/canonical/launchpad/javascript/lp/errors.js' |
53 | --- lib/canonical/launchpad/javascript/lp/errors.js 2009-11-23 19:29:02 +0000 |
54 | +++ lib/canonical/launchpad/javascript/lp/errors.js 2010-01-04 18:54:21 +0000 |
55 | @@ -69,7 +69,7 @@ |
56 | * @param flash_node {Node} The node to red flash. |
57 | * @param msg {String} The message to display. |
58 | */ |
59 | -var display_error = function(flash_node, msg) { |
60 | +Y.lp.display_error = function(flash_node, msg) { |
61 | create_error_overlay(); |
62 | maybe_red_flash(flash_node, function(){ |
63 | error_overlay.showError(msg); |
64 | @@ -77,5 +77,35 @@ |
65 | }); |
66 | }; |
67 | |
68 | -Y.lp.display_error = display_error; |
69 | -}, "0.1", {"requires":["lazr.formoverlay"]}); |
70 | + |
71 | +var info_overlay; |
72 | +/* |
73 | + * Display the form overlay for non-error informational messages. |
74 | + * |
75 | + * @method display_info |
76 | + * @param msg {String} The message to display. |
77 | +*/ |
78 | +Y.lp.display_info = function(msg) { |
79 | + if (info_overlay === undefined) { |
80 | + info_overlay = new Y.lazr.PrettyOverlay({ |
81 | + centered: true, |
82 | + visible: false |
83 | + }); |
84 | + info_overlay.render(); |
85 | + } |
86 | + var content = Y.Node.create( |
87 | + '<div style="background: url(/@@/info-large) no-repeat; ' + |
88 | + 'min-height: 32px; padding-left: 40px; padding-top: 16px"/></div>'); |
89 | + content.appendChild(Y.Node.create(msg)); |
90 | + var button_div = Y.Node.create('<div style="text-align: right"></div>'); |
91 | + var ok_button = Y.Node.create('<button>OK</button>'); |
92 | + ok_button.on('click', function(e) { |
93 | + info_overlay.fire('cancel'); |
94 | + }); |
95 | + button_div.appendChild(ok_button); |
96 | + content.appendChild(button_div); |
97 | + info_overlay.set('bodyContent', content); |
98 | + info_overlay.show(); |
99 | +}; |
100 | + |
101 | +}, "0.1", {"requires":["lazr.formoverlay", "lazr.overlay"]}); |
102 | |
103 | === modified file 'lib/canonical/launchpad/javascript/registry/team.js' |
104 | --- lib/canonical/launchpad/javascript/registry/team.js 2010-01-04 18:54:20 +0000 |
105 | +++ lib/canonical/launchpad/javascript/registry/team.js 2010-01-04 18:54:21 +0000 |
106 | @@ -15,87 +15,121 @@ |
107 | * @method setup_add_member_handler |
108 | */ |
109 | module.setup_add_member_handler = function() { |
110 | + if (Y.UA.ie) { |
111 | + return; |
112 | + } |
113 | + |
114 | var config = { |
115 | header: 'Add a member', |
116 | step_title: 'Search', |
117 | picker_activator: '.menu-link-add_member' |
118 | }; |
119 | |
120 | - var picker = Y.lp.picker.create( |
121 | - 'ValidTeamMember', |
122 | - function(result) { _add_member(result); }, |
123 | - config); |
124 | + Y.lp.picker.create('ValidTeamMember', _add_member, config); |
125 | }; |
126 | |
127 | -var _add_member = function(result) { |
128 | - var spinner = Y.one('#add-member-spinner'); |
129 | - var addmember_link = Y.one('.menu-link-add_member'); |
130 | - addmember_link.setStyle('display', 'none'); |
131 | - spinner.setStyle('display', 'inline'); |
132 | - function disable_spinner() { |
133 | - addmember_link.setStyle('display', 'inline'); |
134 | - spinner.setStyle('display', 'none'); |
135 | - } |
136 | +var _add_member = function(selected_person) { |
137 | + var box = Y.one('#membership'); |
138 | + var spinner = box.query('#add-member-spinner'); |
139 | + var addmember_link = box.query('.menu-link-add_member'); |
140 | + addmember_link.addClass('unseen'); |
141 | + spinner.removeClass('unseen'); |
142 | + var disable_spinner = function() { |
143 | + addmember_link.removeClass('unseen'); |
144 | + spinner.addClass('unseen'); |
145 | + }; |
146 | lp_client = new LP.client.Launchpad(); |
147 | |
148 | var error_handler = new LP.client.ErrorHandler(); |
149 | error_handler.clearProgressUI = disable_spinner; |
150 | error_handler.showError = function(error_msg) { |
151 | - Y.lp.display_error(Y.one('.menu-link-add_member'), error_msg); |
152 | + Y.lp.display_error(addmember_link, error_msg); |
153 | }; |
154 | |
155 | - config = { |
156 | + addmember_config = { |
157 | on: { |
158 | - success: function(member_added) { |
159 | - if (!member_added) { |
160 | - disable_spinner(); |
161 | - alert('Already a member.'); |
162 | - return; |
163 | - } |
164 | - if (result.css.match("team")) { |
165 | - disable_spinner(); |
166 | - alert('This is a team'); |
167 | - return; |
168 | - } |
169 | - var members_section = Y.one('#recently-approved'); |
170 | - var members_ul = Y.one('#recently-approved-ul'); |
171 | + success: function(change_and_status) { |
172 | + var did_status_change = change_and_status[0]; |
173 | + var current_status = change_and_status[1]; |
174 | + var members_section, members_ul, count_elem; |
175 | + if (did_status_change === false) { |
176 | + disable_spinner(); |
177 | + Y.lp.display_info( |
178 | + selected_person.title + ' is already ' + |
179 | + current_status.toLowerCase() + |
180 | + ' as a member of the team.'); |
181 | + return; |
182 | + } |
183 | + |
184 | + if (current_status == 'Invited') { |
185 | + members_section = box.query('#recently-invited'); |
186 | + members_ul = box.query('#recently-invited-ul'); |
187 | + count_elem = box.query('#invited-member-count'); |
188 | + } else if (current_status == 'Proposed') { |
189 | + members_section = box.query('#recently-proposed'); |
190 | + members_ul = box.query('#recently-proposed-ul'); |
191 | + count_elem = box.query('#proposed-member-count'); |
192 | + } else if (current_status == 'Approved') { |
193 | + members_section = box.query('#recently-approved'); |
194 | + members_ul = box.query('#recently-approved-ul'); |
195 | + count_elem = box.query('#approved-member-count'); |
196 | + } else { |
197 | + Y.lp.display_error( |
198 | + addmember_link, |
199 | + 'Unexpected status: ' + current_status); |
200 | + return; |
201 | + } |
202 | var first_node = members_ul.get('firstChild'); |
203 | - config = { |
204 | + |
205 | + var xhtml_person_handler = function(person_html) { |
206 | + if (count_elem === null && current_status == 'Invited') { |
207 | + count_elem = Y.Node.create( |
208 | + '<strong id="invited-member-count">' + |
209 | + '1</strong>'); |
210 | + var count_box = Y.one( |
211 | + '#membership #membership-counts'); |
212 | + count_box.append(Y.Node.create( |
213 | + '<span>, </span>')); |
214 | + count_box.append(count_elem); |
215 | + count_box.append(Y.Node.create( |
216 | + '<span> <a href="/+members#invited">' + |
217 | + 'invited members</a></span>')); |
218 | + } else { |
219 | + var count = count_elem.get('innerHTML'); |
220 | + count = parseInt(count, 10) + 1; |
221 | + count_elem.set('innerHTML', count); |
222 | + } |
223 | + person_repr = Y.Node.create( |
224 | + '<li>' + person_html + '</li>'); |
225 | + members_section.removeClass('unseen'); |
226 | + members_ul.insertBefore(person_repr, first_node); |
227 | + anim = Y.lazr.anim.green_flash({node: person_repr}); |
228 | + anim.run(); |
229 | + disable_spinner(); |
230 | + }; |
231 | + |
232 | + xhtml_person_config = { |
233 | on: { |
234 | - success: function(person_html) { |
235 | - var total_members = Y.one( |
236 | - '#member-count').get('innerHTML'); |
237 | - total_members = parseInt(total_members, 10) + 1; |
238 | - Y.one('#member-count').set( |
239 | - 'innerHTML', total_members); |
240 | - person_repr = Y.Node.create( |
241 | - '<li>' + person_html + '</li>'); |
242 | - members_section.setStyle('visibility', 'visible'); |
243 | - members_ul.insertBefore( |
244 | - person_repr, first_node); |
245 | - anim = Y.lazr.anim.green_flash( |
246 | - {node: person_repr}); |
247 | - anim.run(); |
248 | - disable_spinner(); |
249 | - }, |
250 | + success: xhtml_person_handler, |
251 | failure: error_handler.getFailureHandler() |
252 | }, |
253 | accept: LP.client.XHTML |
254 | }; |
255 | - lp_client.get(result.api_uri, config); |
256 | + lp_client.get(selected_person.api_uri, xhtml_person_config); |
257 | }, |
258 | failure: error_handler.getFailureHandler() |
259 | }, |
260 | parameters: { |
261 | - // XXX: Why do I always have to get absolute URIs out of the URIs |
262 | + // XXX: EdwinGrubbs 2009-12-16 bug=497602 |
263 | + // Why do I always have to get absolute URIs out of the URIs |
264 | // in the picker's result/client.links? |
265 | reviewer: LP.client.get_absolute_uri(LP.client.links.me), |
266 | - person: LP.client.get_absolute_uri(result.api_uri) |
267 | + person: LP.client.get_absolute_uri(selected_person.api_uri) |
268 | } |
269 | }; |
270 | |
271 | lp_client.named_post( |
272 | - LP.client.cache.context.self_link, 'addMember', config); |
273 | + LP.client.cache.context.self_link, 'addMember', addmember_config); |
274 | }; |
275 | |
276 | }, '0.1', {requires: [ |
277 | |
278 | === modified file 'lib/lp/registry/browser/person.py' |
279 | --- lib/lp/registry/browser/person.py 2010-01-04 18:54:20 +0000 |
280 | +++ lib/lp/registry/browser/person.py 2010-01-04 18:54:21 +0000 |
281 | @@ -95,6 +95,7 @@ |
282 | from itertools import chain |
283 | from operator import attrgetter, itemgetter |
284 | from textwrap import dedent |
285 | +from storm.zope.interfaces import IResultSet |
286 | |
287 | from zope.error.interfaces import IErrorReportingUtility |
288 | from zope.app.form.browser import TextAreaWidget, TextWidget |
289 | @@ -227,13 +228,8 @@ |
290 | logoutPerson, allowUnauthenticatedSession) |
291 | from canonical.launchpad.webapp.menu import get_current_view |
292 | from canonical.launchpad.webapp.publisher import LaunchpadView |
293 | -<<<<<<< TREE |
294 | -from canonical.launchpad.webapp.tales import ( |
295 | - DateTimeFormatterAPI, FormattersAPI) |
296 | -======= |
297 | -from canonical.launchpad.webapp.tales import ( |
298 | - DateTimeFormatterAPI, PersonFormatterAPI) |
299 | ->>>>>>> MERGE-SOURCE |
300 | +from canonical.launchpad.webapp.tales import ( |
301 | + DateTimeFormatterAPI, FormattersAPI, PersonFormatterAPI) |
302 | from lazr.uri import URI, InvalidURIError |
303 | |
304 | from canonical.launchpad import _ |
305 | @@ -2489,11 +2485,7 @@ |
306 | |
307 | @property |
308 | def next_url(self): |
309 | -<<<<<<< TREE |
310 | """Redirect to the +languages page if request originated there.""" |
311 | -======= |
312 | - """Redirect back to the originating page.""" |
313 | ->>>>>>> MERGE-SOURCE |
314 | redirection_url = self.request.get('redirection_url') |
315 | if redirection_url: |
316 | return redirection_url |
317 | @@ -2501,11 +2493,7 @@ |
318 | |
319 | @property |
320 | def cancel_url(self): |
321 | -<<<<<<< TREE |
322 | """Redirect to the +languages page if request originated there.""" |
323 | -======= |
324 | - """Redirect back to the originating page.""" |
325 | ->>>>>>> MERGE-SOURCE |
326 | redirection_url = self.getRedirectionURL() |
327 | if redirection_url: |
328 | return redirection_url |
329 | @@ -2745,6 +2733,13 @@ |
330 | orderBy='-TeamMembership.date_proposed') |
331 | return members[:5] |
332 | |
333 | + @cachedproperty |
334 | + def recently_invited_members(self): |
335 | + members = self.context.getMembersByStatus( |
336 | + TeamMembershipStatus.INVITED, |
337 | + orderBy='-TeamMembership.date_proposed') |
338 | + return members[:5] |
339 | + |
340 | @property |
341 | def recently_approved_hidden(self): |
342 | """Optionally hide the div. |
343 | @@ -2752,8 +2747,8 @@ |
344 | The AJAX on the page needs the elements to be present |
345 | but hidden in case it adds a member to the list. |
346 | """ |
347 | - if self.recently_approved_members.count() == 0: |
348 | - return 'visibility: collapse' |
349 | + if IResultSet(self.recently_approved_members).is_empty(): |
350 | + return 'unseen' |
351 | else: |
352 | return '' |
353 | |
354 | @@ -2764,8 +2759,20 @@ |
355 | The AJAX on the page needs the elements to be present |
356 | but hidden in case it adds a member to the list. |
357 | """ |
358 | - if self.recently_proposed_members.count() == 0: |
359 | - return 'visibility: collapse' |
360 | + if IResultSet(self.recently_proposed_members).is_empty(): |
361 | + return 'unseen' |
362 | + else: |
363 | + return '' |
364 | + |
365 | + @property |
366 | + def recently_invited_hidden(self): |
367 | + """Optionally hide the div. |
368 | + |
369 | + The AJAX on the page needs the elements to be present |
370 | + but hidden in case it adds a member to the list. |
371 | + """ |
372 | + if IResultSet(self.recently_invited_members).is_empty(): |
373 | + return 'unseen' |
374 | else: |
375 | return '' |
376 | |
377 | @@ -4241,9 +4248,14 @@ |
378 | def continue_action(self, action, data): |
379 | """Make the selected teams join this team.""" |
380 | context = self.context |
381 | + is_admin = check_permission('launchpad.Admin', context) |
382 | for team in data['teams']: |
383 | - team.join(context, requester=self.user) |
384 | - if context.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED: |
385 | + if is_admin: |
386 | + context.addMember(team, reviewer=self.user) |
387 | + else: |
388 | + team.join(context, requester=self.user) |
389 | + if (context.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED |
390 | + and not is_admin): |
391 | msg = 'proposed to this team.' |
392 | else: |
393 | msg = 'added to this team.' |
394 | |
395 | === modified file 'lib/lp/registry/browser/tests/mailinglist-views.txt' |
396 | --- lib/lp/registry/browser/tests/mailinglist-views.txt 2009-12-03 19:12:02 +0000 |
397 | +++ lib/lp/registry/browser/tests/mailinglist-views.txt 2010-01-04 18:54:21 +0000 |
398 | @@ -33,7 +33,7 @@ |
399 | No Privileges Person is added as a member of the Aardvarks. |
400 | |
401 | >>> no_priv = person_set.getByEmail('no-priv@canonical.com') |
402 | - >>> aardvarks.addMember(no_priv, sample_person) |
403 | + >>> ignored = aardvarks.addMember(no_priv, sample_person) |
404 | |
405 | But regular members can't access the +mailinglist page. |
406 | |
407 | @@ -52,8 +52,9 @@ |
408 | >>> team_membership = getUtility(ITeamMembershipSet).getByPersonAndTeam( |
409 | ... no_priv, aardvarks) |
410 | >>> team_membership.status |
411 | - <DBItem TeamMembershipStatus.APPROVED, (2) Approved> |
412 | - >>> team_membership.setStatus(TeamMembershipStatus.ADMIN, sample_person) |
413 | + <DBItem TeamMembershipStatus.APPROVED... |
414 | + >>> ignored = team_membership.setStatus( |
415 | + ... TeamMembershipStatus.ADMIN, sample_person) |
416 | |
417 | Now No Privileges Person has permission to request mailing lists. |
418 | |
419 | @@ -208,7 +209,7 @@ |
420 | |
421 | A non-owner member can see information about the mailing list. |
422 | |
423 | - >>> pmt.addMember(sample_person, owner) |
424 | + >>> ignored = pmt.addMember(sample_person, owner) |
425 | >>> login('test@canonical.com') |
426 | >>> view = create_initialized_view(pmt, '+index') |
427 | >>> print view.archive_url |
428 | |
429 | === modified file 'lib/lp/registry/interfaces/person.py' |
430 | --- lib/lp/registry/interfaces/person.py 2009-12-09 23:11:31 +0000 |
431 | +++ lib/lp/registry/interfaces/person.py 2010-01-04 18:54:21 +0000 |
432 | @@ -1406,23 +1406,33 @@ |
433 | may_subscribe_to_list=True): |
434 | """Add the given person as a member of this team. |
435 | |
436 | - If the given person is already a member of this team we'll simply |
437 | - change its membership status. Otherwise a new TeamMembership is |
438 | - created with the given status. |
439 | - |
440 | - If the person is actually a team and force_team_add is False, the |
441 | - team will actually be invited to join this one. Otherwise the team |
442 | - is added as if it were a person. |
443 | - |
444 | - If the person is not a team, and may_subscribe_to_list |
445 | - is True, then the person may be subscribed to the team's |
446 | - mailing list, depending on the list status and the person's |
447 | - auto-subscribe settings. |
448 | - |
449 | - The given status must be either Approved, Proposed or Admin. |
450 | - |
451 | - The reviewer is the user who made the given person a member of this |
452 | - team. |
453 | + :param person: If the given person is already a member of this |
454 | + team we'll simply change its membership status. Otherwise a new |
455 | + TeamMembership is created with the given status. |
456 | + |
457 | + :param reviewer: The user who made the given person a member of this |
458 | + team. |
459 | + |
460 | + :param comment: String that will be assigned to the |
461 | + proponent_comment, reviwer_comment, or acknowledger comment. |
462 | + |
463 | + :param status: `TeamMembershipStatus` value must be either |
464 | + Approved, Proposed or Admin. |
465 | + |
466 | + :param force_team_add: If the person is actually a team and |
467 | + force_team_add is False, the team will actually be invited to |
468 | + join this one. Otherwise the team is added as if it were a |
469 | + person. |
470 | + |
471 | + :param may_subscribe_to_list: If the person is not a team, and |
472 | + may_subscribe_to_list is True, then the person may be subscribed |
473 | + to the team's mailing list, depending on the list status and the |
474 | + person's auto-subscribe settings. |
475 | + |
476 | + :return: A tuple containing a boolean indicating when the |
477 | + membership status changed and the current `TeamMembershipStatus`. |
478 | + This depends on the desired status passed as an argument, the |
479 | + subscription policy and the user's priveleges. |
480 | """ |
481 | |
482 | def deactivateAllMembers(comment, reviewer): |
483 | |
484 | === modified file 'lib/lp/registry/model/person.py' |
485 | --- lib/lp/registry/model/person.py 2010-01-04 18:54:20 +0000 |
486 | +++ lib/lp/registry/model/person.py 2010-01-04 18:54:21 +0000 |
487 | @@ -1231,12 +1231,15 @@ |
488 | # By default, teams can only be invited as members, meaning that |
489 | # one of the team's admins will have to accept the invitation |
490 | # before the team is made a member. If force_team_add is True, |
491 | - # though, then we'll add a team as if it was a person. |
492 | - if not force_team_add: |
493 | + # or the user is also an admin of the proposed member, then |
494 | + # we'll add a team as if it was a person. |
495 | + is_reviewer_admin_of_new_member = ( |
496 | + person in reviewer.getAdministratedTeams()) |
497 | + if not force_team_add and not is_reviewer_admin_of_new_member: |
498 | status = TeamMembershipStatus.INVITED |
499 | event = TeamInvitationEvent |
500 | |
501 | - member_added = True |
502 | + status_changed = True |
503 | expires = self.defaultexpirationdate |
504 | tm = TeamMembership.selectOneBy(person=person, team=self) |
505 | if tm is None: |
506 | @@ -1251,12 +1254,12 @@ |
507 | # We can't use tm.setExpirationDate() here because the reviewer |
508 | # here will be the member themselves when they join an OPEN team. |
509 | tm.dateexpires = expires |
510 | - member_added = tm.setStatus(status, reviewer, comment) |
511 | + status_changed = tm.setStatus(status, reviewer, comment) |
512 | |
513 | if not person.is_team and may_subscribe_to_list: |
514 | person.autoSubscribeToMailingList(self.mailing_list, |
515 | requester=reviewer) |
516 | - return member_added |
517 | + return (status_changed, tm.status) |
518 | |
519 | # The three methods below are not in the IPerson interface because we want |
520 | # to protect them with a launchpad.Edit permission. We could do that by |
521 | |
522 | === modified file 'lib/lp/registry/model/teammembership.py' |
523 | --- lib/lp/registry/model/teammembership.py 2010-01-04 18:54:20 +0000 |
524 | +++ lib/lp/registry/model/teammembership.py 2010-01-04 18:54:21 +0000 |
525 | @@ -318,7 +318,6 @@ |
526 | "%(team)s is a member of %(person)s." |
527 | % dict(person=self.person.name, team=self.team.name)) |
528 | |
529 | - |
530 | old_status = self.status |
531 | self.status = status |
532 | |
533 | @@ -367,17 +366,10 @@ |
534 | # When a member proposes himself, a more detailed notification is |
535 | # sent to the team admins by a subscriber of JoinTeamEvent; that's |
536 | # why we don't send anything here. |
537 | -<<<<<<< TREE |
538 | - if self.person == self.last_changed_by and self.status == proposed: |
539 | - return |
540 | - |
541 | - if not silent: |
542 | - self._sendStatusChangeNotification(old_status) |
543 | -======= |
544 | - if self.person != self.last_changed_by or self.status != proposed: |
545 | + if ((self.person != self.last_changed_by or self.status != proposed) |
546 | + and not silent): |
547 | self._sendStatusChangeNotification(old_status) |
548 | return True |
549 | ->>>>>>> MERGE-SOURCE |
550 | |
551 | def _sendStatusChangeNotification(self, old_status): |
552 | """Send a status change notification to all team admins and the |
553 | |
554 | === modified file 'lib/lp/registry/stories/team/xx-team-home.txt' |
555 | --- lib/lp/registry/stories/team/xx-team-home.txt 2009-11-22 15:43:16 +0000 |
556 | +++ lib/lp/registry/stories/team/xx-team-home.txt 2010-01-04 18:54:21 +0000 |
557 | @@ -13,19 +13,24 @@ |
558 | |
559 | >>> print extract_text( |
560 | ... find_tag_by_id(browser.contents, 'recently-approved')) |
561 | - Recently approved |
562 | + Latest members |
563 | Warty Gnome Team |
564 | Daniel Silverstone |
565 | Celso Providelo |
566 | Steve Alexander... |
567 | |
568 | >>> print extract_text( |
569 | - ... find_tag_by_id(browser.contents, 'recently-applied')) |
570 | + ... find_tag_by_id(browser.contents, 'recently-proposed')) |
571 | Pending approval |
572 | Sample Person |
573 | Andrew Bennetts |
574 | |
575 | >>> print extract_text( |
576 | + ... find_tag_by_id(browser.contents, 'recently-invited')) |
577 | + Latest invited |
578 | + Warty Security Team |
579 | + |
580 | + >>> print extract_text( |
581 | ... find_tag_by_id(browser.contents, 'team-owner')) |
582 | Owner: |
583 | Mark Shuttleworth |
584 | @@ -87,15 +92,21 @@ |
585 | itself is not a member of any other team. |
586 | Show received invitations |
587 | |
588 | -If the team does not have any recently approved or proposed members, |
589 | -the recent members sections are not displayed: |
590 | +If the team does not have any recently approved, proposed, or invited |
591 | +members, the empty lists are hidden using the "unseen" css class: |
592 | |
593 | >>> browser.open('http://launchpad.dev/~launchpad') |
594 | - >>> print find_tag_by_id(browser.contents, 'recently-approved') |
595 | - None |
596 | - |
597 | - >>> print find_tag_by_id(browser.contents, 'recently-applied') |
598 | - None |
599 | + >>> tag = find_tag_by_id(browser.contents, 'recently-approved') |
600 | + >>> print tag['class'] |
601 | + unseen |
602 | + |
603 | + >>> tag = find_tag_by_id(browser.contents, 'recently-proposed') |
604 | + >>> print tag['class'] |
605 | + unseen |
606 | + |
607 | + >>> tag = find_tag_by_id(browser.contents, 'recently-invited') |
608 | + >>> print tag['class'] |
609 | + unseen |
610 | |
611 | In the above case there's no user logged in, so it doesn't actually show |
612 | what's the user's involvement with the team. If the user logs in, he'll see |
613 | @@ -187,7 +198,7 @@ |
614 | >>> owner_browser = setupBrowser(auth="Basic foo.bar@canonical.com:test") |
615 | >>> owner_browser.open('http://launchpad.dev/~ubuntu-team') |
616 | >>> print extract_text( |
617 | - ... find_tag_by_id(owner_browser.contents, 'recently-applied')) |
618 | + ... find_tag_by_id(owner_browser.contents, 'recently-proposed')) |
619 | Pending approval |
620 | Sample Person |
621 | Andrew Bennetts |
622 | |
623 | === modified file 'lib/lp/registry/stories/team/xx-team-membership.txt' |
624 | --- lib/lp/registry/stories/team/xx-team-membership.txt 2009-11-22 15:43:16 +0000 |
625 | +++ lib/lp/registry/stories/team/xx-team-membership.txt 2010-01-04 18:54:21 +0000 |
626 | @@ -174,6 +174,19 @@ |
627 | >>> message in second_browser.contents |
628 | True |
629 | |
630 | +An admin can see the former members of the team. |
631 | + |
632 | + >>> browser.open('http://launchpad.dev/~name18/+members') |
633 | + >>> print extract_text( |
634 | + ... find_tag_by_id(browser.contents, 'inactivemembers')) |
635 | + Name Joined in Status... |
636 | + |
637 | +Other users cannot see the former members of the team. |
638 | + |
639 | + >>> user_browser.open('http://launchpad.dev/~name18/+members') |
640 | + >>> print find_tag_by_id(user_browser.contents, 'inactivemembers') |
641 | + None |
642 | + |
643 | |
644 | Team Participation page |
645 | ======================= |
646 | |
647 | === modified file 'lib/lp/registry/templates/team-index.pt' |
648 | --- lib/lp/registry/templates/team-index.pt 2010-01-04 18:54:20 +0000 |
649 | +++ lib/lp/registry/templates/team-index.pt 2010-01-04 18:54:21 +0000 |
650 | @@ -86,7 +86,7 @@ |
651 | <div class="first yui-u"> |
652 | <metal:details use-macro="context/@@+person-macros/team-details" /> |
653 | </div> |
654 | - <div class="first yui-u"> |
655 | + <div class="yui-u"> |
656 | <div tal:content="structure context/@@+portlet-membership" /> |
657 | </div> |
658 | </div> |
659 | |
660 | === modified file 'lib/lp/registry/templates/team-members.pt' |
661 | --- lib/lp/registry/templates/team-members.pt 2009-10-26 14:23:12 +0000 |
662 | +++ lib/lp/registry/templates/team-members.pt 2010-01-04 18:54:21 +0000 |
663 | @@ -207,57 +207,59 @@ |
664 | |
665 | <li tal:condition="view/inactive_memberships/batch/total" |
666 | tal:define="inactive_batch nocall:view/inactive_memberships/currentBatch"> |
667 | - <a name="former"></a> |
668 | - <h2>Former members</h2> |
669 | - |
670 | - <p> |
671 | - These are the members whose subscriptions have expired, or were |
672 | - deactivated by themselves or by a team administrator. |
673 | - </p> |
674 | - |
675 | - <div class="lesser" id="inactive-top-navigation"> |
676 | - <tal:navigation |
677 | - content="structure view/inactive_memberships/@@+navigation-links-upper" /> |
678 | - </div> |
679 | - <table class="listing sortable" id="inactivemembers"> |
680 | - <thead> |
681 | - <tr> |
682 | - <th>Name</th> |
683 | - <th>Joined in</th> |
684 | - <th>Status</th> |
685 | - <th> </th> |
686 | - </tr> |
687 | - </thead> |
688 | - <tbody> |
689 | - <tr tal:repeat="membership inactive_batch"> |
690 | - <tal:block define="member membership/person"> |
691 | - <td> |
692 | - <a tal:attributes="href member/fmt:url" |
693 | - tal:content="member/displayname" /> |
694 | - </td> |
695 | - <td |
696 | - tal:content="membership/datejoined/fmt:date" |
697 | - >2005-04-01</td> |
698 | - |
699 | - <td tal:condition="membership/isExpired"> |
700 | - Expired on |
701 | - <span tal:replace="membership/dateexpires/fmt:date" /> |
702 | - </td> |
703 | - <td |
704 | - tal:condition="not: membership/isExpired" |
705 | - tal:content="membership/status/title" /> |
706 | - <td tal:condition="user_can_edit_memberships"> |
707 | - <a tal:attributes="href membership/fmt:url" |
708 | - ><img src="/@@/edit" |
709 | - title="Change membership duration or status" /></a> |
710 | - </td> |
711 | - </tal:block> |
712 | - </tr> |
713 | - </tbody> |
714 | - </table> |
715 | - <div class="lesser"> |
716 | - <tal:navigation |
717 | - content="structure view/inactive_memberships/@@+navigation-links-lower" /> |
718 | + <div tal:condition="user_can_edit_memberships"> |
719 | + <a name="former"></a> |
720 | + <h2>Former members</h2> |
721 | + |
722 | + <p> |
723 | + These are the members whose subscriptions have expired, or were |
724 | + deactivated by themselves or by a team administrator. |
725 | + </p> |
726 | + |
727 | + <div class="lesser" id="inactive-top-navigation"> |
728 | + <tal:navigation |
729 | + content="structure view/inactive_memberships/@@+navigation-links-upper" /> |
730 | + </div> |
731 | + <table class="listing sortable" id="inactivemembers"> |
732 | + <thead> |
733 | + <tr> |
734 | + <th>Name</th> |
735 | + <th>Joined in</th> |
736 | + <th>Status</th> |
737 | + <th> </th> |
738 | + </tr> |
739 | + </thead> |
740 | + <tbody> |
741 | + <tr tal:repeat="membership inactive_batch"> |
742 | + <tal:block define="member membership/person"> |
743 | + <td> |
744 | + <a tal:attributes="href member/fmt:url" |
745 | + tal:content="member/displayname" /> |
746 | + </td> |
747 | + <td |
748 | + tal:content="membership/datejoined/fmt:date" |
749 | + >2005-04-01</td> |
750 | + |
751 | + <td tal:condition="membership/isExpired"> |
752 | + Expired on |
753 | + <span tal:replace="membership/dateexpires/fmt:date" /> |
754 | + </td> |
755 | + <td |
756 | + tal:condition="not: membership/isExpired" |
757 | + tal:content="membership/status/title" /> |
758 | + <td tal:condition="user_can_edit_memberships"> |
759 | + <a tal:attributes="href membership/fmt:url" |
760 | + ><img src="/@@/edit" |
761 | + title="Change membership duration or status" /></a> |
762 | + </td> |
763 | + </tal:block> |
764 | + </tr> |
765 | + </tbody> |
766 | + </table> |
767 | + <div class="lesser"> |
768 | + <tal:navigation |
769 | + content="structure view/inactive_memberships/@@+navigation-links-lower" /> |
770 | + </div> |
771 | </div> |
772 | </li> |
773 | </ul> |
774 | |
775 | === modified file 'lib/lp/registry/templates/team-portlet-membership.pt' |
776 | --- lib/lp/registry/templates/team-portlet-membership.pt 2010-01-04 18:54:20 +0000 |
777 | +++ lib/lp/registry/templates/team-portlet-membership.pt 2010-01-04 18:54:21 +0000 |
778 | @@ -15,24 +15,24 @@ |
779 | |
780 | <div id="membership-summary" class="portletBody portletContent" |
781 | style="margin-bottom: 1.5em;"> |
782 | - <div id="membership-summary"> |
783 | - <div> |
784 | + <div> |
785 | + <div id="membership-counts"> |
786 | <img src="/@@/team" alt="team" /> |
787 | - <strong id="member-count"> |
788 | + <strong id="approved-member-count"> |
789 | <tal:active content="context/all_member_count" /> |
790 | </strong> |
791 | <a tal:attributes="href string:${context/fmt:url/+members}#active" |
792 | >active members</a><tal:invited |
793 | define="invited_member_count context/invited_member_count" |
794 | condition="invited_member_count">, |
795 | - <strong> |
796 | + <strong id="invited-member-count"> |
797 | <tal:invited_count content="context/invited_member_count" /> |
798 | </strong> |
799 | <a tal:attributes="href string:${context/fmt:url/+members}#invited" |
800 | >invited members</a></tal:invited><tal:proposed |
801 | define="proposed_member_count context/proposed_member_count" |
802 | condition="proposed_member_count">, |
803 | - <strong> |
804 | + <strong id="proposed-member-count"> |
805 | <tal:proposed_count content="proposed_member_count" /> |
806 | </strong> |
807 | <a tal:attributes="href string:${context/fmt:url/+members}#proposed" |
808 | @@ -95,37 +95,40 @@ |
809 | <table style="margin: 0px 0px .5em 0px;"> |
810 | <tr> |
811 | <td style="padding: 0px 1em 1em 0px;" |
812 | - tal:define="link context/menu:overview/add_member"> |
813 | - <span id="add-member-spinner" class="update-in-progress-message" |
814 | - style="display: none"> |
815 | + tal:define="link context/menu:overview/add_member" |
816 | + tal:condition="link/enabled"> |
817 | + <span id="add-member-spinner" |
818 | + class="unseen update-in-progress-message"> |
819 | Saving... |
820 | </span> |
821 | <tal:add-member replace="structure link/fmt:link-icon" /> |
822 | </td> |
823 | - <td style="padding: 0px 0px 1em 0px;" |
824 | + <td colspan="2" |
825 | + style="padding: 0px 0px 1em 0px;" |
826 | tal:define="link context/menu:overview/mugshots"> |
827 | <tal:mugshots replace="structure link/fmt:link-icon" /> |
828 | </td> |
829 | </tr> |
830 | + </table> |
831 | + <table> |
832 | <tr> |
833 | - <td style="padding: 3px 3em 0px 0px;"> |
834 | - <div id="recently-approved" |
835 | - tal:attributes="style view/recently_approved_hidden"> |
836 | - <h3 style="color:black; font-weight:bold; margin: 0px"> |
837 | - Latest members |
838 | - </h3> |
839 | - <ul id="recently-approved-ul"> |
840 | - <li tal:repeat="person view/recently_approved_members" |
841 | - tal:content="structure person/fmt:link" /> |
842 | - </ul> |
843 | - </div> |
844 | + <td id="recently-approved" |
845 | + style="padding-right: 1em" |
846 | + tal:attributes="class view/recently_approved_hidden"> |
847 | + <h3 style="color:black; font-weight:bold; margin: 0px"> |
848 | + Latest members |
849 | + </h3> |
850 | + <ul id="recently-approved-ul"> |
851 | + <li tal:repeat="person view/recently_approved_members" |
852 | + tal:content="structure person/fmt:link" /> |
853 | + </ul> |
854 | </td> |
855 | - <td style="padding: 0px;" id="recently-applied" |
856 | - tal:condition="view/recently_proposed_members"> |
857 | + <td id="recently-proposed" |
858 | + tal:attributes="class view/recently_proposed_hidden"> |
859 | <h3 style="color:black; font-weight:bold; margin: 0px"> |
860 | Pending approval |
861 | </h3> |
862 | - <ul> |
863 | + <ul id="recently-proposed-ul"> |
864 | <li tal:repeat="person view/recently_proposed_members" |
865 | tal:content="structure person/fmt:link" /> |
866 | </ul> |
867 | @@ -136,6 +139,16 @@ |
868 | </td> |
869 | </tr> |
870 | </table> |
871 | + <div id="recently-invited" |
872 | + tal:attributes="class view/recently_invited_hidden"> |
873 | + <h3 style="color:black; font-weight:bold; margin: 0px"> |
874 | + Latest invited |
875 | + </h3> |
876 | + <ul id="recently-invited-ul"> |
877 | + <li tal:repeat="person view/recently_invited_members" |
878 | + tal:content="structure person/fmt:link" /> |
879 | + </ul> |
880 | + </div> |
881 | </tal:can-view> |
882 | </div> |
883 | </tal:root> |
884 | |
885 | === added file 'lib/lp/registry/windmill/tests/test_team_index.py' |
886 | --- lib/lp/registry/windmill/tests/test_team_index.py 1970-01-01 00:00:00 +0000 |
887 | +++ lib/lp/registry/windmill/tests/test_team_index.py 2010-01-04 18:54:21 +0000 |
888 | @@ -0,0 +1,74 @@ |
889 | +# Copyright 2009 Canonical Ltd. This software is licensed under the |
890 | +# GNU Affero General Public License version 3 (see the file LICENSE). |
891 | + |
892 | +"""Test team index page.""" |
893 | + |
894 | +__metaclass__ = type |
895 | +__all__ = [] |
896 | + |
897 | +import unittest |
898 | + |
899 | +from windmill.authoring import WindmillTestClient |
900 | + |
901 | +from canonical.launchpad.windmill.testing import lpuser |
902 | +from canonical.launchpad.windmill.testing.widgets import ( |
903 | + search_and_select_picker_widget) |
904 | + |
905 | +from lp.registry.windmill.testing import RegistryWindmillLayer |
906 | +from lp.testing import TestCaseWithFactory |
907 | + |
908 | + |
909 | +class TestTeamIndex(TestCaseWithFactory): |
910 | + """Test team index page.""" |
911 | + |
912 | + layer = RegistryWindmillLayer |
913 | + |
914 | + def setUp(self): |
915 | + self.client = WindmillTestClient(__name__) |
916 | + |
917 | + def test_addmember(self): |
918 | + self.client.open( |
919 | + url=u'http://launchpad.dev:8085/~testing-spanish-team') |
920 | + |
921 | + lpuser.TRANSLATIONS_ADMIN.ensure_login(self.client) |
922 | + |
923 | + addmember_xpath = ( |
924 | + '//*[@id="membership"]' + |
925 | + '//a[text()="Add member" ' + |
926 | + 'and contains(@class, "js-action")]') |
927 | + # Add rosetta-admins team as a member. |
928 | + approved_count_xpath = '//*[@id="approved-member-count"]' |
929 | + code = "lookupNode({xpath: '%s'}).textContent" % approved_count_xpath |
930 | + result = self.client.commands.execJS(code=code) |
931 | + old_approved_count = int(result['result']) |
932 | + |
933 | + self.client.waits.forElement(xpath=addmember_xpath) |
934 | + self.client.click(xpath=addmember_xpath) |
935 | + # Xpath is 1-indexed. |
936 | + search_and_select_picker_widget(self.client, 'rosetta', 1) |
937 | + |
938 | + self.client.waits.forElement( |
939 | + xpath='//ul[@id="recently-approved-ul"]/li[1]/a', |
940 | + option='href|~rosetta-admins') |
941 | + |
942 | + self.client.asserts.assertText( |
943 | + xpath=approved_count_xpath, |
944 | + validator=str(old_approved_count+1)) |
945 | + |
946 | + # Add another team as a member. |
947 | + invited_count_xpath = '//*[@id="invited-member-count"]' |
948 | + self.client.asserts.assertNotNode(xpath=invited_count_xpath) |
949 | + self.client.click(xpath=addmember_xpath) |
950 | + search_and_select_picker_widget(self.client, 'simple', 1) |
951 | + |
952 | + self.client.waits.forElement( |
953 | + xpath='//ul[@id="recently-invited-ul"]/li[1]/a', |
954 | + option='href|~simple-team') |
955 | + |
956 | + self.client.asserts.assertText( |
957 | + xpath=invited_count_xpath, |
958 | + validator="1") |
959 | + |
960 | + |
961 | +def test_suite(): |
962 | + return unittest.TestLoader().loadTestsFromName(__name__) |
Summary
-------
This branch is dependent on
lp:~edwin-grubbs/launchpad/bug-482176-add-team-member-ajax-part1
I made the following changes:
* Added a windmill test
* Changed the return value from IPerson.addMember() so
the UI knows what status it decided to use.
* If the user is allowed to invite a member because they can admin
the owning team and if the user is allowed to accept the invitation
because they can admin the member team, addMember() now just fully
approves the membership.
Over 50 tests broke because I changed the return value for
addMember() and setStatus(). I only fixed one of them as a sample.
The rest will have to wait for a followup branch, since I'm interested
in getting feedback on the changes I made already.
Implementation details ------- ------- -
-------
lib/canonical/ launchpad/ icing/style- 3-0.css launchpad/ icing/style. css
lib/canonical/
Moved the unseen style to style-3-0.css since style.css
will be going away eventually.
lib/canonical/ launchpad/ javascript/ code/codereview .js
Added bug number to XXX.
lib/canonical/ launchpad/ javascript/ registry/ team.js
Added handling of teams as new members.
lib/lp/ registry/ browser/ person. py
Added code for a list of recently invited members in
the membership portlet.
Using a css class to hide the lists instead of setting
the element's style attribute.
Modified the view for +add-my-teams so that it matches
the behavior of the ajax "Add member" link.
lib/lp/ registry/ browser/ tests/mailingli st-views. txt
Fixed test for new return values.
lib/lp/ registry/ interfaces/ person. py
Documented new return value and organized docs by param.
lib/lp/ registry/ model/person. py
Changed return value.
lib/lp/ registry/ templates/ team-index. pt
Fixed mixing space between left and right portlets.
lib/lp/ registry/ templates/ team-members. pt
Hide former members list from non-admins, since some users
complain when it appears that they used to be a member of a team
that they were added to on accident.
lib/lp/ registry/ templates/ team-portlet- membership. pt
Added recently invited member list.
lib/lp/ registry/ windmill/ tests/test_ team_index. py
Added windmill test.
Tests
-----
./bin/test -vv -t mailinglist- views.txt RegistryWindmil lTest -t test_team_index
./bin/test -vv --layer=
Demo and Q/A
------------
* Open http:// launchpad. dev/~testing- spanish- team
* Log in as <email address hidden>.
* Click on the green "Add member" link in the portlet.
* Search for "rosetta" and click on the rosetta admins team.
* That team should be added to the "Recently approved" list
since carlos admins both of them.
* Click on the green "Add member" link in the portlet.
* Search for "simple" and click on the Simple Team.
* That team should be added to the "Recently invited" list
since carlos does not admin the Simple Team.
* Log out.
* The "Add member" link should be gone.