Merge lp:~michael.nelson/ubuntu-webcatalog/788207-display-reviews-non-js into lp:ubuntu-webcatalog

Proposed by Michael Nelson
Status: Merged
Approved by: Łukasz Czyżykowski
Approved revision: 60
Merged at revision: 52
Proposed branch: lp:~michael.nelson/ubuntu-webcatalog/788207-display-reviews-non-js
Merge into: lp:ubuntu-webcatalog
Diff against target: 486 lines (+188/-29)
18 files modified
.bzrignore (+1/-0)
django_project/config/main.cfg (+1/-0)
src/webcatalog/management/commands/import_for_purchase_apps.py (+0/-2)
src/webcatalog/management/commands/import_ratings_stats.py (+6/-5)
src/webcatalog/schema.py (+1/-0)
src/webcatalog/static/css/webcatalog.css (+22/-1)
src/webcatalog/templates/webcatalog/application_detail.html (+1/-1)
src/webcatalog/templates/webcatalog/application_overview_snippet.html (+1/-1)
src/webcatalog/templates/webcatalog/application_review_list.html (+39/-0)
src/webcatalog/templates/webcatalog/breadcrumbs_snippet.html (+1/-1)
src/webcatalog/templates/webcatalog/rating_summary.html (+4/-2)
src/webcatalog/templatetags/webcatalog.py (+6/-6)
src/webcatalog/tests/test_commands.py (+3/-3)
src/webcatalog/tests/test_templatetags.py (+1/-4)
src/webcatalog/tests/test_views.py (+64/-0)
src/webcatalog/urls.py (+4/-0)
src/webcatalog/utilities.py (+11/-0)
src/webcatalog/views.py (+22/-3)
To merge this branch: bzr merge lp:~michael.nelson/ubuntu-webcatalog/788207-display-reviews-non-js
Reviewer Review Type Date Requested Status
Łukasz Czyżykowski Pending
Review via email: mp+75527@code.launchpad.net

Commit message

Provide a non-js view displaying reviews for an app.

Description of the change

Overview
========

This branch is the first step towards loading reviews live into the app details page (bug 788207).

It provides a non-js page displaying the first page of reviews for an app.

The next branches will:
 * Add a custom paginator object for use on the reviews page,
 * Extract just the snippet with the reviews to a separate template, which can then be rendered on its own when requested via JS.
 * Add the JS to progressively enhance the details page (without js, just a link to reviews, but js will replace that with actual reviews).

To test: `fab bootstrap && fab test`

Screenshot on the bug.

I also updated the import ratings stats management command so that we pull stats for all origins (as currenly we only support ubuntu and commercial PPAs afaik), as otherwise we don't get ratings for commercial apps.

To post a comment you must log in.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-08-15 11:13:35 +0000
3+++ .bzrignore 2011-09-15 11:41:49 +0000
4@@ -12,3 +12,4 @@
5 sourcecode/django/
6 django_project/rnrclient.py
7 django_project/adminaudit
8+django_project/local.cfg
9
10=== modified file 'django_project/config/main.cfg'
11--- django_project/config/main.cfg 2011-09-12 18:51:47 +0000
12+++ django_project/config/main.cfg 2011-09-15 11:41:49 +0000
13@@ -99,6 +99,7 @@
14
15 [rnr]
16 rnr_service_root = http://reviews.ubuntu.com/reviews/api/1.0
17+reviews_cache_timeout = 900
18
19 [sso_api]
20 sso_api_service_root = https://login.staging.ubuntu.com/api/1.0
21
22=== modified file 'src/webcatalog/management/commands/import_for_purchase_apps.py'
23--- src/webcatalog/management/commands/import_for_purchase_apps.py 2011-09-12 13:37:24 +0000
24+++ src/webcatalog/management/commands/import_for_purchase_apps.py 2011-09-15 11:41:49 +0000
25@@ -99,5 +99,3 @@
26 self.stdout.write(message)
27 if flush:
28 self.stdout.flush()
29-
30-
31\ No newline at end of file
32
33=== modified file 'src/webcatalog/management/commands/import_ratings_stats.py'
34--- src/webcatalog/management/commands/import_ratings_stats.py 2011-09-12 13:37:24 +0000
35+++ src/webcatalog/management/commands/import_ratings_stats.py 2011-09-15 11:41:49 +0000
36@@ -30,13 +30,12 @@
37 from django.conf import settings
38 from django.core.management.base import LabelCommand
39
40-from rnrclient import RatingsAndReviewsAPI
41-
42 from webcatalog.models import (
43 Application,
44 DistroSeries,
45 ReviewStatsImport,
46 )
47+from webcatalog.utilities import WebServices
48
49
50 class Command(LabelCommand):
51@@ -93,8 +92,10 @@
52 elif time_since_last_import < timedelta(days=7):
53 num_days = 7
54
55- rnr_api = RatingsAndReviewsAPI(service_root=settings.RNR_SERVICE_ROOT)
56- kwargs = dict(origin='ubuntu', distroseries=distroseries.code_name)
57+ # Currently we have ratings in rnr for ubuntu and for-purchase
58+ # PPAs only, so we set origin to any. If we later support reviews
59+ # for general PPAs, we will need to update this.
60+ kwargs = dict(origin='any', distroseries=distroseries.code_name)
61 if num_days:
62 kwargs['days'] = num_days
63- return rnr_api.review_stats(**kwargs)
64+ return WebServices().rnr_api.review_stats(**kwargs)
65
66=== modified file 'src/webcatalog/schema.py'
67--- src/webcatalog/schema.py 2011-09-12 18:44:56 +0000
68+++ src/webcatalog/schema.py 2011-09-15 11:41:49 +0000
69@@ -75,6 +75,7 @@
70 rnr = ConfigSection()
71 rnr.rnr_service_root = StringConfigOption(
72 default="http://reviews.ubuntu.com/reviews/api/1.0")
73+ rnr.reviews_cache_timeout = IntConfigOption(default=60*15)
74
75 sso_api = ConfigSection()
76 sso_api.sso_api_service_root = StringConfigOption()
77
78=== modified file 'src/webcatalog/static/css/webcatalog.css'
79--- src/webcatalog/static/css/webcatalog.css 2011-08-15 11:34:36 +0000
80+++ src/webcatalog/static/css/webcatalog.css 2011-09-15 11:41:49 +0000
81@@ -206,9 +206,12 @@
82 width: 64px;
83 height: 64px;
84 }
85-.star-rating {
86+.star-rating.small {
87 width: 100px;
88+}
89+.star-rating.average {
90 float: right;
91+ display: block;
92 }
93 .star-rating.large {
94 width: 175px;
95@@ -217,6 +220,24 @@
96 margin: 0;
97 float: none;
98 }
99+.review {
100+ margin: 8px 8px 16px 8px;
101+}
102+.review .star-rating {
103+ display: inline;
104+ margin-right: 8px;
105+}
106+.review p {
107+ margin-top: 10px;
108+ font-size: 14px;
109+}
110+.review .meta {
111+ float: right;
112+ color: #adaaa6;
113+}
114+.review p.usefulness {
115+ font-size: 12px;
116+}
117
118 /* Breadcrumbs styles */
119 #breadcrumbs {
120
121=== modified file 'src/webcatalog/templates/webcatalog/application_detail.html'
122--- src/webcatalog/templates/webcatalog/application_detail.html 2011-08-15 11:28:26 +0000
123+++ src/webcatalog/templates/webcatalog/application_detail.html 2011-09-15 11:41:49 +0000
124@@ -24,7 +24,7 @@
125 </div>
126 <div id="sc-mockup">
127 <div class="header">
128- {% rating_summary application 'large' %}
129+ {% rating_summary application.ratings_average 'large' application.ratings_total %}
130 {% if application.icon %}
131 <img class="icon64" src="{{ application.icon.url }}"/>
132 {% else %}
133
134=== modified file 'src/webcatalog/templates/webcatalog/application_overview_snippet.html'
135--- src/webcatalog/templates/webcatalog/application_overview_snippet.html 2011-07-21 11:31:10 +0000
136+++ src/webcatalog/templates/webcatalog/application_overview_snippet.html 2011-09-15 11:41:49 +0000
137@@ -16,7 +16,7 @@
138 <p>{{ app.comment }}</p>
139 </td>
140 <td class="app-stats">
141- {% rating_summary app %}
142+ {% rating_summary app.ratings_average 'small' app.ratings_total %}
143 </td>
144 </tr>
145 {% endfor %}
146
147=== added file 'src/webcatalog/templates/webcatalog/application_review_list.html'
148--- src/webcatalog/templates/webcatalog/application_review_list.html 1970-01-01 00:00:00 +0000
149+++ src/webcatalog/templates/webcatalog/application_review_list.html 2011-09-15 11:41:49 +0000
150@@ -0,0 +1,39 @@
151+{% extends "webcatalog/base.html" %}
152+{% load i18n %}
153+{% load webcatalog %}
154+
155+{% block title %}Reviews for {{ application.name }}{% endblock %}
156+{% block header %}Reviews for for {{ application.name }}{% endblock %}
157+
158+{% block content %}
159+ {% include "webcatalog/breadcrumbs_snippet.html" %}
160+ <div id="sc-mockup">
161+ <div class="header">
162+ {% rating_summary application.ratings_average 'large' application.ratings_total %}
163+ {% if application.icon %}
164+ <img class="icon64" src="{{ application.icon.url }}"/>
165+ {% else %}
166+ <img class="icon64" src="{{ STATIC_URL }}images/applications-other-64.png"/>
167+ {% endif %}
168+
169+ <h2>{{ application.name }}</h2>
170+ <p>{{ application.comment }}</p>
171+ </div>
172+ {% for review in reviews %}
173+ <div class="review">
174+ <div class="meta">
175+ <strong>{{ review.reviewer_displayname }}</strong>,
176+ {{ review.date_created|slice:":10" }}
177+ </div>
178+ {% rating_summary review.rating %}
179+ <strong>{{ review.summary }}</strong>
180+ <p>{{ review.review_text }}</p>
181+ <p class="usefulness">
182+ {% blocktrans with favorable=review.usefulness_favorable total=review.usefulness_total %}
183+ {{ favorable }} out of {{ total }} found this review helpful.
184+ {% endblocktrans %}
185+ </p>
186+ </div>
187+ {% endfor %}
188+ </div>
189+{% endblock %}
190
191=== modified file 'src/webcatalog/templates/webcatalog/breadcrumbs_snippet.html'
192--- src/webcatalog/templates/webcatalog/breadcrumbs_snippet.html 2011-06-23 14:20:52 +0000
193+++ src/webcatalog/templates/webcatalog/breadcrumbs_snippet.html 2011-09-15 11:41:49 +0000
194@@ -3,4 +3,4 @@
195 <li class="crumb {% if forloop.first %} first{% endif %}{% if forloop.last %} last{% endif %}">
196 <a href="{{ crumb.url }}">{{ crumb.name }}</a></li>
197 {% endfor %}
198-</ul>
199\ No newline at end of file
200+</ul>
201
202=== modified file 'src/webcatalog/templates/webcatalog/rating_summary.html'
203--- src/webcatalog/templates/webcatalog/rating_summary.html 2011-08-15 11:34:36 +0000
204+++ src/webcatalog/templates/webcatalog/rating_summary.html 2011-09-15 11:41:49 +0000
205@@ -1,9 +1,11 @@
206 {% load i18n %}
207-{% if total %}
208-<div class="star-rating {{ size }}">
209+{% if stars %}
210+<div class="star-rating {{ size }}{% if total %} average{% endif %}">
211 {% for star in stars %}
212 <img src="{{ STATIC_URL }}images/star-{{ size }}-{{ star }}.png" />
213 {% endfor %}
214+ {% if total %}
215 <p>{% blocktrans %}{{ total }} Ratings{% endblocktrans %}</p>
216+ {% endif %}
217 </div>
218 {% endif %}
219
220=== modified file 'src/webcatalog/templatetags/webcatalog.py'
221--- src/webcatalog/templatetags/webcatalog.py 2011-09-12 13:58:47 +0000
222+++ src/webcatalog/templatetags/webcatalog.py 2011-09-15 11:41:49 +0000
223@@ -183,19 +183,19 @@
224
225
226 @register.inclusion_tag("webcatalog/rating_summary.html")
227-def rating_summary(app, size='small'):
228+def rating_summary(num_stars, size='small', num_ratings=None):
229 stars = []
230- if not app.ratings_average:
231+ if not num_stars:
232 return {}
233
234- ratings_average = float(app.ratings_average)
235+ num_stars = float(num_stars)
236 for count in range(1, 6):
237- if ratings_average >= count:
238+ if num_stars >= count:
239 stars.append(1)
240- elif 0.5 >= count - ratings_average > 0:
241+ elif 0.5 >= count - num_stars > 0:
242 stars.append(0.5)
243 else:
244 stars.append(0)
245
246- return dict(stars=stars, total=app.ratings_total,
247+ return dict(stars=stars, total=num_ratings,
248 STATIC_URL=settings.STATIC_URL, size=size)
249
250=== modified file 'src/webcatalog/tests/test_commands.py'
251--- src/webcatalog/tests/test_commands.py 2011-09-12 13:37:24 +0000
252+++ src/webcatalog/tests/test_commands.py 2011-09-15 11:41:49 +0000
253@@ -492,7 +492,7 @@
254 call_command('import_ratings_stats', 'onion')
255
256 self.mock_review_stats.assert_called_with(
257- origin='ubuntu', distroseries='onion')
258+ origin='any', distroseries='onion')
259
260 def test_download_review_stats_with_recent(self):
261 # If there has been a recent import, we'll only grab the
262@@ -505,7 +505,7 @@
263 call_command('import_ratings_stats', 'onion')
264
265 self.mock_review_stats.assert_called_with(
266- origin='ubuntu', distroseries='onion', days=3)
267+ origin='any', distroseries='onion', days=3)
268
269 def test_update_app_stats(self):
270 # Any stats provided in the api call will be used to update our
271@@ -541,4 +541,4 @@
272 # update_apps_with_stats returns None on success:
273 self.assertIsNone(command.update_apps_with_stats(natty, stats))
274
275-
276\ No newline at end of file
277+
278
279=== modified file 'src/webcatalog/tests/test_templatetags.py'
280--- src/webcatalog/tests/test_templatetags.py 2011-09-12 13:37:24 +0000
281+++ src/webcatalog/tests/test_templatetags.py 2011-09-15 11:41:49 +0000
282@@ -296,10 +296,7 @@
283
284 class RatingSummaryTestCase(TestCaseWithFactory):
285 def test_rating_summary(self):
286- app = self.factory.make_application(ratings_average=Decimal('3.5'),
287- ratings_total=234)
288-
289- result = rating_summary(app)
290+ result = rating_summary('3.5', num_ratings=234)
291
292 self.assertEqual([1, 1, 1, 0.5, 0], result['stars'])
293 self.assertEqual(234, result['total'])
294
295=== modified file 'src/webcatalog/tests/test_views.py'
296--- src/webcatalog/tests/test_views.py 2011-09-12 13:37:24 +0000
297+++ src/webcatalog/tests/test_views.py 2011-09-15 11:41:49 +0000
298@@ -28,6 +28,7 @@
299 from django.core.urlresolvers import reverse
300
301 from mock import patch
302+from rnrclient import ReviewDetails
303
304 from webcatalog.models import DistroSeries
305 from webcatalog.tests.factory import TestCaseWithFactory
306@@ -37,6 +38,7 @@
307 __all__ = [
308 'ApplicationDetailNoSeriesTestCase',
309 'ApplicationDetailTestCase',
310+ 'ApplicationReviewsTestCase',
311 'OverviewTestCase',
312 'SearchTestCase',
313 ]
314@@ -504,3 +506,65 @@
315 url = reverse('wc-department', args=['maverick', dept.id])
316 maver_pos = response.content.find('<a href="{0}">Ubuntu'.format(url))
317 self.assertTrue(lucid_pos > maver_pos)
318+
319+
320+class ApplicationReviewsTestCase(TestCaseWithFactory):
321+
322+ def setUp(self):
323+ super(ApplicationReviewsTestCase, self).setUp()
324+ get_reviews_fn = 'rnrclient.RatingsAndReviewsAPI.get_reviews'
325+ self.get_reviews_patcher = patch(get_reviews_fn)
326+ self.mock_get_reviews = self.get_reviews_patcher.start()
327+ self.mock_get_reviews.return_value = []
328+
329+ def tearDown(self):
330+ self.get_reviews_patcher.stop()
331+ super(ApplicationReviewsTestCase, self).tearDown()
332+
333+ def test_only_valid_apps(self):
334+ response = self.client.get(reverse('wc-package-reviews',
335+ args=['jaunty', 'doesntexist']))
336+
337+ self.assertEqual(404, response.status_code)
338+
339+ def test_uncached_calls_to_rnr_api(self):
340+ app = self.factory.make_application()
341+
342+ response = self.client.get(reverse('wc-package-reviews',
343+ args=[app.distroseries.code_name, app.package_name]))
344+
345+ self.assertEqual(1, self.mock_get_reviews.call_count)
346+
347+ def test_second_call_cached(self):
348+ app = self.factory.make_application()
349+
350+ for count in range(2):
351+ response = self.client.get(reverse('wc-package-reviews',
352+ args=[app.distroseries.code_name, app.package_name]))
353+
354+ self.assertEqual(1, self.mock_get_reviews.call_count)
355+
356+ def make_review_details(self, package_name=None, summary=None,
357+ rating='3.5'):
358+ if package_name is None:
359+ package_name = self.factory.get_unique_string(prefix='pkg-')
360+ if summary is None:
361+ summary = self.factory.make_unique_string(prefix='summary-')
362+ return ReviewDetails.from_dict(
363+ dict(package_name=package_name, summary=summary,
364+ rating=rating))
365+
366+ def test_renders_reviews(self):
367+ app = self.factory.make_application()
368+ self.mock_get_reviews.return_value = [
369+ self.make_review_details(summary='review_summary1'),
370+ self.make_review_details(summary='review_summary2'),
371+ ]
372+
373+ response = self.client.get(reverse('wc-package-reviews',
374+ args=[app.distroseries.code_name, app.package_name]))
375+
376+ self.assertTemplateUsed(
377+ response, 'webcatalog/application_review_list.html')
378+ self.assertContains(response, 'review_summary1')
379+ self.assertContains(response, 'review_summary2')
380
381=== modified file 'src/webcatalog/urls.py'
382--- src/webcatalog/urls.py 2011-09-12 13:37:24 +0000
383+++ src/webcatalog/urls.py 2011-09-15 11:41:49 +0000
384@@ -37,8 +37,12 @@
385 name='wc-department'),
386 url(r'^applications/(?P<distro>[-.+\w]+)/(?P<package_name>[-.+\w]+)/$',
387 'application_detail', name="wc-package-detail"),
388+ # The following URL redirects to the series-specific URL above.
389 url(r'^applications/(?P<package_name>[-.+\w]+)/$', 'application_detail',
390 name="wc-package-detail"),
391+ url(r'^applications/(?P<distro>[-.+\w]+)/(?P<package_name>[-.+\w]+)/'
392+ r'reviews/$',
393+ 'application_reviews', name="wc-package-reviews"),
394 url(r'^search/$', 'search', name="wc-search"),
395 url(r'^search/(?P<distro>[-.+\w]+)/$', 'search', name="wc-search"),
396
397
398=== modified file 'src/webcatalog/utilities.py'
399--- src/webcatalog/utilities.py 2011-09-12 13:37:24 +0000
400+++ src/webcatalog/utilities.py 2011-09-15 11:41:49 +0000
401@@ -41,6 +41,9 @@
402 from lazr.restfulclient.resource import ServiceRoot
403 from oauth.oauth import OAuthToken
404
405+from rnrclient import RatingsAndReviewsAPI
406+
407+
408 class WebServiceError(Exception):
409 """Raised when we cannot connect to a web service."""
410
411@@ -87,6 +90,14 @@
412
413 return self._identity_provider
414
415+ @property
416+ def rnr_api(self):
417+ return RatingsAndReviewsAPI(service_root=settings.RNR_SERVICE_ROOT)
418+
419+ def get_reviews_for_package(self, package_name, distroseries, page=1):
420+ return self.rnr_api.get_reviews(packagename=package_name,
421+ distroseries=distroseries, page=page)
422+
423 def _fake_validate_token(self, token, openid_identifier, signature):
424 """ This is a version of validate_token that gets the
425 token_secret, consumer_secret from the plaintext signature.
426
427=== modified file 'src/webcatalog/views.py'
428--- src/webcatalog/views.py 2011-09-12 13:37:24 +0000
429+++ src/webcatalog/views.py 2011-09-15 11:41:49 +0000
430@@ -30,22 +30,27 @@
431 from django.core.urlresolvers import reverse
432 from django.db.models import Q
433 from django.http import HttpResponseRedirect
434-from django.template import RequestContext
435 from django.shortcuts import (
436 get_object_or_404,
437 render_to_response,
438 )
439+from django.template import RequestContext
440+from django.views.decorators.cache import cache_page
441
442 from webcatalog.models import (
443 Application,
444 Department,
445 DistroSeries,
446 )
447-from webcatalog.utilities import UserAgentString
448+from webcatalog.utilities import (
449+ UserAgentString,
450+ WebServices,
451+ )
452
453 __metaclass__ = type
454 __all__ = [
455 'application_detail',
456+ 'application_reviews',
457 'index',
458 'department_overview',
459 'search',
460@@ -129,7 +134,7 @@
461 dept = get_object_or_404(Department, pk=dept_id)
462 subdepts = Department.objects.filter(parent=dept)
463 subdepts = subdepts.order_by('name')
464-
465+
466 apps = Application.objects.filter(departments=dept,
467 distroseries__code_name=distro)
468 apps = apps.order_by('name')
469@@ -172,3 +177,17 @@
470 return render_to_response(
471 'webcatalog/application_detail.html', RequestContext(
472 request, atts))
473+
474+
475+@cache_page(settings.REVIEWS_CACHE_TIMEOUT)
476+def application_reviews(request, package_name, distro, page=1):
477+ app = get_object_or_404(Application, package_name=package_name,
478+ distroseries__code_name=distro)
479+ # XXX michaeln 2011-09-15 Don't request reviews for all languages.
480+ reviews = WebServices().get_reviews_for_package(package_name,
481+ distroseries=distro, page=page)
482+ return render_to_response(
483+ 'webcatalog/application_review_list.html', RequestContext(
484+ request, dict(
485+ application=app, distroseries=distro, reviews=reviews,
486+ breadcrumbs=app.crumbs())))

Subscribers

People subscribed via source and target branches