Merge lp:~abentley/launchpad/mustache-bugs into lp:launchpad

Proposed by Aaron Bentley
Status: Merged
Merged at revision: 14193
Proposed branch: lp:~abentley/launchpad/mustache-bugs
Merge into: lp:launchpad
Prerequisite: lp:~abentley/launchpad/support-mustache
Diff against target: 450 lines (+328/-2)
8 files modified
lib/lp/bugs/browser/bugtask.py (+44/-0)
lib/lp/bugs/browser/tests/test_bugtask.py (+136/-1)
lib/lp/bugs/javascript/buglisting.js (+23/-0)
lib/lp/bugs/javascript/tests/test_buglisting.html (+32/-0)
lib/lp/bugs/javascript/tests/test_buglisting.js (+58/-0)
lib/lp/bugs/templates/buglisting-default.pt (+3/-1)
lib/lp/bugs/templates/buglisting.mustache (+25/-0)
lib/lp/bugs/templates/bugs-listing-table-without-navlinks.pt (+7/-0)
To merge this branch: bzr merge lp:~abentley/launchpad/mustache-bugs
Reviewer Review Type Date Requested Status
Benji York (community) code Approve
Review via email: mp+79441@code.launchpad.net

Commit message

Implement new bug listing style on server-side and client-side.

Description of the change

= Summary =
Fix bug 874595: bug listings need to follow new style

== Proposed fix ==
Use Mustache to render new bugs client-side and server-side.
Screenshot: http://people.canonical.com/~abentley/mustache-listings.png

== Pre-implementation notes ==
Discussed with deryck

== Implementation details ==
This is hidden behind a feature flag (bugs.dynamic_bug_listings.enabled). No change to behaviour or performance is expected while the flag is off.

The Mustache template is provided as a separate file, which is loaded by the view class, and provided in JSON form as LP.mustache_listings.

The data to be rendered is provided as LP.cache.mustache_model.

== Tests ==
bin/test -t buglisting -t test_bugtask

== Demo and Q/A ==
Enable the flag. Go to +bugs for a given project or source package. You should see a display similar to the screenshot above.

= Launchpad lint =

Checking for conflicts and issues in changed files.

Linting changed files:
  lib/lp/bugs/javascript/tests/test_buglisting.html
  lib/lp/bugs/javascript/buglisting.js
  versions.cfg
  setup.py
  utilities/find-changed-files.sh
  lib/lp/bugs/templates/buglisting-default.pt
  lib/lp/bugs/browser/tests/test_bugtask.py
  lib/lp/bugs/browser/bugtask.py
  lib/lp/bugs/templates/buglisting.mustache
  utilities/sourcedeps.conf
  utilities/sourcedeps.cache
  lib/lp/bugs/templates/bugs-listing-table-without-navlinks.pt
  lib/lp/bugs/javascript/tests/test_buglisting.js

To post a comment you must log in.
Revision history for this message
Aaron Bentley (abentley) wrote :
Revision history for this message
Benji York (benji) wrote :

Overall this branch looks good. I look forward to using mustache.

I suspect you want to remove the "<em>Server-side mustache</em>" and
"<em>Client-side mustache</em>" bits as well as making the client/server
rendering conditional on the feature flag.

review: Approve (code)
Revision history for this message
Aaron Bentley (abentley) wrote :

The rendering is already conditional on the feature flag. When this feature is more complete, we will want to switch completely to one rendering, but doing that now would prevent us from QAing either the client-side or the server-side renderings.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/bugs/browser/bugtask.py'
--- lib/lp/bugs/browser/bugtask.py 2011-10-19 14:54:07 +0000
+++ lib/lp/bugs/browser/bugtask.py 2011-10-20 19:36:35 +0000
@@ -53,6 +53,7 @@
53 log,53 log,
54 )54 )
55from operator import attrgetter55from operator import attrgetter
56import os.path
56import re57import re
57import transaction58import transaction
58import urllib59import urllib
@@ -74,6 +75,7 @@
74 )75 )
75from lazr.restful.utils import smartquote76from lazr.restful.utils import smartquote
76from lazr.uri import URI77from lazr.uri import URI
78import pystache
77from pytz import utc79from pytz import utc
78from simplejson import dumps80from simplejson import dumps
79from z3c.pt.pagetemplate import ViewPageTemplateFile81from z3c.pt.pagetemplate import ViewPageTemplateFile
@@ -2137,6 +2139,25 @@
2137 """Returns the bug heat flames HTML."""2139 """Returns the bug heat flames HTML."""
2138 return bugtask_heat_html(self.bugtask, target=self.target_context)2140 return bugtask_heat_html(self.bugtask, target=self.target_context)
21392141
2142 @property
2143 def model(self):
2144 """Provide flattened data about bugtask for simple templaters."""
2145 badges = getAdapter(self.bugtask, IPathAdapter, 'image').badges()
2146 target_image = getAdapter(self.target, IPathAdapter, 'image')
2147 return {
2148 'importance': self.importance.title,
2149 'importance_class': 'importance' + self.importance.name,
2150 'status': self.status.title,
2151 'status_class': 'status' + self.status.name,
2152 'title': self.bug.title,
2153 'id': self.bug.id,
2154 'bug_url': canonical_url(self.bugtask),
2155 'bugtarget': self.bugtargetdisplayname,
2156 'bugtarget_css': target_image.sprite_css(),
2157 'bug_heat_html': self.bug_heat_html,
2158 'badges': badges,
2159 }
2160
21402161
2141class BugListingBatchNavigator(TableBatchNavigator):2162class BugListingBatchNavigator(TableBatchNavigator):
2142 """A specialised batch navigator to load smartly extra bug information."""2163 """A specialised batch navigator to load smartly extra bug information."""
@@ -2178,6 +2199,26 @@
2178 """Return a decorated list of visible bug tasks."""2199 """Return a decorated list of visible bug tasks."""
2179 return [self._getListingItem(bugtask) for bugtask in self.batch]2200 return [self._getListingItem(bugtask) for bugtask in self.batch]
21802201
2202 @cachedproperty
2203 def mustache_template(self):
2204 template_path = os.path.join(
2205 config.root, 'lib/lp/bugs/templates/buglisting.mustache')
2206 with open(template_path) as template_file:
2207 return template_file.read()
2208
2209 @property
2210 def mustache_listings(self):
2211 return 'LP.mustache_listings = %s;' % dumps(self.mustache_template)
2212
2213 @property
2214 def mustache(self):
2215 return pystache.render(self.mustache_template, self.model)
2216
2217 @property
2218 def model(self):
2219 bugtasks = [bugtask.model for bugtask in self.getBugListingItems()]
2220 return {'bugtasks': bugtasks}
2221
21812222
2182class NominatedBugReviewAction(EnumeratedType):2223class NominatedBugReviewAction(EnumeratedType):
2183 """Enumeration for nomination review actions"""2224 """Enumeration for nomination review actions"""
@@ -2436,6 +2477,9 @@
24362477
2437 expose_structural_subscription_data_to_js(2478 expose_structural_subscription_data_to_js(
2438 self.context, self.request, self.user)2479 self.context, self.request, self.user)
2480 if getFeatureFlag('bugs.dynamic_bug_listings.enabled'):
2481 cache = IJSONRequestCache(self.request)
2482 cache.objects['mustache_model'] = self.search().model
24392483
2440 @property2484 @property
2441 def columns_to_show(self):2485 def columns_to_show(self):
24422486
=== modified file 'lib/lp/bugs/browser/tests/test_bugtask.py'
--- lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-05 18:02:45 +0000
+++ lib/lp/bugs/browser/tests/test_bugtask.py 2011-10-20 19:36:35 +0000
@@ -3,13 +3,20 @@
33
4__metaclass__ = type4__metaclass__ = type
55
6from contextlib import contextmanager
6from datetime import datetime7from datetime import datetime
8import re
79
8from lazr.lifecycle.event import ObjectModifiedEvent10from lazr.lifecycle.event import ObjectModifiedEvent
11from lazr.restful.interfaces import IJSONRequestCache
9from lazr.lifecycle.snapshot import Snapshot12from lazr.lifecycle.snapshot import Snapshot
10from pytz import UTC13from pytz import UTC
14import soupmatchers
11from storm.store import Store15from storm.store import Store
12from testtools.matchers import LessThan16from testtools.matchers import (
17 LessThan,
18 Not,
19 )
13import transaction20import transaction
14from zope.component import (21from zope.component import (
15 getMultiAdapter,22 getMultiAdapter,
@@ -38,7 +45,9 @@
38from lp.bugs.browser.bugtask import (45from lp.bugs.browser.bugtask import (
39 BugActivityItem,46 BugActivityItem,
40 BugTaskEditView,47 BugTaskEditView,
48 BugTaskListingItem,
41 BugTasksAndNominationsView,49 BugTasksAndNominationsView,
50 BugTaskSearchListingView,
42 )51 )
43from lp.bugs.interfaces.bugactivity import IBugActivitySet52from lp.bugs.interfaces.bugactivity import IBugActivitySet
44from lp.bugs.interfaces.bugnomination import IBugNomination53from lp.bugs.interfaces.bugnomination import IBugNomination
@@ -54,8 +63,11 @@
54from lp.services.propertycache import get_property_cache63from lp.services.propertycache import get_property_cache
55from lp.soyuz.interfaces.component import IComponentSet64from lp.soyuz.interfaces.component import IComponentSet
56from lp.testing import (65from lp.testing import (
66 BrowserTestCase,
57 celebrity_logged_in,67 celebrity_logged_in,
68 feature_flags,
58 person_logged_in,69 person_logged_in,
70 set_feature_flag,
59 TestCaseWithFactory,71 TestCaseWithFactory,
60 )72 )
61from lp.testing._webservice import QueryCollector73from lp.testing._webservice import QueryCollector
@@ -1194,3 +1206,126 @@
1194 self._assertThatUnbatchedAndBatchedActivityMatch(1206 self._assertThatUnbatchedAndBatchedActivityMatch(
1195 unbatched_view.activity_and_comments[4:],1207 unbatched_view.activity_and_comments[4:],
1196 batched_view.activity_and_comments)1208 batched_view.activity_and_comments)
1209
1210
1211def make_bug_task_listing_item(factory):
1212 owner = factory.makePerson()
1213 bug = factory.makeBug(
1214 owner=owner, private=True, security_related=True)
1215 bugtask = factory.makeBugTask(bug)
1216 bug_task_set = getUtility(IBugTaskSet)
1217 bug_badge_properties = bug_task_set.getBugTaskBadgeProperties(
1218 [bugtask])
1219 badge_property = bug_badge_properties[bugtask]
1220 return owner, BugTaskListingItem(
1221 bugtask,
1222 badge_property['has_branch'],
1223 badge_property['has_specification'],
1224 badge_property['has_patch'],
1225 target_context=bugtask.target)
1226
1227
1228class TestBugTaskSearchListingView(BrowserTestCase):
1229
1230 layer = DatabaseFunctionalLayer
1231
1232 server_listing = soupmatchers.Tag(
1233 'Server', 'em', text='Server-side mustache')
1234
1235 client_listing = soupmatchers.Tag(
1236 'client-listing', True, attrs={'id': 'client-listing'})
1237
1238 def makeView(self, bugtask=None):
1239 request = LaunchpadTestRequest()
1240 if bugtask is None:
1241 bugtask = self.factory.makeBugTask()
1242 view = BugTaskSearchListingView(bugtask.target, request)
1243 view.initialize()
1244 return view
1245
1246 @contextmanager
1247 def dynamic_listings(self):
1248 with feature_flags():
1249 set_feature_flag(u'bugs.dynamic_bug_listings.enabled', u'on')
1250 yield
1251
1252 def test_mustache_model_missing_if_no_flag(self):
1253 """The IJSONRequestCache should contain mustache_model."""
1254 view = self.makeView()
1255 cache = IJSONRequestCache(view.request)
1256 self.assertIs(None, cache.objects.get('mustache_model'))
1257
1258 def test_mustache_model_in_json(self):
1259 """The IJSONRequestCache should contain mustache_model.
1260
1261 mustache_model should contain bugtasks, the BugTaskListingItem.model
1262 for each BugTask.
1263 """
1264 owner, item = make_bug_task_listing_item(self.factory)
1265 self.useContext(person_logged_in(owner))
1266 with self.dynamic_listings():
1267 view = self.makeView(item.bugtask)
1268 cache = IJSONRequestCache(view.request)
1269 bugtasks = cache.objects['mustache_model']['bugtasks']
1270 self.assertEqual(1, len(bugtasks))
1271 self.assertEqual(item.model, bugtasks[0])
1272
1273 def getBugtaskBrowser(self):
1274 bugtask = self.factory.makeBugTask()
1275 with person_logged_in(bugtask.target.owner):
1276 bugtask.target.official_malone = True
1277 browser = self.getViewBrowser(
1278 bugtask.target, '+bugs', rootsite='bugs')
1279 return bugtask, browser
1280
1281 def assertHTML(self, browser, *tags, **kwargs):
1282 matcher = soupmatchers.HTMLContains(*tags)
1283 if kwargs.get('invert', False):
1284 matcher = Not(matcher)
1285 self.assertThat(browser.contents, matcher)
1286
1287 @staticmethod
1288 def getBugNumberTag(bug_task):
1289 """Bug numbers with a leading hash are unique to new rendering."""
1290 bug_number_re = re.compile(r'\#%d' % bug_task.bug.id)
1291 return soupmatchers.Tag('bug_number', 'td', text=bug_number_re)
1292
1293 def test_mustache_rendering_missing_if_no_flag(self):
1294 """If the flag is missing, then no mustache features appear."""
1295 bug_task, browser = self.getBugtaskBrowser()
1296 number_tag = self.getBugNumberTag(bug_task)
1297 self.assertHTML(browser, number_tag, invert=True)
1298 self.assertHTML(browser, self.client_listing, invert=True)
1299 self.assertHTML(browser, self.server_listing, invert=True)
1300
1301 def test_mustache_rendering(self):
1302 """If the flag is present, then all mustache features appear."""
1303 with self.dynamic_listings():
1304 bug_task, browser = self.getBugtaskBrowser()
1305 bug_number = self.getBugNumberTag(bug_task)
1306 self.assertHTML(
1307 browser, self.client_listing, self.server_listing, bug_number)
1308
1309
1310class TestBugTaskListingItem(TestCaseWithFactory):
1311
1312 layer = DatabaseFunctionalLayer
1313
1314 def test_model(self):
1315 """Model contains expected fields with expected values."""
1316 owner, item = make_bug_task_listing_item(self.factory)
1317 with person_logged_in(owner):
1318 model = item.model
1319 self.assertEqual('Undecided', model['importance'])
1320 self.assertEqual('importanceUNDECIDED', model['importance_class'])
1321 self.assertEqual('New', model['status'])
1322 self.assertEqual('statusNEW', model['status_class'])
1323 self.assertEqual(item.bug.title, model['title'])
1324 self.assertEqual(item.bug.id, model['id'])
1325 self.assertEqual(canonical_url(item.bugtask), model['bug_url'])
1326 self.assertEqual(item.bugtargetdisplayname, model['bugtarget'])
1327 self.assertEqual('sprite product', model['bugtarget_css'])
1328 self.assertEqual(item.bug_heat_html, model['bug_heat_html'])
1329 self.assertEqual(
1330 '<span alt="private" title="Private" class="sprite private">'
1331 '&nbsp;</span>', model['badges'])
11971332
=== added file 'lib/lp/bugs/javascript/buglisting.js'
--- lib/lp/bugs/javascript/buglisting.js 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/javascript/buglisting.js 2011-10-20 19:36:35 +0000
@@ -0,0 +1,23 @@
1/* Copyright 2011 Canonical Ltd. This software is licensed under the
2 * GNU Affero General Public License version 3 (see the file LICENSE).
3 *
4 * Client-side rendering of bug listings.
5 *
6 * @module bugs
7 * @submodule buglisting
8 */
9
10YUI.add('lp.bugs.buglisting', function(Y) {
11
12var namespace = Y.namespace('lp.bugs.buglisting');
13
14namespace.rendertable = function(){
15 client_listing = Y.one('#client-listing');
16 if (client_listing === null){
17 return;
18 }
19 var txt = Mustache.to_html(LP.mustache_listings, LP.cache.mustache_model);
20 client_listing.set('innerHTML', txt);
21};
22
23}, "0.1", {"requires": ["node"]});
024
=== added file 'lib/lp/bugs/javascript/tests/test_buglisting.html'
--- lib/lp/bugs/javascript/tests/test_buglisting.html 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/javascript/tests/test_buglisting.html 2011-10-20 19:36:35 +0000
@@ -0,0 +1,32 @@
1<html>
2 <head>
3 <title>Bug task listing</title>
4
5 <!-- YUI and test setup -->
6 <script type="text/javascript"
7 src="../../../../canonical/launchpad/icing/yui/yui/yui.js">
8 </script>
9 <link rel="stylesheet" href="../../../app/javascript/testing/test.css" />
10 <script type="text/javascript"
11 src="../../../app/javascript/testing/testrunner.js"></script>
12
13 <script type="text/javascript"
14 src="../../../contrib/javascript/mustache.js"></script>
15
16 <!-- The module under test -->
17 <script type="text/javascript"
18 src="../buglisting.js"></script>
19
20 <!-- The test suite -->
21 <script type="text/javascript"
22 src="test_buglisting.js"></script>
23
24 <!-- Pretty up the sample html -->
25 <style type="text/css">
26 div#sample {margin:15px; width:200px; border:1px solid #999; padding:10px;}
27 </style>
28 </head>
29 <body class="yui3-skin-sam">
30 <div id="fixture"></div>
31 </body>
32</html>
033
=== added file 'lib/lp/bugs/javascript/tests/test_buglisting.js'
--- lib/lp/bugs/javascript/tests/test_buglisting.js 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/javascript/tests/test_buglisting.js 2011-10-20 19:36:35 +0000
@@ -0,0 +1,58 @@
1YUI({
2 base: '../../../../canonical/launchpad/icing/yui/',
3 filter: 'raw', combine: false, fetchCSS: false
4 }).use('test', 'console', 'lp.bugs.buglisting', function(Y) {
5
6var suite = new Y.Test.Suite("lp.bugs.buglisting Tests");
7var module = Y.lp.bugs.buglisting;
8
9/**
10 * Test is_notification_level_shown() for a given set of
11 * conditions.
12 */
13suite.add(new Y.Test.Case({
14 name: 'rendertable',
15
16 setUp: function () {
17 this.MY_NAME = "ME";
18 window.LP = { links: { me: "/~" + this.MY_NAME } };
19 },
20
21 tearDown: function() {
22 delete window.LP;
23 this.set_fixture('');
24 },
25
26 set_fixture: function(value) {
27 var fixture = Y.one('#fixture');
28 fixture.set('innerHTML', value);
29 },
30 test_rendertable_no_client_listing: function() {
31 module.rendertable();
32 },
33 test_rendertable: function() {
34 this.set_fixture('<div id="client-listing"></div>');
35 window.LP.cache = {
36 mustache_model: {
37 foo: 'bar'
38 }
39 };
40 window.LP.mustache_listings = "{{foo}}";
41 module.rendertable();
42 Y.Assert.areEqual('bar', Y.one('#client-listing').get('innerHTML'));
43 }
44}));
45
46var handle_complete = function(data) {
47 window.status = '::::' + JSON.stringify(data);
48 };
49Y.Test.Runner.on('complete', handle_complete);
50Y.Test.Runner.add(suite);
51
52var console = new Y.Console({newestOnTop: false});
53console.render('#log');
54
55Y.on('domready', function() {
56 Y.Test.Runner.run();
57});
58});
059
=== modified file 'lib/lp/bugs/templates/buglisting-default.pt'
--- lib/lp/bugs/templates/buglisting-default.pt 2011-06-22 14:09:43 +0000
+++ lib/lp/bugs/templates/buglisting-default.pt 2011-10-20 19:36:35 +0000
@@ -18,10 +18,12 @@
18 </tal:comment>18 </tal:comment>
19 <script type="text/javascript"19 <script type="text/javascript"
20 tal:condition="not: view/shouldShowAdvancedForm">20 tal:condition="not: view/shouldShowAdvancedForm">
21 LPS.use('lp.registry.structural_subscription', function(Y) {21 LPS.use('lp.registry.structural_subscription', 'lp.bugs.buglisting',
22 function(Y) {
22 Y.on('domready', function() {23 Y.on('domready', function() {
23 Y.lp.registry.structural_subscription.setup(24 Y.lp.registry.structural_subscription.setup(
24 {content_box: "#structural-subscription-content-box"});25 {content_box: "#structural-subscription-content-box"});
26 Y.lp.bugs.buglisting.rendertable()
25 });27 });
26 });28 });
27 </script>29 </script>
2830
=== added file 'lib/lp/bugs/templates/buglisting.mustache'
--- lib/lp/bugs/templates/buglisting.mustache 1970-01-01 00:00:00 +0000
+++ lib/lp/bugs/templates/buglisting.mustache 2011-10-20 19:36:35 +0000
@@ -0,0 +1,25 @@
1<table class="listing narrow-listing">
2<tbody>
3{{#bugtasks}}
4 <tr>
5 <td class={{importance_class}}>
6 {{importance}}
7 </td>
8 <td>
9 #{{id}} <a href="{{bug_url}}">{{title}}</a>
10 </td>
11 <td align="right">
12 {{{badges}}}{{{bug_heat_html}}}
13 </td>
14 </tr>
15 <tr >
16 <td class={{status_class}} style="border-style: none">
17 {{status}}
18 </td>
19 <td colspan="2" style="border-style: none">
20 <span class="{{bugtarget_css}}">{{bugtarget}}</span>
21 </td>
22 </tr>
23{{/bugtasks}}
24</tbody>
25</table>
026
=== modified file 'lib/lp/bugs/templates/bugs-listing-table-without-navlinks.pt'
--- lib/lp/bugs/templates/bugs-listing-table-without-navlinks.pt 2011-05-27 18:10:50 +0000
+++ lib/lp/bugs/templates/bugs-listing-table-without-navlinks.pt 2011-10-20 19:36:35 +0000
@@ -76,5 +76,12 @@
76 </tr>76 </tr>
77 </tbody>77 </tbody>
78 </table>78 </table>
79 <tal:mustache condition="request/features/bugs.dynamic_bug_listings.enabled">
80 <em>Server-side mustache</em>
81 <span tal:replace="structure context/mustache" />
82 <em>Client-side mustache</em>
83 <span id="client-listing" />
84 <script tal:content="structure context/mustache_listings" />
85 </tal:mustache>
79</tal:results>86</tal:results>
80</tal:root>87</tal:root>