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 | 215 | font-weight: bold; | 215 | font-weight: bold; |
6 | 216 | } | 216 | } |
7 | 217 | 217 | ||
8 | 218 | .unseen {display: none;} | ||
9 | 219 | |||
10 | 220 | |||
11 | 218 | /* Common presentations */ | 221 | /* Common presentations */ |
12 | 219 | div.see-all { | 222 | div.see-all { |
13 | 220 | font-size: 123.1%; /* Same as a h2 */ | 223 | font-size: 123.1%; /* Same as a h2 */ |
14 | 221 | 224 | ||
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 | 428 | fieldset .expanded {display: block;} | 428 | fieldset .expanded {display: block;} |
20 | 429 | fieldset.collapsible legend {font-weight: normal;} | 429 | fieldset.collapsible legend {font-weight: normal;} |
21 | 430 | 430 | ||
22 | 431 | .unseen {display: none;} | ||
23 | 432 | |||
24 | 433 | form.primary.search { | 431 | form.primary.search { |
25 | 434 | margin-bottom: 2em; | 432 | margin-bottom: 2em; |
26 | 435 | } | 433 | } |
27 | 436 | 434 | ||
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 | 22 | var link = Y.one('#request-review'); | 22 | var link = Y.one('#request-review'); |
33 | 23 | if (link !== null) { | 23 | if (link !== null) { |
34 | 24 | link.addClass('js-action'); | 24 | link.addClass('js-action'); |
41 | 25 | /* XXX: salgado, 2009-11-11: This will cause the picker to be | 25 | /* XXX: salgado 2009-11-11 bug=497603 |
42 | 26 | * recreated every time the user clicks on the link. Although that | 26 | * This will cause the picker to be recreated every time the |
43 | 27 | * makes it unnecessary to have the widget cleared, it makes it | 27 | * user clicks on the link. Although that makes it unnecessary |
44 | 28 | * impossible to persist the state of the picker between clicks on the | 28 | * to have the widget cleared, it makes it impossible to persist |
45 | 29 | * link. We should probably have a policy to enforce that we | 29 | * the state of the picker between clicks on the link. We |
46 | 30 | * just hide/show widgets when a link is clicked more than once, | 30 | * should probably have a policy to enforce that we just |
47 | 31 | * hide/show widgets when a link is clicked more than once, | ||
48 | 31 | * instead of recreating the widgets every time. | 32 | * instead of recreating the widgets every time. |
49 | 32 | */ | 33 | */ |
50 | 33 | link.on('click', show_request_review_form); | 34 | link.on('click', show_request_review_form); |
51 | 34 | 35 | ||
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 | 69 | * @param flash_node {Node} The node to red flash. | 69 | * @param flash_node {Node} The node to red flash. |
57 | 70 | * @param msg {String} The message to display. | 70 | * @param msg {String} The message to display. |
58 | 71 | */ | 71 | */ |
60 | 72 | var display_error = function(flash_node, msg) { | 72 | Y.lp.display_error = function(flash_node, msg) { |
61 | 73 | create_error_overlay(); | 73 | create_error_overlay(); |
62 | 74 | maybe_red_flash(flash_node, function(){ | 74 | maybe_red_flash(flash_node, function(){ |
63 | 75 | error_overlay.showError(msg); | 75 | error_overlay.showError(msg); |
64 | @@ -77,5 +77,35 @@ | |||
65 | 77 | }); | 77 | }); |
66 | 78 | }; | 78 | }; |
67 | 79 | 79 | ||
70 | 80 | Y.lp.display_error = display_error; | 80 | |
71 | 81 | }, "0.1", {"requires":["lazr.formoverlay"]}); | 81 | var info_overlay; |
72 | 82 | /* | ||
73 | 83 | * Display the form overlay for non-error informational messages. | ||
74 | 84 | * | ||
75 | 85 | * @method display_info | ||
76 | 86 | * @param msg {String} The message to display. | ||
77 | 87 | */ | ||
78 | 88 | Y.lp.display_info = function(msg) { | ||
79 | 89 | if (info_overlay === undefined) { | ||
80 | 90 | info_overlay = new Y.lazr.PrettyOverlay({ | ||
81 | 91 | centered: true, | ||
82 | 92 | visible: false | ||
83 | 93 | }); | ||
84 | 94 | info_overlay.render(); | ||
85 | 95 | } | ||
86 | 96 | var content = Y.Node.create( | ||
87 | 97 | '<div style="background: url(/@@/info-large) no-repeat; ' + | ||
88 | 98 | 'min-height: 32px; padding-left: 40px; padding-top: 16px"/></div>'); | ||
89 | 99 | content.appendChild(Y.Node.create(msg)); | ||
90 | 100 | var button_div = Y.Node.create('<div style="text-align: right"></div>'); | ||
91 | 101 | var ok_button = Y.Node.create('<button>OK</button>'); | ||
92 | 102 | ok_button.on('click', function(e) { | ||
93 | 103 | info_overlay.fire('cancel'); | ||
94 | 104 | }); | ||
95 | 105 | button_div.appendChild(ok_button); | ||
96 | 106 | content.appendChild(button_div); | ||
97 | 107 | info_overlay.set('bodyContent', content); | ||
98 | 108 | info_overlay.show(); | ||
99 | 109 | }; | ||
100 | 110 | |||
101 | 111 | }, "0.1", {"requires":["lazr.formoverlay", "lazr.overlay"]}); | ||
102 | 82 | 112 | ||
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 | 15 | * @method setup_add_member_handler | 15 | * @method setup_add_member_handler |
108 | 16 | */ | 16 | */ |
109 | 17 | module.setup_add_member_handler = function() { | 17 | module.setup_add_member_handler = function() { |
110 | 18 | if (Y.UA.ie) { | ||
111 | 19 | return; | ||
112 | 20 | } | ||
113 | 21 | |||
114 | 18 | var config = { | 22 | var config = { |
115 | 19 | header: 'Add a member', | 23 | header: 'Add a member', |
116 | 20 | step_title: 'Search', | 24 | step_title: 'Search', |
117 | 21 | picker_activator: '.menu-link-add_member' | 25 | picker_activator: '.menu-link-add_member' |
118 | 22 | }; | 26 | }; |
119 | 23 | 27 | ||
124 | 24 | var picker = Y.lp.picker.create( | 28 | Y.lp.picker.create('ValidTeamMember', _add_member, config); |
121 | 25 | 'ValidTeamMember', | ||
122 | 26 | function(result) { _add_member(result); }, | ||
123 | 27 | config); | ||
125 | 28 | }; | 29 | }; |
126 | 29 | 30 | ||
136 | 30 | var _add_member = function(result) { | 31 | var _add_member = function(selected_person) { |
137 | 31 | var spinner = Y.one('#add-member-spinner'); | 32 | var box = Y.one('#membership'); |
138 | 32 | var addmember_link = Y.one('.menu-link-add_member'); | 33 | var spinner = box.query('#add-member-spinner'); |
139 | 33 | addmember_link.setStyle('display', 'none'); | 34 | var addmember_link = box.query('.menu-link-add_member'); |
140 | 34 | spinner.setStyle('display', 'inline'); | 35 | addmember_link.addClass('unseen'); |
141 | 35 | function disable_spinner() { | 36 | spinner.removeClass('unseen'); |
142 | 36 | addmember_link.setStyle('display', 'inline'); | 37 | var disable_spinner = function() { |
143 | 37 | spinner.setStyle('display', 'none'); | 38 | addmember_link.removeClass('unseen'); |
144 | 38 | } | 39 | spinner.addClass('unseen'); |
145 | 40 | }; | ||
146 | 39 | lp_client = new LP.client.Launchpad(); | 41 | lp_client = new LP.client.Launchpad(); |
147 | 40 | 42 | ||
148 | 41 | var error_handler = new LP.client.ErrorHandler(); | 43 | var error_handler = new LP.client.ErrorHandler(); |
149 | 42 | error_handler.clearProgressUI = disable_spinner; | 44 | error_handler.clearProgressUI = disable_spinner; |
150 | 43 | error_handler.showError = function(error_msg) { | 45 | error_handler.showError = function(error_msg) { |
152 | 44 | Y.lp.display_error(Y.one('.menu-link-add_member'), error_msg); | 46 | Y.lp.display_error(addmember_link, error_msg); |
153 | 45 | }; | 47 | }; |
154 | 46 | 48 | ||
156 | 47 | config = { | 49 | addmember_config = { |
157 | 48 | on: { | 50 | on: { |
171 | 49 | success: function(member_added) { | 51 | success: function(change_and_status) { |
172 | 50 | if (!member_added) { | 52 | var did_status_change = change_and_status[0]; |
173 | 51 | disable_spinner(); | 53 | var current_status = change_and_status[1]; |
174 | 52 | alert('Already a member.'); | 54 | var members_section, members_ul, count_elem; |
175 | 53 | return; | 55 | if (did_status_change === false) { |
176 | 54 | } | 56 | disable_spinner(); |
177 | 55 | if (result.css.match("team")) { | 57 | Y.lp.display_info( |
178 | 56 | disable_spinner(); | 58 | selected_person.title + ' is already ' + |
179 | 57 | alert('This is a team'); | 59 | current_status.toLowerCase() + |
180 | 58 | return; | 60 | ' as a member of the team.'); |
181 | 59 | } | 61 | return; |
182 | 60 | var members_section = Y.one('#recently-approved'); | 62 | } |
183 | 61 | var members_ul = Y.one('#recently-approved-ul'); | 63 | |
184 | 64 | if (current_status == 'Invited') { | ||
185 | 65 | members_section = box.query('#recently-invited'); | ||
186 | 66 | members_ul = box.query('#recently-invited-ul'); | ||
187 | 67 | count_elem = box.query('#invited-member-count'); | ||
188 | 68 | } else if (current_status == 'Proposed') { | ||
189 | 69 | members_section = box.query('#recently-proposed'); | ||
190 | 70 | members_ul = box.query('#recently-proposed-ul'); | ||
191 | 71 | count_elem = box.query('#proposed-member-count'); | ||
192 | 72 | } else if (current_status == 'Approved') { | ||
193 | 73 | members_section = box.query('#recently-approved'); | ||
194 | 74 | members_ul = box.query('#recently-approved-ul'); | ||
195 | 75 | count_elem = box.query('#approved-member-count'); | ||
196 | 76 | } else { | ||
197 | 77 | Y.lp.display_error( | ||
198 | 78 | addmember_link, | ||
199 | 79 | 'Unexpected status: ' + current_status); | ||
200 | 80 | return; | ||
201 | 81 | } | ||
202 | 62 | var first_node = members_ul.get('firstChild'); | 82 | var first_node = members_ul.get('firstChild'); |
204 | 63 | config = { | 83 | |
205 | 84 | var xhtml_person_handler = function(person_html) { | ||
206 | 85 | if (count_elem === null && current_status == 'Invited') { | ||
207 | 86 | count_elem = Y.Node.create( | ||
208 | 87 | '<strong id="invited-member-count">' + | ||
209 | 88 | '1</strong>'); | ||
210 | 89 | var count_box = Y.one( | ||
211 | 90 | '#membership #membership-counts'); | ||
212 | 91 | count_box.append(Y.Node.create( | ||
213 | 92 | '<span>, </span>')); | ||
214 | 93 | count_box.append(count_elem); | ||
215 | 94 | count_box.append(Y.Node.create( | ||
216 | 95 | '<span> <a href="/+members#invited">' + | ||
217 | 96 | 'invited members</a></span>')); | ||
218 | 97 | } else { | ||
219 | 98 | var count = count_elem.get('innerHTML'); | ||
220 | 99 | count = parseInt(count, 10) + 1; | ||
221 | 100 | count_elem.set('innerHTML', count); | ||
222 | 101 | } | ||
223 | 102 | person_repr = Y.Node.create( | ||
224 | 103 | '<li>' + person_html + '</li>'); | ||
225 | 104 | members_section.removeClass('unseen'); | ||
226 | 105 | members_ul.insertBefore(person_repr, first_node); | ||
227 | 106 | anim = Y.lazr.anim.green_flash({node: person_repr}); | ||
228 | 107 | anim.run(); | ||
229 | 108 | disable_spinner(); | ||
230 | 109 | }; | ||
231 | 110 | |||
232 | 111 | xhtml_person_config = { | ||
233 | 64 | on: { | 112 | on: { |
250 | 65 | success: function(person_html) { | 113 | success: xhtml_person_handler, |
235 | 66 | var total_members = Y.one( | ||
236 | 67 | '#member-count').get('innerHTML'); | ||
237 | 68 | total_members = parseInt(total_members, 10) + 1; | ||
238 | 69 | Y.one('#member-count').set( | ||
239 | 70 | 'innerHTML', total_members); | ||
240 | 71 | person_repr = Y.Node.create( | ||
241 | 72 | '<li>' + person_html + '</li>'); | ||
242 | 73 | members_section.setStyle('visibility', 'visible'); | ||
243 | 74 | members_ul.insertBefore( | ||
244 | 75 | person_repr, first_node); | ||
245 | 76 | anim = Y.lazr.anim.green_flash( | ||
246 | 77 | {node: person_repr}); | ||
247 | 78 | anim.run(); | ||
248 | 79 | disable_spinner(); | ||
249 | 80 | }, | ||
251 | 81 | failure: error_handler.getFailureHandler() | 114 | failure: error_handler.getFailureHandler() |
252 | 82 | }, | 115 | }, |
253 | 83 | accept: LP.client.XHTML | 116 | accept: LP.client.XHTML |
254 | 84 | }; | 117 | }; |
256 | 85 | lp_client.get(result.api_uri, config); | 118 | lp_client.get(selected_person.api_uri, xhtml_person_config); |
257 | 86 | }, | 119 | }, |
258 | 87 | failure: error_handler.getFailureHandler() | 120 | failure: error_handler.getFailureHandler() |
259 | 88 | }, | 121 | }, |
260 | 89 | parameters: { | 122 | parameters: { |
262 | 90 | // XXX: Why do I always have to get absolute URIs out of the URIs | 123 | // XXX: EdwinGrubbs 2009-12-16 bug=497602 |
263 | 124 | // Why do I always have to get absolute URIs out of the URIs | ||
264 | 91 | // in the picker's result/client.links? | 125 | // in the picker's result/client.links? |
265 | 92 | reviewer: LP.client.get_absolute_uri(LP.client.links.me), | 126 | reviewer: LP.client.get_absolute_uri(LP.client.links.me), |
267 | 93 | person: LP.client.get_absolute_uri(result.api_uri) | 127 | person: LP.client.get_absolute_uri(selected_person.api_uri) |
268 | 94 | } | 128 | } |
269 | 95 | }; | 129 | }; |
270 | 96 | 130 | ||
271 | 97 | lp_client.named_post( | 131 | lp_client.named_post( |
273 | 98 | LP.client.cache.context.self_link, 'addMember', config); | 132 | LP.client.cache.context.self_link, 'addMember', addmember_config); |
274 | 99 | }; | 133 | }; |
275 | 100 | 134 | ||
276 | 101 | }, '0.1', {requires: [ | 135 | }, '0.1', {requires: [ |
277 | 102 | 136 | ||
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 | 95 | from itertools import chain | 95 | from itertools import chain |
283 | 96 | from operator import attrgetter, itemgetter | 96 | from operator import attrgetter, itemgetter |
284 | 97 | from textwrap import dedent | 97 | from textwrap import dedent |
285 | 98 | from storm.zope.interfaces import IResultSet | ||
286 | 98 | 99 | ||
287 | 99 | from zope.error.interfaces import IErrorReportingUtility | 100 | from zope.error.interfaces import IErrorReportingUtility |
288 | 100 | from zope.app.form.browser import TextAreaWidget, TextWidget | 101 | from zope.app.form.browser import TextAreaWidget, TextWidget |
289 | @@ -227,13 +228,8 @@ | |||
290 | 227 | logoutPerson, allowUnauthenticatedSession) | 228 | logoutPerson, allowUnauthenticatedSession) |
291 | 228 | from canonical.launchpad.webapp.menu import get_current_view | 229 | from canonical.launchpad.webapp.menu import get_current_view |
292 | 229 | from canonical.launchpad.webapp.publisher import LaunchpadView | 230 | from canonical.launchpad.webapp.publisher import LaunchpadView |
300 | 230 | <<<<<<< TREE | 231 | from canonical.launchpad.webapp.tales import ( |
301 | 231 | from canonical.launchpad.webapp.tales import ( | 232 | DateTimeFormatterAPI, FormattersAPI, PersonFormatterAPI) |
295 | 232 | DateTimeFormatterAPI, FormattersAPI) | ||
296 | 233 | ======= | ||
297 | 234 | from canonical.launchpad.webapp.tales import ( | ||
298 | 235 | DateTimeFormatterAPI, PersonFormatterAPI) | ||
299 | 236 | >>>>>>> MERGE-SOURCE | ||
302 | 237 | from lazr.uri import URI, InvalidURIError | 233 | from lazr.uri import URI, InvalidURIError |
303 | 238 | 234 | ||
304 | 239 | from canonical.launchpad import _ | 235 | from canonical.launchpad import _ |
305 | @@ -2489,11 +2485,7 @@ | |||
306 | 2489 | 2485 | ||
307 | 2490 | @property | 2486 | @property |
308 | 2491 | def next_url(self): | 2487 | def next_url(self): |
309 | 2492 | <<<<<<< TREE | ||
310 | 2493 | """Redirect to the +languages page if request originated there.""" | 2488 | """Redirect to the +languages page if request originated there.""" |
311 | 2494 | ======= | ||
312 | 2495 | """Redirect back to the originating page.""" | ||
313 | 2496 | >>>>>>> MERGE-SOURCE | ||
314 | 2497 | redirection_url = self.request.get('redirection_url') | 2489 | redirection_url = self.request.get('redirection_url') |
315 | 2498 | if redirection_url: | 2490 | if redirection_url: |
316 | 2499 | return redirection_url | 2491 | return redirection_url |
317 | @@ -2501,11 +2493,7 @@ | |||
318 | 2501 | 2493 | ||
319 | 2502 | @property | 2494 | @property |
320 | 2503 | def cancel_url(self): | 2495 | def cancel_url(self): |
321 | 2504 | <<<<<<< TREE | ||
322 | 2505 | """Redirect to the +languages page if request originated there.""" | 2496 | """Redirect to the +languages page if request originated there.""" |
323 | 2506 | ======= | ||
324 | 2507 | """Redirect back to the originating page.""" | ||
325 | 2508 | >>>>>>> MERGE-SOURCE | ||
326 | 2509 | redirection_url = self.getRedirectionURL() | 2497 | redirection_url = self.getRedirectionURL() |
327 | 2510 | if redirection_url: | 2498 | if redirection_url: |
328 | 2511 | return redirection_url | 2499 | return redirection_url |
329 | @@ -2745,6 +2733,13 @@ | |||
330 | 2745 | orderBy='-TeamMembership.date_proposed') | 2733 | orderBy='-TeamMembership.date_proposed') |
331 | 2746 | return members[:5] | 2734 | return members[:5] |
332 | 2747 | 2735 | ||
333 | 2736 | @cachedproperty | ||
334 | 2737 | def recently_invited_members(self): | ||
335 | 2738 | members = self.context.getMembersByStatus( | ||
336 | 2739 | TeamMembershipStatus.INVITED, | ||
337 | 2740 | orderBy='-TeamMembership.date_proposed') | ||
338 | 2741 | return members[:5] | ||
339 | 2742 | |||
340 | 2748 | @property | 2743 | @property |
341 | 2749 | def recently_approved_hidden(self): | 2744 | def recently_approved_hidden(self): |
342 | 2750 | """Optionally hide the div. | 2745 | """Optionally hide the div. |
343 | @@ -2752,8 +2747,8 @@ | |||
344 | 2752 | The AJAX on the page needs the elements to be present | 2747 | The AJAX on the page needs the elements to be present |
345 | 2753 | but hidden in case it adds a member to the list. | 2748 | but hidden in case it adds a member to the list. |
346 | 2754 | """ | 2749 | """ |
349 | 2755 | if self.recently_approved_members.count() == 0: | 2750 | if IResultSet(self.recently_approved_members).is_empty(): |
350 | 2756 | return 'visibility: collapse' | 2751 | return 'unseen' |
351 | 2757 | else: | 2752 | else: |
352 | 2758 | return '' | 2753 | return '' |
353 | 2759 | 2754 | ||
354 | @@ -2764,8 +2759,20 @@ | |||
355 | 2764 | The AJAX on the page needs the elements to be present | 2759 | The AJAX on the page needs the elements to be present |
356 | 2765 | but hidden in case it adds a member to the list. | 2760 | but hidden in case it adds a member to the list. |
357 | 2766 | """ | 2761 | """ |
360 | 2767 | if self.recently_proposed_members.count() == 0: | 2762 | if IResultSet(self.recently_proposed_members).is_empty(): |
361 | 2768 | return 'visibility: collapse' | 2763 | return 'unseen' |
362 | 2764 | else: | ||
363 | 2765 | return '' | ||
364 | 2766 | |||
365 | 2767 | @property | ||
366 | 2768 | def recently_invited_hidden(self): | ||
367 | 2769 | """Optionally hide the div. | ||
368 | 2770 | |||
369 | 2771 | The AJAX on the page needs the elements to be present | ||
370 | 2772 | but hidden in case it adds a member to the list. | ||
371 | 2773 | """ | ||
372 | 2774 | if IResultSet(self.recently_invited_members).is_empty(): | ||
373 | 2775 | return 'unseen' | ||
374 | 2769 | else: | 2776 | else: |
375 | 2770 | return '' | 2777 | return '' |
376 | 2771 | 2778 | ||
377 | @@ -4241,9 +4248,14 @@ | |||
378 | 4241 | def continue_action(self, action, data): | 4248 | def continue_action(self, action, data): |
379 | 4242 | """Make the selected teams join this team.""" | 4249 | """Make the selected teams join this team.""" |
380 | 4243 | context = self.context | 4250 | context = self.context |
381 | 4251 | is_admin = check_permission('launchpad.Admin', context) | ||
382 | 4244 | for team in data['teams']: | 4252 | for team in data['teams']: |
385 | 4245 | team.join(context, requester=self.user) | 4253 | if is_admin: |
386 | 4246 | if context.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED: | 4254 | context.addMember(team, reviewer=self.user) |
387 | 4255 | else: | ||
388 | 4256 | team.join(context, requester=self.user) | ||
389 | 4257 | if (context.subscriptionpolicy == TeamSubscriptionPolicy.MODERATED | ||
390 | 4258 | and not is_admin): | ||
391 | 4247 | msg = 'proposed to this team.' | 4259 | msg = 'proposed to this team.' |
392 | 4248 | else: | 4260 | else: |
393 | 4249 | msg = 'added to this team.' | 4261 | msg = 'added to this team.' |
394 | 4250 | 4262 | ||
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 | 33 | No Privileges Person is added as a member of the Aardvarks. | 33 | No Privileges Person is added as a member of the Aardvarks. |
400 | 34 | 34 | ||
401 | 35 | >>> no_priv = person_set.getByEmail('no-priv@canonical.com') | 35 | >>> no_priv = person_set.getByEmail('no-priv@canonical.com') |
403 | 36 | >>> aardvarks.addMember(no_priv, sample_person) | 36 | >>> ignored = aardvarks.addMember(no_priv, sample_person) |
404 | 37 | 37 | ||
405 | 38 | But regular members can't access the +mailinglist page. | 38 | But regular members can't access the +mailinglist page. |
406 | 39 | 39 | ||
407 | @@ -52,8 +52,9 @@ | |||
408 | 52 | >>> team_membership = getUtility(ITeamMembershipSet).getByPersonAndTeam( | 52 | >>> team_membership = getUtility(ITeamMembershipSet).getByPersonAndTeam( |
409 | 53 | ... no_priv, aardvarks) | 53 | ... no_priv, aardvarks) |
410 | 54 | >>> team_membership.status | 54 | >>> team_membership.status |
413 | 55 | <DBItem TeamMembershipStatus.APPROVED, (2) Approved> | 55 | <DBItem TeamMembershipStatus.APPROVED... |
414 | 56 | >>> team_membership.setStatus(TeamMembershipStatus.ADMIN, sample_person) | 56 | >>> ignored = team_membership.setStatus( |
415 | 57 | ... TeamMembershipStatus.ADMIN, sample_person) | ||
416 | 57 | 58 | ||
417 | 58 | Now No Privileges Person has permission to request mailing lists. | 59 | Now No Privileges Person has permission to request mailing lists. |
418 | 59 | 60 | ||
419 | @@ -208,7 +209,7 @@ | |||
420 | 208 | 209 | ||
421 | 209 | A non-owner member can see information about the mailing list. | 210 | A non-owner member can see information about the mailing list. |
422 | 210 | 211 | ||
424 | 211 | >>> pmt.addMember(sample_person, owner) | 212 | >>> ignored = pmt.addMember(sample_person, owner) |
425 | 212 | >>> login('test@canonical.com') | 213 | >>> login('test@canonical.com') |
426 | 213 | >>> view = create_initialized_view(pmt, '+index') | 214 | >>> view = create_initialized_view(pmt, '+index') |
427 | 214 | >>> print view.archive_url | 215 | >>> print view.archive_url |
428 | 215 | 216 | ||
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 | 1406 | may_subscribe_to_list=True): | 1406 | may_subscribe_to_list=True): |
434 | 1407 | """Add the given person as a member of this team. | 1407 | """Add the given person as a member of this team. |
435 | 1408 | 1408 | ||
453 | 1409 | If the given person is already a member of this team we'll simply | 1409 | :param person: If the given person is already a member of this |
454 | 1410 | change its membership status. Otherwise a new TeamMembership is | 1410 | team we'll simply change its membership status. Otherwise a new |
455 | 1411 | created with the given status. | 1411 | TeamMembership is created with the given status. |
456 | 1412 | 1412 | ||
457 | 1413 | If the person is actually a team and force_team_add is False, the | 1413 | :param reviewer: The user who made the given person a member of this |
458 | 1414 | team will actually be invited to join this one. Otherwise the team | 1414 | team. |
459 | 1415 | is added as if it were a person. | 1415 | |
460 | 1416 | 1416 | :param comment: String that will be assigned to the | |
461 | 1417 | If the person is not a team, and may_subscribe_to_list | 1417 | proponent_comment, reviwer_comment, or acknowledger comment. |
462 | 1418 | is True, then the person may be subscribed to the team's | 1418 | |
463 | 1419 | mailing list, depending on the list status and the person's | 1419 | :param status: `TeamMembershipStatus` value must be either |
464 | 1420 | auto-subscribe settings. | 1420 | Approved, Proposed or Admin. |
465 | 1421 | 1421 | ||
466 | 1422 | The given status must be either Approved, Proposed or Admin. | 1422 | :param force_team_add: If the person is actually a team and |
467 | 1423 | 1423 | force_team_add is False, the team will actually be invited to | |
468 | 1424 | The reviewer is the user who made the given person a member of this | 1424 | join this one. Otherwise the team is added as if it were a |
469 | 1425 | team. | 1425 | person. |
470 | 1426 | |||
471 | 1427 | :param may_subscribe_to_list: If the person is not a team, and | ||
472 | 1428 | may_subscribe_to_list is True, then the person may be subscribed | ||
473 | 1429 | to the team's mailing list, depending on the list status and the | ||
474 | 1430 | person's auto-subscribe settings. | ||
475 | 1431 | |||
476 | 1432 | :return: A tuple containing a boolean indicating when the | ||
477 | 1433 | membership status changed and the current `TeamMembershipStatus`. | ||
478 | 1434 | This depends on the desired status passed as an argument, the | ||
479 | 1435 | subscription policy and the user's priveleges. | ||
480 | 1426 | """ | 1436 | """ |
481 | 1427 | 1437 | ||
482 | 1428 | def deactivateAllMembers(comment, reviewer): | 1438 | def deactivateAllMembers(comment, reviewer): |
483 | 1429 | 1439 | ||
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 | 1231 | # By default, teams can only be invited as members, meaning that | 1231 | # By default, teams can only be invited as members, meaning that |
489 | 1232 | # one of the team's admins will have to accept the invitation | 1232 | # one of the team's admins will have to accept the invitation |
490 | 1233 | # before the team is made a member. If force_team_add is True, | 1233 | # before the team is made a member. If force_team_add is True, |
493 | 1234 | # though, then we'll add a team as if it was a person. | 1234 | # or the user is also an admin of the proposed member, then |
494 | 1235 | if not force_team_add: | 1235 | # we'll add a team as if it was a person. |
495 | 1236 | is_reviewer_admin_of_new_member = ( | ||
496 | 1237 | person in reviewer.getAdministratedTeams()) | ||
497 | 1238 | if not force_team_add and not is_reviewer_admin_of_new_member: | ||
498 | 1236 | status = TeamMembershipStatus.INVITED | 1239 | status = TeamMembershipStatus.INVITED |
499 | 1237 | event = TeamInvitationEvent | 1240 | event = TeamInvitationEvent |
500 | 1238 | 1241 | ||
502 | 1239 | member_added = True | 1242 | status_changed = True |
503 | 1240 | expires = self.defaultexpirationdate | 1243 | expires = self.defaultexpirationdate |
504 | 1241 | tm = TeamMembership.selectOneBy(person=person, team=self) | 1244 | tm = TeamMembership.selectOneBy(person=person, team=self) |
505 | 1242 | if tm is None: | 1245 | if tm is None: |
506 | @@ -1251,12 +1254,12 @@ | |||
507 | 1251 | # We can't use tm.setExpirationDate() here because the reviewer | 1254 | # We can't use tm.setExpirationDate() here because the reviewer |
508 | 1252 | # here will be the member themselves when they join an OPEN team. | 1255 | # here will be the member themselves when they join an OPEN team. |
509 | 1253 | tm.dateexpires = expires | 1256 | tm.dateexpires = expires |
511 | 1254 | member_added = tm.setStatus(status, reviewer, comment) | 1257 | status_changed = tm.setStatus(status, reviewer, comment) |
512 | 1255 | 1258 | ||
513 | 1256 | if not person.is_team and may_subscribe_to_list: | 1259 | if not person.is_team and may_subscribe_to_list: |
514 | 1257 | person.autoSubscribeToMailingList(self.mailing_list, | 1260 | person.autoSubscribeToMailingList(self.mailing_list, |
515 | 1258 | requester=reviewer) | 1261 | requester=reviewer) |
517 | 1259 | return member_added | 1262 | return (status_changed, tm.status) |
518 | 1260 | 1263 | ||
519 | 1261 | # The three methods below are not in the IPerson interface because we want | 1264 | # The three methods below are not in the IPerson interface because we want |
520 | 1262 | # to protect them with a launchpad.Edit permission. We could do that by | 1265 | # to protect them with a launchpad.Edit permission. We could do that by |
521 | 1263 | 1266 | ||
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 | 318 | "%(team)s is a member of %(person)s." | 318 | "%(team)s is a member of %(person)s." |
527 | 319 | % dict(person=self.person.name, team=self.team.name)) | 319 | % dict(person=self.person.name, team=self.team.name)) |
528 | 320 | 320 | ||
529 | 321 | |||
530 | 322 | old_status = self.status | 321 | old_status = self.status |
531 | 323 | self.status = status | 322 | self.status = status |
532 | 324 | 323 | ||
533 | @@ -367,17 +366,10 @@ | |||
534 | 367 | # When a member proposes himself, a more detailed notification is | 366 | # When a member proposes himself, a more detailed notification is |
535 | 368 | # sent to the team admins by a subscriber of JoinTeamEvent; that's | 367 | # sent to the team admins by a subscriber of JoinTeamEvent; that's |
536 | 369 | # why we don't send anything here. | 368 | # why we don't send anything here. |
545 | 370 | <<<<<<< TREE | 369 | if ((self.person != self.last_changed_by or self.status != proposed) |
546 | 371 | if self.person == self.last_changed_by and self.status == proposed: | 370 | and not silent): |
539 | 372 | return | ||
540 | 373 | |||
541 | 374 | if not silent: | ||
542 | 375 | self._sendStatusChangeNotification(old_status) | ||
543 | 376 | ======= | ||
544 | 377 | if self.person != self.last_changed_by or self.status != proposed: | ||
547 | 378 | self._sendStatusChangeNotification(old_status) | 371 | self._sendStatusChangeNotification(old_status) |
548 | 379 | return True | 372 | return True |
549 | 380 | >>>>>>> MERGE-SOURCE | ||
550 | 381 | 373 | ||
551 | 382 | def _sendStatusChangeNotification(self, old_status): | 374 | def _sendStatusChangeNotification(self, old_status): |
552 | 383 | """Send a status change notification to all team admins and the | 375 | """Send a status change notification to all team admins and the |
553 | 384 | 376 | ||
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 | 13 | 13 | ||
559 | 14 | >>> print extract_text( | 14 | >>> print extract_text( |
560 | 15 | ... find_tag_by_id(browser.contents, 'recently-approved')) | 15 | ... find_tag_by_id(browser.contents, 'recently-approved')) |
562 | 16 | Recently approved | 16 | Latest members |
563 | 17 | Warty Gnome Team | 17 | Warty Gnome Team |
564 | 18 | Daniel Silverstone | 18 | Daniel Silverstone |
565 | 19 | Celso Providelo | 19 | Celso Providelo |
566 | 20 | Steve Alexander... | 20 | Steve Alexander... |
567 | 21 | 21 | ||
568 | 22 | >>> print extract_text( | 22 | >>> print extract_text( |
570 | 23 | ... find_tag_by_id(browser.contents, 'recently-applied')) | 23 | ... find_tag_by_id(browser.contents, 'recently-proposed')) |
571 | 24 | Pending approval | 24 | Pending approval |
572 | 25 | Sample Person | 25 | Sample Person |
573 | 26 | Andrew Bennetts | 26 | Andrew Bennetts |
574 | 27 | 27 | ||
575 | 28 | >>> print extract_text( | 28 | >>> print extract_text( |
576 | 29 | ... find_tag_by_id(browser.contents, 'recently-invited')) | ||
577 | 30 | Latest invited | ||
578 | 31 | Warty Security Team | ||
579 | 32 | |||
580 | 33 | >>> print extract_text( | ||
581 | 29 | ... find_tag_by_id(browser.contents, 'team-owner')) | 34 | ... find_tag_by_id(browser.contents, 'team-owner')) |
582 | 30 | Owner: | 35 | Owner: |
583 | 31 | Mark Shuttleworth | 36 | Mark Shuttleworth |
584 | @@ -87,15 +92,21 @@ | |||
585 | 87 | itself is not a member of any other team. | 92 | itself is not a member of any other team. |
586 | 88 | Show received invitations | 93 | Show received invitations |
587 | 89 | 94 | ||
590 | 90 | If the team does not have any recently approved or proposed members, | 95 | If the team does not have any recently approved, proposed, or invited |
591 | 91 | the recent members sections are not displayed: | 96 | members, the empty lists are hidden using the "unseen" css class: |
592 | 92 | 97 | ||
593 | 93 | >>> browser.open('http://launchpad.dev/~launchpad') | 98 | >>> browser.open('http://launchpad.dev/~launchpad') |
599 | 94 | >>> print find_tag_by_id(browser.contents, 'recently-approved') | 99 | >>> tag = find_tag_by_id(browser.contents, 'recently-approved') |
600 | 95 | None | 100 | >>> print tag['class'] |
601 | 96 | 101 | unseen | |
602 | 97 | >>> print find_tag_by_id(browser.contents, 'recently-applied') | 102 | |
603 | 98 | None | 103 | >>> tag = find_tag_by_id(browser.contents, 'recently-proposed') |
604 | 104 | >>> print tag['class'] | ||
605 | 105 | unseen | ||
606 | 106 | |||
607 | 107 | >>> tag = find_tag_by_id(browser.contents, 'recently-invited') | ||
608 | 108 | >>> print tag['class'] | ||
609 | 109 | unseen | ||
610 | 99 | 110 | ||
611 | 100 | In the above case there's no user logged in, so it doesn't actually show | 111 | In the above case there's no user logged in, so it doesn't actually show |
612 | 101 | what's the user's involvement with the team. If the user logs in, he'll see | 112 | what's the user's involvement with the team. If the user logs in, he'll see |
613 | @@ -187,7 +198,7 @@ | |||
614 | 187 | >>> owner_browser = setupBrowser(auth="Basic foo.bar@canonical.com:test") | 198 | >>> owner_browser = setupBrowser(auth="Basic foo.bar@canonical.com:test") |
615 | 188 | >>> owner_browser.open('http://launchpad.dev/~ubuntu-team') | 199 | >>> owner_browser.open('http://launchpad.dev/~ubuntu-team') |
616 | 189 | >>> print extract_text( | 200 | >>> print extract_text( |
618 | 190 | ... find_tag_by_id(owner_browser.contents, 'recently-applied')) | 201 | ... find_tag_by_id(owner_browser.contents, 'recently-proposed')) |
619 | 191 | Pending approval | 202 | Pending approval |
620 | 192 | Sample Person | 203 | Sample Person |
621 | 193 | Andrew Bennetts | 204 | Andrew Bennetts |
622 | 194 | 205 | ||
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 | 174 | >>> message in second_browser.contents | 174 | >>> message in second_browser.contents |
628 | 175 | True | 175 | True |
629 | 176 | 176 | ||
630 | 177 | An admin can see the former members of the team. | ||
631 | 178 | |||
632 | 179 | >>> browser.open('http://launchpad.dev/~name18/+members') | ||
633 | 180 | >>> print extract_text( | ||
634 | 181 | ... find_tag_by_id(browser.contents, 'inactivemembers')) | ||
635 | 182 | Name Joined in Status... | ||
636 | 183 | |||
637 | 184 | Other users cannot see the former members of the team. | ||
638 | 185 | |||
639 | 186 | >>> user_browser.open('http://launchpad.dev/~name18/+members') | ||
640 | 187 | >>> print find_tag_by_id(user_browser.contents, 'inactivemembers') | ||
641 | 188 | None | ||
642 | 189 | |||
643 | 177 | 190 | ||
644 | 178 | Team Participation page | 191 | Team Participation page |
645 | 179 | ======================= | 192 | ======================= |
646 | 180 | 193 | ||
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 | 86 | <div class="first yui-u"> | 86 | <div class="first yui-u"> |
652 | 87 | <metal:details use-macro="context/@@+person-macros/team-details" /> | 87 | <metal:details use-macro="context/@@+person-macros/team-details" /> |
653 | 88 | </div> | 88 | </div> |
655 | 89 | <div class="first yui-u"> | 89 | <div class="yui-u"> |
656 | 90 | <div tal:content="structure context/@@+portlet-membership" /> | 90 | <div tal:content="structure context/@@+portlet-membership" /> |
657 | 91 | </div> | 91 | </div> |
658 | 92 | </div> | 92 | </div> |
659 | 93 | 93 | ||
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 | 207 | 207 | ||
665 | 208 | <li tal:condition="view/inactive_memberships/batch/total" | 208 | <li tal:condition="view/inactive_memberships/batch/total" |
666 | 209 | tal:define="inactive_batch nocall:view/inactive_memberships/currentBatch"> | 209 | tal:define="inactive_batch nocall:view/inactive_memberships/currentBatch"> |
718 | 210 | <a name="former"></a> | 210 | <div tal:condition="user_can_edit_memberships"> |
719 | 211 | <h2>Former members</h2> | 211 | <a name="former"></a> |
720 | 212 | 212 | <h2>Former members</h2> | |
721 | 213 | <p> | 213 | |
722 | 214 | These are the members whose subscriptions have expired, or were | 214 | <p> |
723 | 215 | deactivated by themselves or by a team administrator. | 215 | These are the members whose subscriptions have expired, or were |
724 | 216 | </p> | 216 | deactivated by themselves or by a team administrator. |
725 | 217 | 217 | </p> | |
726 | 218 | <div class="lesser" id="inactive-top-navigation"> | 218 | |
727 | 219 | <tal:navigation | 219 | <div class="lesser" id="inactive-top-navigation"> |
728 | 220 | content="structure view/inactive_memberships/@@+navigation-links-upper" /> | 220 | <tal:navigation |
729 | 221 | </div> | 221 | content="structure view/inactive_memberships/@@+navigation-links-upper" /> |
730 | 222 | <table class="listing sortable" id="inactivemembers"> | 222 | </div> |
731 | 223 | <thead> | 223 | <table class="listing sortable" id="inactivemembers"> |
732 | 224 | <tr> | 224 | <thead> |
733 | 225 | <th>Name</th> | 225 | <tr> |
734 | 226 | <th>Joined in</th> | 226 | <th>Name</th> |
735 | 227 | <th>Status</th> | 227 | <th>Joined in</th> |
736 | 228 | <th> </th> | 228 | <th>Status</th> |
737 | 229 | </tr> | 229 | <th> </th> |
738 | 230 | </thead> | 230 | </tr> |
739 | 231 | <tbody> | 231 | </thead> |
740 | 232 | <tr tal:repeat="membership inactive_batch"> | 232 | <tbody> |
741 | 233 | <tal:block define="member membership/person"> | 233 | <tr tal:repeat="membership inactive_batch"> |
742 | 234 | <td> | 234 | <tal:block define="member membership/person"> |
743 | 235 | <a tal:attributes="href member/fmt:url" | 235 | <td> |
744 | 236 | tal:content="member/displayname" /> | 236 | <a tal:attributes="href member/fmt:url" |
745 | 237 | </td> | 237 | tal:content="member/displayname" /> |
746 | 238 | <td | 238 | </td> |
747 | 239 | tal:content="membership/datejoined/fmt:date" | 239 | <td |
748 | 240 | >2005-04-01</td> | 240 | tal:content="membership/datejoined/fmt:date" |
749 | 241 | 241 | >2005-04-01</td> | |
750 | 242 | <td tal:condition="membership/isExpired"> | 242 | |
751 | 243 | Expired on | 243 | <td tal:condition="membership/isExpired"> |
752 | 244 | <span tal:replace="membership/dateexpires/fmt:date" /> | 244 | Expired on |
753 | 245 | </td> | 245 | <span tal:replace="membership/dateexpires/fmt:date" /> |
754 | 246 | <td | 246 | </td> |
755 | 247 | tal:condition="not: membership/isExpired" | 247 | <td |
756 | 248 | tal:content="membership/status/title" /> | 248 | tal:condition="not: membership/isExpired" |
757 | 249 | <td tal:condition="user_can_edit_memberships"> | 249 | tal:content="membership/status/title" /> |
758 | 250 | <a tal:attributes="href membership/fmt:url" | 250 | <td tal:condition="user_can_edit_memberships"> |
759 | 251 | ><img src="/@@/edit" | 251 | <a tal:attributes="href membership/fmt:url" |
760 | 252 | title="Change membership duration or status" /></a> | 252 | ><img src="/@@/edit" |
761 | 253 | </td> | 253 | title="Change membership duration or status" /></a> |
762 | 254 | </tal:block> | 254 | </td> |
763 | 255 | </tr> | 255 | </tal:block> |
764 | 256 | </tbody> | 256 | </tr> |
765 | 257 | </table> | 257 | </tbody> |
766 | 258 | <div class="lesser"> | 258 | </table> |
767 | 259 | <tal:navigation | 259 | <div class="lesser"> |
768 | 260 | content="structure view/inactive_memberships/@@+navigation-links-lower" /> | 260 | <tal:navigation |
769 | 261 | content="structure view/inactive_memberships/@@+navigation-links-lower" /> | ||
770 | 262 | </div> | ||
771 | 261 | </div> | 263 | </div> |
772 | 262 | </li> | 264 | </li> |
773 | 263 | </ul> | 265 | </ul> |
774 | 264 | 266 | ||
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 | 15 | 15 | ||
780 | 16 | <div id="membership-summary" class="portletBody portletContent" | 16 | <div id="membership-summary" class="portletBody portletContent" |
781 | 17 | style="margin-bottom: 1.5em;"> | 17 | style="margin-bottom: 1.5em;"> |
784 | 18 | <div id="membership-summary"> | 18 | <div> |
785 | 19 | <div> | 19 | <div id="membership-counts"> |
786 | 20 | <img src="/@@/team" alt="team" /> | 20 | <img src="/@@/team" alt="team" /> |
788 | 21 | <strong id="member-count"> | 21 | <strong id="approved-member-count"> |
789 | 22 | <tal:active content="context/all_member_count" /> | 22 | <tal:active content="context/all_member_count" /> |
790 | 23 | </strong> | 23 | </strong> |
791 | 24 | <a tal:attributes="href string:${context/fmt:url/+members}#active" | 24 | <a tal:attributes="href string:${context/fmt:url/+members}#active" |
792 | 25 | >active members</a><tal:invited | 25 | >active members</a><tal:invited |
793 | 26 | define="invited_member_count context/invited_member_count" | 26 | define="invited_member_count context/invited_member_count" |
794 | 27 | condition="invited_member_count">, | 27 | condition="invited_member_count">, |
796 | 28 | <strong> | 28 | <strong id="invited-member-count"> |
797 | 29 | <tal:invited_count content="context/invited_member_count" /> | 29 | <tal:invited_count content="context/invited_member_count" /> |
798 | 30 | </strong> | 30 | </strong> |
799 | 31 | <a tal:attributes="href string:${context/fmt:url/+members}#invited" | 31 | <a tal:attributes="href string:${context/fmt:url/+members}#invited" |
800 | 32 | >invited members</a></tal:invited><tal:proposed | 32 | >invited members</a></tal:invited><tal:proposed |
801 | 33 | define="proposed_member_count context/proposed_member_count" | 33 | define="proposed_member_count context/proposed_member_count" |
802 | 34 | condition="proposed_member_count">, | 34 | condition="proposed_member_count">, |
804 | 35 | <strong> | 35 | <strong id="proposed-member-count"> |
805 | 36 | <tal:proposed_count content="proposed_member_count" /> | 36 | <tal:proposed_count content="proposed_member_count" /> |
806 | 37 | </strong> | 37 | </strong> |
807 | 38 | <a tal:attributes="href string:${context/fmt:url/+members}#proposed" | 38 | <a tal:attributes="href string:${context/fmt:url/+members}#proposed" |
808 | @@ -95,37 +95,40 @@ | |||
809 | 95 | <table style="margin: 0px 0px .5em 0px;"> | 95 | <table style="margin: 0px 0px .5em 0px;"> |
810 | 96 | <tr> | 96 | <tr> |
811 | 97 | <td style="padding: 0px 1em 1em 0px;" | 97 | <td style="padding: 0px 1em 1em 0px;" |
815 | 98 | tal:define="link context/menu:overview/add_member"> | 98 | tal:define="link context/menu:overview/add_member" |
816 | 99 | <span id="add-member-spinner" class="update-in-progress-message" | 99 | tal:condition="link/enabled"> |
817 | 100 | style="display: none"> | 100 | <span id="add-member-spinner" |
818 | 101 | class="unseen update-in-progress-message"> | ||
819 | 101 | Saving... | 102 | Saving... |
820 | 102 | </span> | 103 | </span> |
821 | 103 | <tal:add-member replace="structure link/fmt:link-icon" /> | 104 | <tal:add-member replace="structure link/fmt:link-icon" /> |
822 | 104 | </td> | 105 | </td> |
824 | 105 | <td style="padding: 0px 0px 1em 0px;" | 106 | <td colspan="2" |
825 | 107 | style="padding: 0px 0px 1em 0px;" | ||
826 | 106 | tal:define="link context/menu:overview/mugshots"> | 108 | tal:define="link context/menu:overview/mugshots"> |
827 | 107 | <tal:mugshots replace="structure link/fmt:link-icon" /> | 109 | <tal:mugshots replace="structure link/fmt:link-icon" /> |
828 | 108 | </td> | 110 | </td> |
829 | 109 | </tr> | 111 | </tr> |
830 | 112 | </table> | ||
831 | 113 | <table> | ||
832 | 110 | <tr> | 114 | <tr> |
844 | 111 | <td style="padding: 3px 3em 0px 0px;"> | 115 | <td id="recently-approved" |
845 | 112 | <div id="recently-approved" | 116 | style="padding-right: 1em" |
846 | 113 | tal:attributes="style view/recently_approved_hidden"> | 117 | tal:attributes="class view/recently_approved_hidden"> |
847 | 114 | <h3 style="color:black; font-weight:bold; margin: 0px"> | 118 | <h3 style="color:black; font-weight:bold; margin: 0px"> |
848 | 115 | Latest members | 119 | Latest members |
849 | 116 | </h3> | 120 | </h3> |
850 | 117 | <ul id="recently-approved-ul"> | 121 | <ul id="recently-approved-ul"> |
851 | 118 | <li tal:repeat="person view/recently_approved_members" | 122 | <li tal:repeat="person view/recently_approved_members" |
852 | 119 | tal:content="structure person/fmt:link" /> | 123 | tal:content="structure person/fmt:link" /> |
853 | 120 | </ul> | 124 | </ul> |
843 | 121 | </div> | ||
854 | 122 | </td> | 125 | </td> |
857 | 123 | <td style="padding: 0px;" id="recently-applied" | 126 | <td id="recently-proposed" |
858 | 124 | tal:condition="view/recently_proposed_members"> | 127 | tal:attributes="class view/recently_proposed_hidden"> |
859 | 125 | <h3 style="color:black; font-weight:bold; margin: 0px"> | 128 | <h3 style="color:black; font-weight:bold; margin: 0px"> |
860 | 126 | Pending approval | 129 | Pending approval |
861 | 127 | </h3> | 130 | </h3> |
863 | 128 | <ul> | 131 | <ul id="recently-proposed-ul"> |
864 | 129 | <li tal:repeat="person view/recently_proposed_members" | 132 | <li tal:repeat="person view/recently_proposed_members" |
865 | 130 | tal:content="structure person/fmt:link" /> | 133 | tal:content="structure person/fmt:link" /> |
866 | 131 | </ul> | 134 | </ul> |
867 | @@ -136,6 +139,16 @@ | |||
868 | 136 | </td> | 139 | </td> |
869 | 137 | </tr> | 140 | </tr> |
870 | 138 | </table> | 141 | </table> |
871 | 142 | <div id="recently-invited" | ||
872 | 143 | tal:attributes="class view/recently_invited_hidden"> | ||
873 | 144 | <h3 style="color:black; font-weight:bold; margin: 0px"> | ||
874 | 145 | Latest invited | ||
875 | 146 | </h3> | ||
876 | 147 | <ul id="recently-invited-ul"> | ||
877 | 148 | <li tal:repeat="person view/recently_invited_members" | ||
878 | 149 | tal:content="structure person/fmt:link" /> | ||
879 | 150 | </ul> | ||
880 | 151 | </div> | ||
881 | 139 | </tal:can-view> | 152 | </tal:can-view> |
882 | 140 | </div> | 153 | </div> |
883 | 141 | </tal:root> | 154 | </tal:root> |
884 | 142 | 155 | ||
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 | 1 | # Copyright 2009 Canonical Ltd. This software is licensed under the | ||
890 | 2 | # GNU Affero General Public License version 3 (see the file LICENSE). | ||
891 | 3 | |||
892 | 4 | """Test team index page.""" | ||
893 | 5 | |||
894 | 6 | __metaclass__ = type | ||
895 | 7 | __all__ = [] | ||
896 | 8 | |||
897 | 9 | import unittest | ||
898 | 10 | |||
899 | 11 | from windmill.authoring import WindmillTestClient | ||
900 | 12 | |||
901 | 13 | from canonical.launchpad.windmill.testing import lpuser | ||
902 | 14 | from canonical.launchpad.windmill.testing.widgets import ( | ||
903 | 15 | search_and_select_picker_widget) | ||
904 | 16 | |||
905 | 17 | from lp.registry.windmill.testing import RegistryWindmillLayer | ||
906 | 18 | from lp.testing import TestCaseWithFactory | ||
907 | 19 | |||
908 | 20 | |||
909 | 21 | class TestTeamIndex(TestCaseWithFactory): | ||
910 | 22 | """Test team index page.""" | ||
911 | 23 | |||
912 | 24 | layer = RegistryWindmillLayer | ||
913 | 25 | |||
914 | 26 | def setUp(self): | ||
915 | 27 | self.client = WindmillTestClient(__name__) | ||
916 | 28 | |||
917 | 29 | def test_addmember(self): | ||
918 | 30 | self.client.open( | ||
919 | 31 | url=u'http://launchpad.dev:8085/~testing-spanish-team') | ||
920 | 32 | |||
921 | 33 | lpuser.TRANSLATIONS_ADMIN.ensure_login(self.client) | ||
922 | 34 | |||
923 | 35 | addmember_xpath = ( | ||
924 | 36 | '//*[@id="membership"]' + | ||
925 | 37 | '//a[text()="Add member" ' + | ||
926 | 38 | 'and contains(@class, "js-action")]') | ||
927 | 39 | # Add rosetta-admins team as a member. | ||
928 | 40 | approved_count_xpath = '//*[@id="approved-member-count"]' | ||
929 | 41 | code = "lookupNode({xpath: '%s'}).textContent" % approved_count_xpath | ||
930 | 42 | result = self.client.commands.execJS(code=code) | ||
931 | 43 | old_approved_count = int(result['result']) | ||
932 | 44 | |||
933 | 45 | self.client.waits.forElement(xpath=addmember_xpath) | ||
934 | 46 | self.client.click(xpath=addmember_xpath) | ||
935 | 47 | # Xpath is 1-indexed. | ||
936 | 48 | search_and_select_picker_widget(self.client, 'rosetta', 1) | ||
937 | 49 | |||
938 | 50 | self.client.waits.forElement( | ||
939 | 51 | xpath='//ul[@id="recently-approved-ul"]/li[1]/a', | ||
940 | 52 | option='href|~rosetta-admins') | ||
941 | 53 | |||
942 | 54 | self.client.asserts.assertText( | ||
943 | 55 | xpath=approved_count_xpath, | ||
944 | 56 | validator=str(old_approved_count+1)) | ||
945 | 57 | |||
946 | 58 | # Add another team as a member. | ||
947 | 59 | invited_count_xpath = '//*[@id="invited-member-count"]' | ||
948 | 60 | self.client.asserts.assertNotNode(xpath=invited_count_xpath) | ||
949 | 61 | self.client.click(xpath=addmember_xpath) | ||
950 | 62 | search_and_select_picker_widget(self.client, 'simple', 1) | ||
951 | 63 | |||
952 | 64 | self.client.waits.forElement( | ||
953 | 65 | xpath='//ul[@id="recently-invited-ul"]/li[1]/a', | ||
954 | 66 | option='href|~simple-team') | ||
955 | 67 | |||
956 | 68 | self.client.asserts.assertText( | ||
957 | 69 | xpath=invited_count_xpath, | ||
958 | 70 | validator="1") | ||
959 | 71 | |||
960 | 72 | |||
961 | 73 | def test_suite(): | ||
962 | 74 | 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.