Merge lp:~danilo/launchpad/bug728370-direct-subs into lp:launchpad

Proposed by Данило Шеган
Status: Merged
Approved by: Данило Шеган
Approved revision: no longer in the source branch.
Merged at revision: 12826
Proposed branch: lp:~danilo/launchpad/bug728370-direct-subs
Merge into: lp:launchpad
Prerequisite: lp:~danilo/launchpad/bug728370-nondirect-subs
Diff against target: 899 lines (+779/-13)
6 files modified
lib/lp/bugs/javascript/subscription.js (+212/-9)
lib/lp/bugs/javascript/tests/test_subscription.html (+1/-0)
lib/lp/bugs/javascript/tests/test_subscription.js (+560/-1)
lib/lp/bugs/model/personsubscriptioninfo.py (+2/-1)
lib/lp/bugs/model/tests/test_personsubscriptioninfo.py (+1/-0)
lib/lp/bugs/templates/bug-subscription-list.pt (+3/-2)
To merge this branch: bzr merge lp:~danilo/launchpad/bug728370-direct-subs
Reviewer Review Type Date Requested Status
Brad Crittenden (community) code Approve
Review via email: mp+57464@code.launchpad.net

Commit message

[r=bac][bug=728370][incr] Provide rendering of descriptions for all non-structural subscriptions on a bug:+subscriptions page.

Description of the change

= Bug 728370: show subscription descriptions =

This is part of work on bug 728370: displaying user-readable descriptions of all the subscriptions a person has either directly or implicitely to a particular bug. Main goal of the page is for people to be able to unsubscribe quickly from bug mail. This branch does not worry about structural subscriptions.

I'll fix lint issues after the review (so diff doesn't grow even further). Diff is slightly over-sized, but majority of the changes are tests (about 2/3 of the diff).

== Proposed fix ==

Continuing the work on the branch this depends on (getting landed), we introduce a direct subscription handler and tests for it. It directly returns the reason (unlike "other" subscriptions which also returned potential variable replacements).

Also, this includes actual Y.Node construction for in-page elements containing these descriptions, but it is ugly on-purpose. Prettyfying should be the next, independent step (branch). Since this is protected by the feature flag, no harm is done by landing this as-is.

In future steps (not part of 728370), we'll also introduce actions for each of these subscriptions. The goal is to get this landed and allow people to independently work on prettyfying and adding suitable actions for unsubscribing.

== Tests ==

lib/lp/bugs/javascript/tests/test_subscription.html
bin/test -cvvt PersonSubscriptionInfo.*getDataForClient

== Demo and Q/A ==

Look at:

  https://bugs.launchpad.dev/firefox/+bug/1/+subscriptions

(note, add "malone.advanced-structural-subscriptions.enabled default 1 on" to launchpad.dev/+feature-rules)

To facilitate the demo, add a few subscriptions:

 - directly subscribe to bug 1
 - subscribe one of your teams you are a member of
 - subscribe one of your teams you are an admin of
 - make bugs 2 and 3 duplicates of bug 1
 - subscribe personally/as-team-member/as-team-admin to one of those
   duplicates
 - make yourself/your team the assignee on one or several bug tasks

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/javascript/tests/test_subscription.html
  lib/lp/bugs/javascript/subscription.js
  lib/lp/bugs/javascript/tests/test_subscription.js
  lib/lp/bugs/templates/bug-subscription-list.pt
  lib/lp/bugs/model/personsubscriptioninfo.py
  lib/lp/bugs/model/tests/test_personsubscriptioninfo.py

./lib/lp/bugs/model/personsubscriptioninfo.py
     119: E501 line too long (80 characters)
     259: E301 expected 1 blank line, found 0
     269: E301 expected 1 blank line, found 0
     276: E301 expected 1 blank line, found 0
     307: E202 whitespace before ')'
     315: E202 whitespace before ')'
     119: Line exceeds 78 characters.
./lib/lp/bugs/model/tests/test_personsubscriptioninfo.py
      12: 'searchbuilder' imported but unused
      29: 'anonymous_logged_in' imported but unused
       8: 'Store' imported but unused
      16: 'BugTaskStatus' imported but unused
      10: 'ProxyFactory' imported but unused
       9: 'Unauthorized' imported but unused
      16: 'BugTaskImportance' imported but unused
      29: 'login_person' imported but unused
      26: 'BugSubscriptionFilter' imported but unused
      13: 'IStore' imported but unused
      37: E302 expected 2 blank lines, found 1

To post a comment you must log in.
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Danilo,

Two small issues I found are:

* dedent the line 'one direct personal subscription.');

* for bug.private the situation may be more complicated. the person may be the product owner and not in the bug supervisor team. it is a corner case but one where we should not give false information.

The elaborate substitution framework you've created looks to be very well thought out and tested. I hope that the usage of it justifies the work you've done...but I don't see the larger design here.

I'm very pleased with the level of documentation and the testing you've done is quite thorough.

review: Approve (code)
Revision history for this message
Brad Crittenden (bac) wrote :

Hi Danilo,

Two small issues I found are:

* dedent the line 'one direct personal subscription.');

* for bug.private the situation may be more complicated. the person may be the product owner and not in the bug supervisor team. it is a corner case but one where we should not give false information.

The elaborate substitution framework you've created looks to be very well thought out and tested. I hope that the usage of it justifies the work you've done...but I don't see the larger design here.

I'm very pleased with the level of documentation and the testing you've done is quite thorough.

review: Approve (code)
Revision history for this message
Данило Шеган (danilo) wrote :

As mentioned on IRC, previous branch answers both of your concerns: subscriptions as implicit supervisor are handled there, and the substitution framework is actually used for stuff constructed there. Will fix the indentation, though that's how Emacs espresso-mode indents for string concatenation with +.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'lib/lp/bugs/javascript/subscription.js'
2--- lib/lp/bugs/javascript/subscription.js 2011-04-13 06:26:27 +0000
3+++ lib/lp/bugs/javascript/subscription.js 2011-04-13 16:02:16 +0000
4@@ -26,6 +26,9 @@
5 */
6 var reasons = {
7 NOT_SUBSCRIBED: "You are not subscribed to this bug.",
8+ NOT_PERSONALLY_SUBSCRIBED: (
9+ "You are not directly subscribed to this bug, " +
10+ "but you have other subscriptions."),
11 MUTED_SUBSCRIPTION: "You have muted all email from this bug."
12 };
13 namespace._reasons = reasons;
14@@ -77,10 +80,10 @@
15 /* These are the duplicate bug variations. */
16 var _SUBSCRIBED_TO_DUPLICATE = (
17 'a direct subscriber to bug {duplicate_bug}, which is marked as a ' +
18- 'duplicate of this bug, {bug_id}');
19+ 'duplicate of this bug, {bug_id}.');
20 var _SUBSCRIBED_TO_DUPLICATES = (
21 'a direct subscriber to bugs {duplicate_bugs}, which are marked as ' +
22- 'duplicates of this bug, {bug_id}');
23+ 'duplicates of this bug, {bug_id}.');
24 /* These are the actual strings to use. */
25 Y.mix(reasons, {
26 YOU_SUBSCRIBED_TO_DUPLICATE: _BECAUSE_YOU_ARE + _SUBSCRIBED_TO_DUPLICATE,
27@@ -189,7 +192,7 @@
28 if (typeof(bug) == 'string') {
29 return bug;
30 } else {
31- return ObjectLink(bug, 'bug #' + bug.id.toString(), bug.web_link);
32+ return ObjectLink(bug, '#' + bug.id.toString(), bug.web_link);
33 }
34 }
35
36@@ -416,17 +419,217 @@
37
38 }
39
40-function get_subscription_reason(config) {
41+/**
42+ * Get direct subscription information.
43+ */
44+function get_direct_subscription_information(info) {
45+ var reason;
46+ var reasons = namespace._reasons;
47+ if (info.count == 0) {
48+ reason = reasons.NOT_SUBSCRIBED;
49+ } else if (info.muted) {
50+ reason = reasons.MUTED_SUBSCRIPTION;
51+ } else if (info.direct.personal.length > 0) {
52+ if (info.direct.personal.length > 1) {
53+ Y.error(
54+ 'A person should not have more than ' +
55+ 'one direct personal subscription.');
56+ }
57+ var subscription = info.direct.personal[0];
58+ var bug = subscription.bug;
59+ if (subscription.principal_is_reporter) {
60+ reason = reasons.YOU_REPORTED;
61+ } else if (bug.private) {
62+ reason = reasons.YOU_SUBSCRIBED_BUG_SUPERVISOR;
63+ } else if (bug.security_related) {
64+ // If bug is both private and security-related, you'll
65+ // only get the description talking about privacy.
66+ // Not considered a big deal.
67+ reason = reasons.YOU_SUBSCRIBED_SECURITY_CONTACT;
68+ } else {
69+ reason = reasons.YOU_SUBSCRIBED;
70+ }
71+ } else {
72+ // No direct subscriptions, but there are other
73+ // subscriptions (because info.count != 0).
74+ reason = reasons.NOT_PERSONALLY_SUBSCRIBED;
75+ }
76+ return reason;
77+}
78+namespace._get_direct_subscription_information =
79+ get_direct_subscription_information;
80+
81+/**
82+ * Returns an anchor element HTML for an ObjectLink element.
83+ * It safely encodes the `title` and `url` elements to avoid any XSS vectors.
84+ *
85+ * @method get_objectlink_html
86+ * @param {Object} element ObjectLink element or a simple string.
87+ * @returns {String} HTML for the A element representing passed in
88+ * ObjectLink `element`. If `element` is a string, return it unmodified.
89+ */
90+function get_objectlink_html(element) {
91+ if (Y.Lang.isString(element)) {
92+ return element;
93+ } else if (Y.Lang.isObject(element)) {
94+ if (element.url === undefined && element.title == undefined) {
95+ Y.error('Not a proper ObjectLink.');
96+ }
97+ var node = Y.Node.create('<div></div>');
98+ node.appendChild(
99+ Y.Node.create('<a></a>')
100+ .set('href', element.url)
101+ .set('text', element.title));
102+ var text = node.get('innerHTML');
103+ node.destroy(true);
104+ return text;
105+ }
106+}
107+namespace._get_objectlink_html = get_objectlink_html;
108+
109+/**
110+ * Array sort function for objects sorting them by their `title` property.
111+ */
112+function sort_by_title(a, b) {
113+ return ((a.title == b.title) ? 0 :
114+ ((a.title > b.title) ? 1 : -1));
115+}
116+
117+/**
118+ * Renders the description in a safe manner escaping HTML as appropriate.
119+ *
120+ * @method safely_render_description
121+ * @param {Object} subscription Object containing the string `reason` and
122+ * object `vars` containing variables to be replaced in `reason`.
123+ * @param {Object} additional_vars Objects containing additional, global
124+ * variables to also be replaced if not overridden.
125+ * @returns {String} `reason` with all {var} occurrences replaced with
126+ * appropriate subscription.vars[var] values.
127+ */
128+function safely_render_description(subscription, additional_vars) {
129+ function var_replacer(key, vars) {
130+ if (vars !== undefined) {
131+ if (Y.Lang.isArray(vars)) {
132+ vars.sort(sort_by_title);
133+ // We want a plural concatenation.
134+ var final_element = vars.pop();
135+ var text_elements = [];
136+ for (var index in vars) {
137+ text_elements.push(get_objectlink_html(vars[index]));
138+ };
139+ return (text_elements.join(', ') +
140+ ' and ' + get_objectlink_html(final_element));
141+ } else {
142+ return get_objectlink_html(vars);
143+ }
144+ } else {
145+ if (Y.Lang.isObject(additional_vars) &&
146+ additional_vars.hasOwnProperty(key)) {
147+ return get_objectlink_html(additional_vars[key]);
148+ }
149+ }
150+ }
151+ return Y.substitute(subscription.reason, subscription.vars, var_replacer);
152+}
153+namespace._safely_render_description = safely_render_description;
154+
155+/**
156+ * Creates a node to store the direct subscription information.
157+ *
158+ * @param {Object} info LP.cache.bug_subscription_info object.
159+ * @returns {Object} Y.Node with the ID of 'direct-description' and
160+ * text set to the actual textual description of the direct
161+ * personal subscriptions (if any).
162+ */
163+function get_direct_description_node(info) {
164+ var direct = get_direct_subscription_information(info);
165+ var direct_node = Y.Node.create('<div></div>')
166+ .set('id', 'direct-subscription')
167+ .set('text', direct);
168+ return direct_node;
169+}
170+namespace._get_direct_description_node = get_direct_description_node;
171+
172+/**
173+ * Creates a node to store single subscription description.
174+ *
175+ * @param {Object} subscription Object containing `reason` and `vars`
176+ * to be substituted into `reason` with safely_render_description.
177+ * @param {Object} extra_data Extra variables to substitute.
178+ * @returns {Object} Y.Node with the class 'bug-subscription-description'
179+ * and textual description in a separate node with
180+ * class 'description-text'.
181+ */
182+function get_single_description_node(subscription, extra_data) {
183+ var node = Y.Node.create('<div></div>')
184+ .addClass('subscription-description');
185+ node.appendChild(
186+ Y.Node.create('<div></div>')
187+ .addClass('description-text')
188+ .set('innerHTML',
189+ safely_render_description(subscription, extra_data)));
190+ return node;
191+}
192+namespace._get_single_description_node = get_single_description_node;
193+
194+/**
195+ * Creates a node to store "other" subscriptions information.
196+ * "Other" means any bug subscriptions which are not personal and direct.
197+ *
198+ * @param {Object} info LP.cache.bug_subscription_info object.
199+ * @param {Object} extra_data Additional global variables to substitute
200+ * in strings. Passed directly through to safely_render_description().
201+ * @returns {Object} Y.Node with the ID of 'other-subscriptions' and
202+ * add descriptions of each subscription as a separate node.
203+ */
204+function get_other_descriptions_node(info, extra_data) {
205+ var subs = gather_nondirect_subscriptions(info);
206+ if (subs.length > 0) {
207+ var other_node = Y.Node.create('<div></div>')
208+ .set('id', 'other-subscriptions');
209+
210+ for (var index in subs) {
211+ other_node.appendChild(
212+ get_single_description_node(subs[index], extra_data));
213+ }
214+
215+ return other_node;
216+ } else {
217+ return undefined;
218+ }
219+}
220+namespace._get_other_descriptions_node = get_other_descriptions_node;
221+
222+
223+/**
224+ * Add descriptions for all non-structural subscriptions to the page.
225+ *
226+ * @param {Object} config Object specifying the node to populate in
227+ * `description_box` and allowing LP.cache.bug_subscription_info
228+ * override with `subscription_info` property.
229+ */
230+function show_subscription_description(config) {
231 // Allow tests to pass subscription_info directly in.
232 var info = config.subscription_info || LP.cache.bug_subscription_info;
233- console.log(info);
234+ // Replace subscription-cache-reference-* strings with actual
235+ // object references.
236 replace_textual_references(info, LP.cache);
237
238- var subs = gather_nondirect_subscriptions(info);
239- console.log(subs);
240-
241+ var extra_data = {
242+ bug_id: '#' + info.bug_id.toString(),
243+ };
244+
245+ var content_node = Y.one(config.description_box);
246+
247+ var direct_node = get_direct_description_node(info);
248+ content_node.appendChild(direct_node);
249+
250+ var other_node = get_other_descriptions_node(info, extra_data);
251+ if (other_node !== undefined) {
252+ content_node.appendChild(other_node);
253+ }
254 }
255-namespace.get_subscription_reason = get_subscription_reason
256+namespace.show_subscription_description = show_subscription_description
257
258 }, '0.1', {requires: [
259 'dom', 'node', 'substitute'
260
261=== modified file 'lib/lp/bugs/javascript/tests/test_subscription.html'
262--- lib/lp/bugs/javascript/tests/test_subscription.html 2011-04-11 10:00:45 +0000
263+++ lib/lp/bugs/javascript/tests/test_subscription.html 2011-04-13 16:02:16 +0000
264@@ -34,6 +34,7 @@
265 </head>
266 <body class="yui3-skin-sam">
267 <!-- Example markup required by test suite -->
268+ <div id="test-root"></div>
269
270 <!-- The test output -->
271 <div id="log"></div>
272
273=== modified file 'lib/lp/bugs/javascript/tests/test_subscription.js'
274--- lib/lp/bugs/javascript/tests/test_subscription.js 2011-04-13 06:26:27 +0000
275+++ lib/lp/bugs/javascript/tests/test_subscription.js 2011-04-13 16:02:16 +0000
276@@ -588,7 +588,7 @@
277 Y.Assert.areEqual('My team', subs[0].vars.team.title);
278 Y.Assert.areEqual('http://link', subs[0].vars.team.url);
279
280- Y.Assert.areEqual('bug #1', subs[0].vars.duplicate_bug.title);
281+ Y.Assert.areEqual('#1', subs[0].vars.duplicate_bug.title);
282 Y.Assert.areEqual(
283 'http://launchpad/bug/1', subs[0].vars.duplicate_bug.url);
284 },
285@@ -722,6 +722,565 @@
286 },
287 }));
288
289+
290+/**
291+ * Helper to construct a single 'category' of subscriptions,
292+ * grouped by type (personally, as team member and as team admin).
293+ */
294+function _constructCategory(personal, as_member, as_admin) {
295+ if (personal === undefined) {
296+ personal = [];
297+ }
298+ if (as_member === undefined) {
299+ as_member = [];
300+ }
301+ if (as_admin === undefined) {
302+ as_admin = [];
303+ }
304+ return {
305+ count: personal.length + as_admin.length + as_member.length,
306+ personal: personal,
307+ as_team_member: as_member,
308+ as_team_admin: as_admin
309+ };
310+}
311+
312+/**
313+ * Get the reason for a direct subscription.
314+ * Tests for method get_direct_subscription_information().
315+ */
316+suite.add(new Y.Test.Case({
317+ name: 'Get reason for a direct subscription',
318+
319+ _should: {
320+ error: {
321+ test_multiple_direct_subscriptions:
322+ new Error('A person should not have more than ' +
323+ 'one direct personal subscription.')
324+ }
325+ },
326+
327+ test_multiple_direct_subscriptions: function() {
328+ // It should not be possible to have multiple direct,
329+ // personal subscriptions.
330+ // This errors out (see _should.error above).
331+ var info = {
332+ direct: _constructCategory(['1', '2']),
333+ count: 2
334+ };
335+ module._get_direct_subscription_information(info);
336+ },
337+
338+ test_no_subscriptions: function() {
339+ // There are no subscriptions at all.
340+ var info = {
341+ direct: _constructCategory(),
342+ from_duplicates: _constructCategory()
343+ };
344+ info.count = info.direct.count + info.from_duplicates.count;
345+
346+ Y.Assert.areEqual(
347+ module._reasons.NOT_SUBSCRIBED,
348+ module._get_direct_subscription_information(info));
349+ },
350+
351+ test_no_direct_subscriptions: function() {
352+ // There is no direct subscription, but there are
353+ // other subscriptions.
354+ var info = {
355+ direct: _constructCategory(),
356+ from_duplicates: _constructCategory(['dupe'])
357+ };
358+ info.count = info.direct.count + info.from_duplicates.count;
359+ Y.Assert.areSame(
360+ module._reasons.NOT_PERSONALLY_SUBSCRIBED,
361+ module._get_direct_subscription_information(info));
362+ },
363+
364+ test_muted_subscription: function() {
365+ // The direct subscription is muted.
366+ var info = {
367+ direct: _constructCategory(['direct']),
368+ muted: true,
369+ };
370+ info.count = info.direct.count;
371+ Y.Assert.areSame(
372+ module._reasons.MUTED_SUBSCRIPTION,
373+ module._get_direct_subscription_information(info));
374+ },
375+
376+ test_direct_subscription: function() {
377+ // The simple direct subscription.
378+ var sub = {
379+ bug: {
380+ private: false,
381+ security_related: false,
382+ },
383+ principal_is_reporter: false,
384+ };
385+ var info = {
386+ direct: _constructCategory([sub]),
387+ count: 1
388+ };
389+
390+ Y.Assert.areSame(
391+ module._reasons.YOU_SUBSCRIBED,
392+ module._get_direct_subscription_information(info));
393+ },
394+
395+ test_direct_subscription_as_reporter: function() {
396+ // The direct subscription created for bug reporter.
397+ var sub = {
398+ bug: {},
399+ principal_is_reporter: true,
400+ };
401+ var info = {
402+ direct: _constructCategory([sub]),
403+ count: 1
404+ };
405+ Y.Assert.areSame(
406+ module._reasons.YOU_REPORTED,
407+ module._get_direct_subscription_information(info));
408+ },
409+
410+ test_direct_subscription_for_supervisor: function() {
411+ // The direct subscription created on private bugs for
412+ // the bug supervisor.
413+ var sub = {
414+ bug: {
415+ private: true,
416+ },
417+ };
418+ var info = {
419+ direct: _constructCategory([sub]),
420+ count: 1
421+ };
422+ Y.Assert.areSame(
423+ module._reasons.YOU_SUBSCRIBED_BUG_SUPERVISOR,
424+ module._get_direct_subscription_information(info));
425+ },
426+
427+ test_direct_subscription_for_security_contact: function() {
428+ // The simple direct subscription.
429+ var sub = {
430+ bug: {
431+ security_related: true,
432+ },
433+ };
434+ var info = {
435+ direct: _constructCategory([sub]),
436+ count: 1
437+ };
438+ Y.Assert.areSame(
439+ module._reasons.YOU_SUBSCRIBED_SECURITY_CONTACT,
440+ module._get_direct_subscription_information(info));
441+ },
442+
443+}));
444+
445+/**
446+ * Test for get_objectlink_html() method.
447+ */
448+suite.add(new Y.Test.Case({
449+ name: 'Test conversion of ObjectLink to HTML.',
450+
451+ _should: {
452+ error: {
453+ test_non_link: new Error('Not a proper ObjectLink.')
454+ }
455+ },
456+
457+ test_string: function() {
458+ // When a string is passed in, it is returned unmodified.
459+ var link = 'test';
460+ Y.Assert.areEqual(
461+ link,
462+ module._get_objectlink_html(link));
463+ },
464+
465+ test_non_link: function() {
466+ // When an object that doesn't have both 'title' and 'url'
467+ // passed in, it fails. (see _should.error above)
468+ var link = {};
469+ module._get_objectlink_html(link);
470+ },
471+
472+ test_simple: function() {
473+ // When a string is passed in, it is returned unmodified.
474+ var link = {
475+ title: 'Title',
476+ url: 'http://url/'
477+ };
478+ Y.Assert.areEqual(
479+ '<a href="http://url/">Title</a>',
480+ module._get_objectlink_html(link));
481+ },
482+
483+ test_escaping_title: function() {
484+ // Even with title containing HTML characters, they are properly
485+ // escaped.
486+ var link = {
487+ title: 'Title<script>',
488+ url: 'http://url/'
489+ };
490+ Y.Assert.areEqual(
491+ '<a href="http://url/">Title&lt;script&gt;</a>',
492+ module._get_objectlink_html(link));
493+ },
494+
495+ test_escaping_url: function() {
496+ // Even with title containing HTML characters, they are properly
497+ // escaped.
498+ var url = 'http://url/" onclick="javascript:alert(\'test\');" a="';
499+ var link = {
500+ title: 'Title',
501+ url: url
502+ };
503+ // Firefox returns:
504+ // '<a href="http://url/%22%20onclick=%22' +
505+ // 'javascript:alert%28%27test%27%29;%22%20a=%22">Title</a>'
506+ // WebKit returns:
507+ // '<a href="http://url/&quot; onclick=&quot;'+
508+ // 'javascript:alert(\'test\');&quot; a=&quot;">Title</a>'
509+ Y.Assert.areNotEqual(
510+ '<a href="' + url + '">Title</a>',
511+ module._get_objectlink_html(link));
512+ },
513+
514+}));
515+
516+/**
517+ * Test for safely_render_description() method.
518+ */
519+suite.add(new Y.Test.Case({
520+ name: 'Test variable substitution in subscription descriptions.',
521+
522+ _should: {
523+ error: {
524+ test_non_link: new Error('Not a proper ObjectLink.')
525+ }
526+ },
527+
528+ test_no_variables: function() {
529+ // For a string with no variables, no substitution is performed.
530+ var sub = {
531+ reason: 'test string with no vars',
532+ vars: { no: 'vars' }
533+ };
534+
535+ Y.Assert.areEqual(
536+ sub.reason,
537+ module._safely_render_description(sub));
538+ },
539+
540+ test_missing_variable: function() {
541+ // If a variable is missing, it is not substituted.
542+ var sub = {
543+ reason: 'test string with {foo}',
544+ vars: {}
545+ };
546+
547+ Y.Assert.areEqual(
548+ 'test string with {foo}',
549+ module._safely_render_description(sub));
550+ },
551+
552+ test_string_variable: function() {
553+ // Plain string variables are directly substituted.
554+ var sub = {
555+ reason: 'test string with {foo}',
556+ vars: { foo: 'nothing' }
557+ };
558+
559+ Y.Assert.areEqual(
560+ 'test string with nothing',
561+ module._safely_render_description(sub));
562+ },
563+
564+ _constructObjectLink: function(title, url) {
565+ // Constructs a mock ObjectLink.
566+ return { title: title, url: url };
567+ },
568+
569+ test_objectlink_variable: function() {
570+ // ObjectLink variables get turned into actual HTML links.
571+ var sub = {
572+ reason: 'test string with {foo}',
573+ vars: { foo: this._constructObjectLink('Title', 'http://link/') }
574+ };
575+
576+ Y.Assert.areEqual(
577+ 'test string with <a href="http://link/">Title</a>',
578+ module._safely_render_description(sub));
579+ },
580+
581+ test_multiple_variables: function() {
582+ // For multiple variables, they all get replaced.
583+ var sub = {
584+ reason: '{simple} string with {foo} {simple}',
585+ vars: {
586+ foo: this._constructObjectLink('Link', 'http://link/'),
587+ simple: "test"
588+ }
589+ };
590+
591+ Y.Assert.areEqual(
592+ 'test string with <a href="http://link/">Link</a> test',
593+ module._safely_render_description(sub));
594+ },
595+
596+ test_extra_variable: function() {
597+ // Passing in extra variables causes them to be replaced as well.
598+ var sub = {
599+ reason: 'test string with {extra}',
600+ vars: {}
601+ };
602+ var extra_vars = {
603+ extra: 'something extra'
604+ };
605+
606+ Y.Assert.areEqual(
607+ 'test string with something extra',
608+ module._safely_render_description(sub, extra_vars));
609+ },
610+
611+ test_extra_objectlink_variable: function() {
612+ // Passing in extra ObjectLink variable gets properly substituted.
613+ var sub = {
614+ reason: 'test string with {extra}',
615+ vars: {}
616+ };
617+ var extra_vars = {
618+ extra: this._constructObjectLink('extras', 'http://link/')
619+ };
620+
621+ Y.Assert.areEqual(
622+ 'test string with <a href="http://link/">extras</a>',
623+ module._safely_render_description(sub, extra_vars));
624+ },
625+
626+}));
627+
628+/**
629+ * Test for get_direct_description_node() method.
630+ */
631+suite.add(new Y.Test.Case({
632+ name: 'Test direct node construction with appropriate description.',
633+
634+ test_no_subscriptions: function() {
635+ // A description is added in even when there are no subscriptions.
636+ var info = {
637+ direct: _constructCategory(),
638+ count: 0
639+ };
640+ var expected_text = module._get_direct_subscription_information(info);
641+ var node = module._get_direct_description_node(info);
642+ Y.Assert.areEqual(
643+ 'direct-subscription', node.get('id'));
644+ Y.Assert.areEqual(
645+ expected_text, node.get('text'));
646+ },
647+
648+ test_direct_subscription: function() {
649+ // One personal, direct subscription exists.
650+ var info = {
651+ direct: _constructCategory([{ bug: {} }]),
652+ count: 1
653+ };
654+ var expected_text = module._get_direct_subscription_information(info);
655+ var node = module._get_direct_description_node(info);
656+ Y.Assert.areEqual(
657+ 'direct-subscription', node.get('id'));
658+ Y.Assert.areEqual(
659+ expected_text, node.get('text'));
660+ },
661+
662+}));
663+
664+/**
665+ * Test for get_single_description_node() method.
666+ */
667+suite.add(new Y.Test.Case({
668+ name: 'Test single subscription description node construction.',
669+
670+ test_simple_text: function() {
671+ // A simple subscription with 'Text' as the reason and no variables.
672+ var sub = { reason: 'Text', vars: {} };
673+ var node = module._get_single_description_node(sub);
674+
675+ // The node has appropriate CSS class set.
676+ Y.Assert.isTrue(node.hasClass('subscription-description'));
677+
678+ // There is also a sub-node containing the actual description.
679+ var subnode = node.one('.description-text');
680+ Y.Assert.areEqual('Text', subnode.get('text'));
681+ },
682+
683+ test_variable_substitution: function() {
684+ // A subscription with variables and extra variables
685+ // has them replaced.
686+ var sub = { reason: 'Test {var1} {var2}',
687+ vars: { var1: 'my text'} };
688+ var extra_data = { var2: 'globally' };
689+ var node = module._get_single_description_node(sub, extra_data);
690+
691+ // The node has appropriate CSS class set.
692+ Y.Assert.isTrue(node.hasClass('subscription-description'));
693+
694+ // There is also a sub-node containing the actual description.
695+ var subnode = node.one('.description-text');
696+ Y.Assert.areEqual('Test my text globally', subnode.get('text'));
697+ },
698+
699+}));
700+
701+/**
702+ * Test for get_other_descriptions_node() method.
703+ */
704+suite.add(new Y.Test.Case({
705+ name: 'Test creation of node describing all non-direct subscriptions.',
706+
707+ test_no_subscriptions: function() {
708+ // With just a personal subscription, undefined is returned.
709+ var info = {
710+ direct: _constructCategory([{ bug: {} }]),
711+ from_duplicate: _constructCategory(),
712+ as_assignee: _constructCategory(),
713+ as_owner: _constructCategory(),
714+ count: 1
715+ };
716+ Y.Assert.areSame(
717+ undefined,
718+ module._get_other_descriptions_node(info));
719+ },
720+
721+ test_one_subscription: function() {
722+ // There is a subscription on the duplicate bug.
723+ var info = {
724+ direct: _constructCategory(),
725+ from_duplicate: _constructCategory([{ bug: {id: 1} }]),
726+ as_assignee: _constructCategory(),
727+ as_owner: _constructCategory(),
728+ count: 1
729+ };
730+
731+ // A node is returned with ID of 'other-subscriptions'.
732+ var node = module._get_other_descriptions_node(info);
733+ Y.Assert.areEqual(
734+ 'other-subscriptions', node.get('id'));
735+ // And it contains single '.subscription-description' node.
736+ Y.Assert.areEqual(
737+ 1, node.all('.subscription-description').size());
738+ },
739+
740+ test_multiple_subscription: function() {
741+ // There is a subscription on the duplicate bug 1,
742+ // and another as assignee on bug 2.
743+ var info = {
744+ direct: _constructCategory(),
745+ from_duplicate: _constructCategory([{ bug: {id: 1} }]),
746+ as_assignee: _constructCategory([{ bug: {id: 2} }]),
747+ as_owner: _constructCategory(),
748+ count: 1
749+ };
750+
751+ // A node is returned containing two
752+ // '.subscription-description' nodes.
753+ var node = module._get_other_descriptions_node(info);
754+ Y.Assert.areEqual(
755+ 2, node.all('.subscription-description').size());
756+ },
757+
758+}));
759+
760+/**
761+ * Test for show_subscription_description() method.
762+ */
763+suite.add(new Y.Test.Case({
764+ name: 'Test showing of subscription descriptions.',
765+
766+ setUp: function() {
767+ this.content_node = Y.Node.create('<div></div>')
768+ .set('id', 'description-container');
769+ this.parent_node = Y.one('#test-root');
770+ this.parent_node.appendChild(this.content_node);
771+ this.config = {
772+ description_box: '#description-container',
773+ };
774+ },
775+
776+ tearDown: function() {
777+ this.parent_node.empty(true);
778+ delete this.config;
779+ },
780+
781+ test_no_subscriptions: function() {
782+ // With no subscriptions, a simple description of that state
783+ // is added.
784+ this.config.subscription_info = {
785+ direct: _constructCategory(),
786+ from_duplicate: _constructCategory(),
787+ as_assignee: _constructCategory(),
788+ as_owner: _constructCategory(),
789+ bug_id: 1,
790+ count: 0
791+ };
792+ window.LP = {};
793+ module.show_subscription_description(this.config);
794+ this.wait(function() {
795+ Y.Assert.areEqual(
796+ 1, this.content_node.all('#direct-subscription').size());
797+ Y.Assert.areEqual(
798+ 0, this.content_node.all('#other-subscriptions').size());
799+ }, 50);
800+ },
801+
802+ test_combined_subscriptions: function() {
803+ // With both direct and implicit subscriptions,
804+ // we get a simple description and a node with other descriptions.
805+ this.config.subscription_info = {
806+ direct: _constructCategory([{ bug: {id:1} }]),
807+ from_duplicate: _constructCategory([{ bug: {id:2} }]),
808+ as_assignee: _constructCategory([{ bug: {id:3} }]),
809+ as_owner: _constructCategory(),
810+ bug_id: 1,
811+ count: 0
812+ };
813+ window.LP = {};
814+ module.show_subscription_description(this.config);
815+ this.wait(function() {
816+ Y.Assert.areEqual(
817+ 1, this.content_node.all('#direct-subscription').size());
818+ Y.Assert.areEqual(
819+ 1, this.content_node.all('#other-subscriptions').size());
820+ }, 50);
821+ },
822+
823+ test_reference_substitutions: function() {
824+ // References of the form `subscription-cache-reference-*` get
825+ // replaced with LP.cache[...] values.
826+ this.config.subscription_info = {
827+ reference: 'subscription-cache-reference-X',
828+ direct: _constructCategory(),
829+ from_duplicate: _constructCategory(),
830+ as_assignee: _constructCategory(),
831+ as_owner: _constructCategory(),
832+ bug_id: 1,
833+ count: 0
834+ };
835+ window.LP = {
836+ cache: {
837+ 'subscription-cache-reference-X': 'value'
838+ }
839+ };
840+ module.show_subscription_description(this.config);
841+ Y.Assert.areEqual(
842+ 'value',
843+ this.config.subscription_info.reference);
844+ },
845+
846+}));
847+
848 var handle_complete = function(data) {
849 status_node = Y.Node.create(
850 '<p id="complete">Test status: complete</p>');
851
852=== modified file 'lib/lp/bugs/model/personsubscriptioninfo.py'
853--- lib/lp/bugs/model/personsubscriptioninfo.py 2011-04-08 13:40:12 +0000
854+++ lib/lp/bugs/model/personsubscriptioninfo.py 2011-04-13 16:02:16 +0000
855@@ -297,7 +297,8 @@
856 'as_owner': as_owner,
857 'as_assignee': as_assignee,
858 'count': self.count,
859- 'muted': self.muted
860+ 'muted': self.muted,
861+ 'bug_id': self.bug.id,
862 }
863 for category, collection in ((as_owner, self.as_owner),
864 (as_assignee, self.as_assignee)):
865
866=== modified file 'lib/lp/bugs/model/tests/test_personsubscriptioninfo.py'
867--- lib/lp/bugs/model/tests/test_personsubscriptioninfo.py 2011-04-07 21:31:29 +0000
868+++ lib/lp/bugs/model/tests/test_personsubscriptioninfo.py 2011-04-13 16:02:16 +0000
869@@ -110,6 +110,7 @@
870 self.assertEqual(subscriptions['from_duplicate']['count'], 0)
871 self.assertEqual(subscriptions['as_owner']['count'], 0)
872 self.assertEqual(subscriptions['as_assignee']['count'], 0)
873+ self.assertEqual(subscriptions['bug_id'], self.bug.id)
874
875 def test_assignee(self):
876 with person_logged_in(self.subscriber):
877
878=== modified file 'lib/lp/bugs/templates/bug-subscription-list.pt'
879--- lib/lp/bugs/templates/bug-subscription-list.pt 2011-04-08 10:35:12 +0000
880+++ lib/lp/bugs/templates/bug-subscription-list.pt 2011-04-13 16:02:16 +0000
881@@ -22,8 +22,8 @@
882 Y.on('domready', function() {
883 ss_module.setup_bug_subscriptions(
884 {content_box: "#structural-subscription-content-box"});
885- console.log(info_module.get_subscription_reason(
886- {description_box: "#description"}));
887+ info_module.show_subscription_description(
888+ {description_box: "#subscription-description-box"});
889 });
890 });
891 </script>
892@@ -35,6 +35,7 @@
893
894 <div id="maincontent">
895 <div id="nonportlets" class="readable">
896+ <div id="subscription-description-box"></div>
897 <div id="subscription-listing"></div>
898
899 <div id="structural-subscription-content-box"></div>