Merge lp:~michael.nelson/ubuntu-webcatalog/788207-include-ratings-stats into lp:ubuntu-webcatalog

Proposed by Michael Nelson
Status: Merged
Approved by: Michael Foord
Approved revision: 52
Merged at revision: 43
Proposed branch: lp:~michael.nelson/ubuntu-webcatalog/788207-include-ratings-stats
Merge into: lp:ubuntu-webcatalog
Diff against target: 517 lines (+319/-16)
12 files modified
.bzrignore (+1/-0)
django_project/config/main.cfg (+2/-0)
fabtasks/bootstrap.py (+6/-0)
new_file_template.py (+0/-9)
requirements.txt (+1/-0)
src/webcatalog/management/commands/import_app_install_data.py (+1/-1)
src/webcatalog/management/commands/import_ratings_stats.py (+98/-0)
src/webcatalog/migrations/0006_add_review_stats.py (+92/-0)
src/webcatalog/models/__init__.py (+3/-1)
src/webcatalog/models/applications.py (+15/-0)
src/webcatalog/schema.py (+4/-0)
src/webcatalog/tests/test_commands.py (+96/-5)
To merge this branch: bzr merge lp:~michael.nelson/ubuntu-webcatalog/788207-include-ratings-stats
Reviewer Review Type Date Requested Status
Michael Foord (community) Approve
Review via email: mp+68381@code.launchpad.net

Commit message

Add import_ratings_stats management command.

Description of the change

Overview
========
This is the first branch for bug 813346 - displaying ratings stats for each app. It defines a management command to pull in the review stats for a given distroseries and update the application objects (with new attributes defined in the migration).

The next branch will add the stats to the UI.

To test:
follow the readme to bootstrap and then `fab test`

To post a comment you must log in.
52. By Michael Nelson

Updated new_file_template removing unnecessary cruft (according to mfoord).

Revision history for this message
Michael Hall (mhall119) wrote :

There isn't actually a README file

Revision history for this message
Michael Foord (mfoord) wrote :

Nice tests.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file '.bzrignore'
2--- .bzrignore 2011-07-06 13:53:41 +0000
3+++ .bzrignore 2011-07-20 15:32:34 +0000
4@@ -10,3 +10,4 @@
5 branches/
6 sourcecode/framework/
7 sourcecode/django/
8+django_project/rnrclient.py
9
10=== modified file 'django_project/config/main.cfg'
11--- django_project/config/main.cfg 2011-07-05 16:09:31 +0000
12+++ django_project/config/main.cfg 2011-07-20 15:32:34 +0000
13@@ -98,3 +98,5 @@
14 [google]
15 google_analytics_id = UA-1018242-24
16
17+[rnr]
18+rnr_service_root = http://reviews.ubuntu.com/reviews/api/1.0
19
20=== modified file 'fabtasks/bootstrap.py'
21--- fabtasks/bootstrap.py 2011-07-05 16:09:31 +0000
22+++ fabtasks/bootstrap.py 2011-07-20 15:32:34 +0000
23@@ -112,6 +112,12 @@
24 _symlink(
25 "branches/django-openid-auth/django_openid_auth",
26 "django_project/django_openid_auth")
27+ _get_or_pull_bzr_branch(
28+ "lp:/~rnr-developers/rnr-server/rnrclient",
29+ "rnrclient")
30+ _symlink(
31+ "branches/rnrclient/rnrclient.py",
32+ "django_project/rnrclient.py")
33
34 def bootstrap():
35 virtualenv_create()
36
37=== modified file 'new_file_template.py'
38--- new_file_template.py 2011-04-07 08:57:13 +0000
39+++ new_file_template.py 2011-07-20 15:32:34 +0000
40@@ -16,12 +16,3 @@
41 # along with this program. If not, see <http://www.gnu.org/licenses/>.
42
43 """XXX: Module docstring goes here."""
44-
45-from __future__ import (
46- absolute_import,
47- with_statement,
48- )
49-
50-__metaclass__ = type
51-__all__ = [
52- ]
53
54=== modified file 'requirements.txt'
55--- requirements.txt 2011-06-28 10:29:23 +0000
56+++ requirements.txt 2011-07-20 15:32:34 +0000
57@@ -8,6 +8,7 @@
58 django-preflight
59 mock
60 PIL
61+piston-mini-client
62 python-debian
63 python-openid
64 setuptools
65
66=== modified file 'src/webcatalog/management/commands/import_app_install_data.py'
67--- src/webcatalog/management/commands/import_app_install_data.py 2011-07-07 14:15:04 +0000
68+++ src/webcatalog/management/commands/import_app_install_data.py 2011-07-20 15:32:34 +0000
69@@ -241,7 +241,7 @@
70
71 prefetched_apps = dict((app.package_name, app) for app in
72 Application.objects.filter(distroseries=distroseries))
73-
74+
75 # For each package in the apt-cache, we update (or possibly
76 # create) a matching db entry.
77 for package in cache:
78
79=== added file 'src/webcatalog/management/commands/import_ratings_stats.py'
80--- src/webcatalog/management/commands/import_ratings_stats.py 1970-01-01 00:00:00 +0000
81+++ src/webcatalog/management/commands/import_ratings_stats.py 2011-07-20 15:32:34 +0000
82@@ -0,0 +1,98 @@
83+# -*- coding: utf-8 -*-
84+# This file is part of the Ubuntu Web Catalog
85+# Copyright (C) 2011 Canonical Ltd.
86+#
87+# This program is free software: you can redistribute it and/or modify
88+# it under the terms of the GNU Affero General Public License as
89+# published by the Free Software Foundation, either version 3 of the
90+# License, or (at your option) any later version.
91+#
92+# This program is distributed in the hope that it will be useful,
93+# but WITHOUT ANY WARRANTY; without even the implied warranty of
94+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
95+# GNU Affero General Public License for more details.
96+#
97+# You should have received a copy of the GNU Affero General Public License
98+# along with this program. If not, see <http://www.gnu.org/licenses/>.
99+
100+"""Management command to import review statistics for a distroseries."""
101+
102+import os
103+import shutil
104+import tempfile
105+import urllib
106+import operator
107+from datetime import (
108+ datetime,
109+ timedelta,
110+ )
111+
112+from django.conf import settings
113+from django.core.management.base import LabelCommand
114+
115+from rnrclient import RatingsAndReviewsAPI
116+
117+from webcatalog.models import (
118+ Application,
119+ DistroSeries,
120+ ReviewStatsImport,
121+ )
122+
123+
124+class Command(LabelCommand):
125+
126+ help = "Update application review statistics."
127+ verbosity = 1
128+
129+ def handle_label(self, distroseries_name, **options):
130+ self.verbosity = int(options['verbosity'])
131+ # Check when we most recently imported review stats for this
132+ # distroseries.
133+ distroseries, created = DistroSeries.objects.get_or_create(
134+ code_name=distroseries_name)
135+
136+ stats = self.download_review_stats(distroseries)
137+
138+ self.update_apps_with_stats(distroseries, stats)
139+
140+ self.update_last_import_timestamp(distroseries)
141+
142+ def update_apps_with_stats(self, distroseries, stats):
143+ stats_dict = dict((stat.package_name, stat) for stat in stats)
144+ apps = Application.objects.filter(distroseries=distroseries,
145+ package_name__in=stats_dict.keys()).order_by('package_name')
146+
147+ for app in apps:
148+ stat = stats_dict[app.package_name]
149+ app.ratings_average = stat.ratings_average
150+ app.ratings_total = stat.ratings_total
151+ app.ratings_histogram = stat.histogram
152+ app.save()
153+
154+ def update_last_import_timestamp(self, distroseries):
155+ review_stats_import, created = ReviewStatsImport.objects.get_or_create(
156+ distroseries=distroseries)
157+ review_stats_import.last_import = datetime.utcnow()
158+ review_stats_import.save()
159+
160+ def download_review_stats(self, distroseries):
161+ # Download the appropriate stats based on above.
162+ other_imports = ReviewStatsImport.objects.filter(
163+ distroseries=distroseries).order_by('-last_import')
164+ num_days = None
165+ if other_imports:
166+ stats_import = other_imports[0]
167+ time_since_last_import = (
168+ datetime.utcnow() - stats_import.last_import)
169+ if time_since_last_import < timedelta(days=1):
170+ num_days = 1
171+ elif time_since_last_import < timedelta(days=3):
172+ num_days = 3
173+ elif time_since_last_import < timedelta(days=7):
174+ num_days = 7
175+
176+ rnr_api = RatingsAndReviewsAPI(service_root=settings.RNR_SERVICE_ROOT)
177+ kwargs = dict(origin='ubuntu', distroseries=distroseries.code_name)
178+ if num_days:
179+ kwargs['days'] = num_days
180+ return rnr_api.review_stats(**kwargs)
181
182=== added file 'src/webcatalog/migrations/0006_add_review_stats.py'
183--- src/webcatalog/migrations/0006_add_review_stats.py 1970-01-01 00:00:00 +0000
184+++ src/webcatalog/migrations/0006_add_review_stats.py 2011-07-20 15:32:34 +0000
185@@ -0,0 +1,92 @@
186+# encoding: utf-8
187+import datetime
188+from south.db import db
189+from south.v2 import SchemaMigration
190+from django.db import models
191+
192+class Migration(SchemaMigration):
193+
194+ def forwards(self, orm):
195+
196+ # Adding model 'ReviewStatsImport'
197+ db.create_table('webcatalog_reviewstatsimport', (
198+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
199+ ('distroseries', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['webcatalog.DistroSeries'])),
200+ ('last_import', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime.utcnow)),
201+ ))
202+ db.send_create_signal('webcatalog', ['ReviewStatsImport'])
203+
204+ # Adding field 'Application.ratings_total'
205+ db.add_column('webcatalog_application', 'ratings_total', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True), keep_default=False)
206+
207+ # Adding field 'Application.ratings_average'
208+ db.add_column('webcatalog_application', 'ratings_average', self.gf('django.db.models.fields.DecimalField')(null=True, max_digits=3, decimal_places=2, blank=True), keep_default=False)
209+
210+ # Adding field 'Application.ratings_histogram'
211+ db.add_column('webcatalog_application', 'ratings_histogram', self.gf('django.db.models.fields.CharField')(default='', max_length=128, blank=True), keep_default=False)
212+
213+
214+ def backwards(self, orm):
215+
216+ # Deleting model 'ReviewStatsImport'
217+ db.delete_table('webcatalog_reviewstatsimport')
218+
219+ # Deleting field 'Application.ratings_total'
220+ db.delete_column('webcatalog_application', 'ratings_total')
221+
222+ # Deleting field 'Application.ratings_average'
223+ db.delete_column('webcatalog_application', 'ratings_average')
224+
225+ # Deleting field 'Application.ratings_histogram'
226+ db.delete_column('webcatalog_application', 'ratings_histogram')
227+
228+
229+ models = {
230+ 'webcatalog.application': {
231+ 'Meta': {'unique_together': "(('distroseries', 'archive_id'),)", 'object_name': 'Application'},
232+ 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
233+ 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
234+ 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}),
235+ 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
236+ 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
237+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
238+ 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}),
239+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
240+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}),
241+ 'for_purchase': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
242+ 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
243+ 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
244+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
245+ 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
246+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}),
247+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
248+ 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
249+ 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
250+ 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2'}),
251+ 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}),
252+ 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
253+ 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
254+ 'screenshot_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
255+ 'section': ('django.db.models.fields.CharField', [], {'max_length': '32'})
256+ },
257+ 'webcatalog.department': {
258+ 'Meta': {'object_name': 'Department'},
259+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
260+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
261+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'})
262+ },
263+ 'webcatalog.distroseries': {
264+ 'Meta': {'object_name': 'DistroSeries'},
265+ 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
266+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
267+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'})
268+ },
269+ 'webcatalog.reviewstatsimport': {
270+ 'Meta': {'object_name': 'ReviewStatsImport'},
271+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}),
272+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
273+ 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
274+ }
275+ }
276+
277+ complete_apps = ['webcatalog']
278
279=== modified file 'src/webcatalog/models/__init__.py'
280--- src/webcatalog/models/__init__.py 2011-06-27 16:31:36 +0000
281+++ src/webcatalog/models/__init__.py 2011-07-20 15:32:34 +0000
282@@ -26,11 +26,13 @@
283 'DistroSeries',
284 'Application',
285 'Department',
286+ 'ReviewStatsImport',
287 ]
288
289 from .oauthtoken import Token, Consumer, Nonce, DataStore
290 from .applications import (
291- DistroSeries,
292 Application,
293 Department,
294+ DistroSeries,
295+ ReviewStatsImport,
296 )
297
298=== modified file 'src/webcatalog/models/applications.py'
299--- src/webcatalog/models/applications.py 2011-07-07 20:54:45 +0000
300+++ src/webcatalog/models/applications.py 2011-07-20 15:32:34 +0000
301@@ -24,6 +24,7 @@
302
303 import logging
304 import re
305+from datetime import datetime
306
307 from django.core.urlresolvers import reverse
308 from django.db import models
309@@ -34,6 +35,7 @@
310 __all__ = [
311 'Application',
312 'DistroSeries',
313+ 'ReviewStatsImport',
314 ]
315
316
317@@ -82,6 +84,11 @@
318 price = models.DecimalField(max_digits=7, decimal_places=2, null=True,
319 help_text="For-purchase applications (in US Dollars).")
320
321+ ratings_total = models.IntegerField(null=True, blank=True)
322+ ratings_average = models.DecimalField(max_digits=3, decimal_places=2,
323+ null=True, blank=True)
324+ ratings_histogram = models.CharField(max_length=128, blank=True)
325+
326 # Other desktop fields used by s-c
327 # x-gnome-fullname
328 # X-AppInstall-Ignore
329@@ -171,3 +178,11 @@
330
331 class Meta:
332 app_label = 'webcatalog'
333+
334+
335+class ReviewStatsImport(models.Model):
336+ distroseries = models.ForeignKey(DistroSeries, unique=True)
337+ last_import = models.DateTimeField(default=datetime.utcnow)
338+
339+ class Meta:
340+ app_label = 'webcatalog'
341
342=== modified file 'src/webcatalog/schema.py'
343--- src/webcatalog/schema.py 2011-07-05 16:09:31 +0000
344+++ src/webcatalog/schema.py 2011-07-20 15:32:34 +0000
345@@ -60,3 +60,7 @@
346 preflight = ConfigSection()
347 preflight.preflight_base_template = StringConfigOption(
348 default="webcatalog/base.html")
349+
350+ rnr = ConfigSection()
351+ rnr.rnr_service_root = StringConfigOption(
352+ default="http://reviews.ubuntu.com/reviews/api/1.0")
353
354=== modified file 'src/webcatalog/tests/test_commands.py'
355--- src/webcatalog/tests/test_commands.py 2011-07-06 14:12:57 +0000
356+++ src/webcatalog/tests/test_commands.py 2011-07-20 15:32:34 +0000
357@@ -25,6 +25,10 @@
358 import os
359 import shutil
360 import tempfile
361+from datetime import (
362+ datetime,
363+ timedelta,
364+ )
365 from decimal import Decimal
366
367 from django.conf import settings
368@@ -34,14 +38,19 @@
369 MagicMock,
370 Mock,
371 )
372+from rnrclient import ReviewsStats
373
374 from webcatalog.models import (
375 Application,
376 DistroSeries,
377+ ReviewStatsImport,
378 )
379 from webcatalog.management.commands.import_app_install_data import (
380 Cache as CacheContextManager,
381- Command,
382+ Command as ImportAppInstallCommand,
383+ )
384+from webcatalog.management.commands.import_ratings_stats import (
385+ Command as ImportRatingsStatsCommand,
386 )
387 from webcatalog.tests.factory import TestCaseWithFactory
388
389@@ -49,6 +58,7 @@
390 __all__ = [
391 'ImportAppInstallTestCase',
392 'ImportForPurchaseAppsTestCase',
393+ 'ImportRatingsTestCase',
394 ]
395
396
397@@ -159,7 +169,7 @@
398 with patch(get_uri_fn) as mock_get_uri:
399 mock_get_uri.return_value = 'http://example.com/my.deb'
400 with patch('urllib.urlretrieve') as mock_urlretrieve:
401- Command().get_latest_app_data_for_series('natty', tmp_dir)
402+ ImportAppInstallCommand().get_latest_app_data_for_series('natty', tmp_dir)
403 shutil.rmtree(tmp_dir)
404
405 mock_urlretrieve.assert_called_with(
406@@ -252,7 +262,7 @@
407 self.assertTrue(scribus.icon.size > 1)
408
409 def test_get_app_data_uri_for_series(self):
410- uri = Command().get_app_data_uri_for_series('natty')
411+ uri = ImportAppInstallCommand().get_app_data_uri_for_series('natty')
412
413 self.assertEqual('http://example.com/app-install-1.01.deb', uri)
414
415@@ -342,7 +352,7 @@
416 code_name='natty').count())
417
418 with CacheContextManager('natty') as cache:
419- Command().update_from_cache(cache, 'natty')
420+ ImportAppInstallCommand().update_from_cache(cache, 'natty')
421
422 self.assertEqual(1, DistroSeries.objects.filter(
423 code_name='natty').count())
424@@ -351,7 +361,7 @@
425 self.assertEqual(0, Application.objects.count())
426
427 with CacheContextManager('natty') as cache:
428- Command().update_from_cache(cache, 'natty')
429+ ImportAppInstallCommand().update_from_cache(cache, 'natty')
430
431 # There are 4 packages in our mock_cache which is patched in the
432 # setUp.
433@@ -437,3 +447,84 @@
434 app = Application.objects.get(name='MyApp',
435 distroseries=self.distroseries)
436 self.assertEqual(6461, app.icon.size)
437+
438+
439+class ImportRatingsTestCase(TestCaseWithFactory):
440+
441+ def setUp(self):
442+ super(ImportRatingsTestCase, self).setUp()
443+ review_stats_fn = 'rnrclient.RatingsAndReviewsAPI.review_stats'
444+ self.ratings_stats_patcher = patch(review_stats_fn)
445+ self.mock_review_stats = self.ratings_stats_patcher.start()
446+ self.mock_review_stats.return_value = []
447+
448+ def tearDown(self):
449+ self.ratings_stats_patcher.stop()
450+ super(ImportRatingsTestCase, self).tearDown()
451+
452+ def test_creates_last_import_record(self):
453+ onion = self.factory.make_distroseries(code_name='onion')
454+ self.assertEqual(0,
455+ ReviewStatsImport.objects.filter(distroseries=onion).count())
456+
457+ call_command('import_ratings_stats', 'onion')
458+
459+ self.assertEqual(1,
460+ ReviewStatsImport.objects.filter(distroseries=onion).count())
461+
462+ def test_updates_last_import_record(self):
463+ onion = self.factory.make_distroseries(code_name='onion')
464+ orig_timestamp = datetime(2011, 07, 18, 14, 43)
465+ import_record = ReviewStatsImport.objects.create(
466+ distroseries=onion, last_import=orig_timestamp)
467+
468+ call_command('import_ratings_stats', 'onion')
469+
470+ import_records = ReviewStatsImport.objects.filter(distroseries=onion)
471+ self.assertEqual(1, import_records.count())
472+ self.assertTrue(orig_timestamp < import_records[0].last_import)
473+
474+ def test_download_review_stats_no_previous(self):
475+ # If there have been no previous imports, the complete stats
476+ # will be retrieved.
477+ onion = self.factory.make_distroseries(code_name='onion')
478+
479+ call_command('import_ratings_stats', 'onion')
480+
481+ self.mock_review_stats.assert_called_with(
482+ origin='ubuntu', distroseries='onion')
483+
484+ def test_download_review_stats_with_recent(self):
485+ # If there has been a recent import, we'll only grab the
486+ # relevant stats.
487+ onion = self.factory.make_distroseries(code_name='onion')
488+ orig_timestamp = datetime.utcnow() - timedelta(2)
489+ import_record = ReviewStatsImport.objects.create(
490+ distroseries=onion, last_import=orig_timestamp)
491+
492+ call_command('import_ratings_stats', 'onion')
493+
494+ self.mock_review_stats.assert_called_with(
495+ origin='ubuntu', distroseries='onion', days=3)
496+
497+ def test_update_app_stats(self):
498+ # Any stats provided in the api call will be used to update our
499+ # apps in the db.
500+ natty = self.factory.make_distroseries(code_name='natty')
501+ scribus = self.factory.make_application(package_name='scribus',
502+ distroseries=natty)
503+ otherapp = self.factory.make_application(package_name='otherapp',
504+ distroseries=natty)
505+
506+ scribus_stats = ReviewsStats.from_dict(dict(package_name='scribus',
507+ ratings_average='4.00', ratings_total=4,
508+ histogram='[0, 1, 0, 1, 2]'))
509+ self.mock_review_stats.return_value = [scribus_stats]
510+ call_command('import_ratings_stats', 'natty')
511+
512+ scribus = Application.objects.get(id=scribus.id)
513+ self.assertEqual(Decimal('4.00'), scribus.ratings_average)
514+ self.assertEqual(4, scribus.ratings_total)
515+ self.assertEqual('[0, 1, 0, 1, 2]', scribus.ratings_histogram)
516+ otherapp = Application.objects.get(id=otherapp.id)
517+ self.assertEqual(None, otherapp.ratings_total)

Subscribers

People subscribed via source and target branches