Merge lp:~elachuni/ubuntu-webcatalog/latest-app into lp:ubuntu-webcatalog

Proposed by Anthony Lenton
Status: Merged
Approved by: Łukasz Czyżykowski
Approved revision: 83
Merged at revision: 79
Proposed branch: lp:~elachuni/ubuntu-webcatalog/latest-app
Merge into: lp:ubuntu-webcatalog
Diff against target: 693 lines (+355/-93)
13 files modified
src/webcatalog/admin.py (+1/-1)
src/webcatalog/forms.py (+17/-0)
src/webcatalog/management/commands/check_all_latest.py (+64/-0)
src/webcatalog/management/commands/import_for_purchase_apps.py (+16/-8)
src/webcatalog/managers.py (+18/-0)
src/webcatalog/migrations/0013_add_is_latest.py (+162/-0)
src/webcatalog/models/applications.py (+1/-1)
src/webcatalog/tests/factory.py (+2/-2)
src/webcatalog/tests/test_commands.py (+40/-9)
src/webcatalog/tests/test_managers.py (+13/-0)
src/webcatalog/tests/test_models.py (+0/-1)
src/webcatalog/tests/test_views.py (+14/-37)
src/webcatalog/views.py (+7/-34)
To merge this branch: bzr merge lp:~elachuni/ubuntu-webcatalog/latest-app
Reviewer Review Type Date Requested Status
Łukasz Czyżykowski (community) Approve
Review via email: mp+97792@code.launchpad.net

Commit message

Made the default URL for apps always render the latest available version for an app, instead of redirecting to a user-agent guessed distroseries-specific version.

Description of the change

Overview
========
This branch makes the default URL for apps (the distroseries-agnostic one) always render the latest available version for an app, instead of redirecting to a user-agent guessed distroseries-specific version, and starts work towards making department views and search results distroseries-agnostic too.

Details
=======
The code for rendering the latest available version of an app instead of redirecting was fairly simple (as the AppicationManager object already had a find_best method), and removed more code than it added afaict.
The following items are work towards distroseries-agnostic department lists and search results:

- Added an 'is_latest' field on Application
- Added a 'check_all_latest' command, that batches updates to that the running time goes from zomg slow to a few seconds, on a 60K app DB.
- import_for_purchase_apps updates the is_latest bit after each run. (I wonder if this makes more sense than just running check_all_latest after importing for-purchase apps, as check_all_latest is now pretty fast. Maybe even check_latest could be removed).
- Added is_latest to the list_filters in Application's admin UI

While I was there I couldn't resist a couple of drive-throughs:

- Refactored the part of the application_detail view that sent an email into the EmailDownloadLinkForm (I wanted to get it out of the view and I think it makes sense on the form, I wouldn't mind moving it somewhere else instead)
- Refactored import_for_purchase_apps so that icon data is fetched once per app, instead of once per app per distroseries.

To post a comment you must log in.
83. By Anthony Lenton

Refactored check_all_latest to remove sillyness.

Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/webcatalog/admin.py'
2--- src/webcatalog/admin.py 2012-03-09 15:11:21 +0000
3+++ src/webcatalog/admin.py 2012-03-16 04:28:18 +0000
4@@ -40,7 +40,7 @@
5 class ApplicationAdmin(admin.ModelAdmin):
6 list_display = ('package_name', 'name', 'comment', 'distroseries')
7 search_fields = ('package_name', 'name', 'comment')
8- list_filter = ('distroseries', 'departments')
9+ list_filter = ('distroseries', 'for_purchase', 'is_latest', 'departments')
10 exclude = ('for_purchase', 'archive_id')
11
12
13
14=== modified file 'src/webcatalog/forms.py'
15--- src/webcatalog/forms.py 2012-03-13 15:16:22 +0000
16+++ src/webcatalog/forms.py 2012-03-16 04:28:18 +0000
17@@ -27,6 +27,9 @@
18 from StringIO import StringIO
19
20 from django import forms
21+from django.conf import settings
22+from django.core.mail import EmailMultiAlternatives
23+from django.template.loader import render_to_string
24
25 from webcatalog.models import Application
26
27@@ -116,3 +119,17 @@
28
29 class EmailDownloadLinkForm(forms.Form):
30 email = forms.EmailField()
31+
32+ def send_email(self, app_name, link):
33+ subject = 'Link for {0}'.format(app_name)
34+ context = {'name': app_name, 'link': link}
35+ html = render_to_string('webcatalog/email_download_link.html',
36+ context)
37+ text = render_to_string('webcatalog/email_download_link.txt',
38+ context)
39+ sender = ("Ubuntu Application Directory <%s>" %
40+ settings.NOREPLY_FROM_ADDRESS)
41+ recipient = [self.cleaned_data['email']]
42+ message = EmailMultiAlternatives(subject, text, sender, recipient)
43+ message.attach_alternative(html, 'text/html')
44+ message.send()
45
46=== added file 'src/webcatalog/management/commands/check_all_latest.py'
47--- src/webcatalog/management/commands/check_all_latest.py 1970-01-01 00:00:00 +0000
48+++ src/webcatalog/management/commands/check_all_latest.py 2012-03-16 04:28:18 +0000
49@@ -0,0 +1,64 @@
50+# -*- coding: utf-8 -*-
51+# This file is part of the Apps Directory
52+# Copyright (C) 2011 Canonical Ltd.
53+#
54+# This program is free software: you can redistribute it and/or modify
55+# it under the terms of the GNU Affero General Public License as
56+# published by the Free Software Foundation, either version 3 of the
57+# License, or (at your option) any later version.
58+#
59+# This program is distributed in the hope that it will be useful,
60+# but WITHOUT ANY WARRANTY; without even the implied warranty of
61+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
62+# GNU Affero General Public License for more details.
63+#
64+# You should have received a copy of the GNU Affero General Public License
65+# along with this program. If not, see <http://www.gnu.org/licenses/>.
66+
67+"""Management command to update the 'is_latest' bit on *all* Applications."""
68+
69+from __future__ import (
70+ absolute_import,
71+ )
72+
73+from django.core.management.base import BaseCommand
74+
75+from webcatalog.models import Application
76+
77+__metaclass__ = type
78+__all__ = [
79+ 'Command',
80+ ]
81+
82+BATCH_SIZE = 200
83+
84+
85+class Command(BaseCommand):
86+ help = "Updates the 'is_latest' bit on all Applications."
87+
88+ def handle(self, *args, **options):
89+ """A naive implementation of this command could be:
90+
91+ for p in Application.objects.values_list('package_name',
92+ flat=True).distinct(): Application.objects.check_latest(p)
93+
94+ This command should do exactly that, only much faster.
95+ """
96+ # 1. Clear all is_latest bits
97+ Application.objects.all().update(is_latest=False)
98+
99+ # 2. Update is_latest in batches of BATCH_SIZE.
100+ packages = Application.objects.values_list('id',
101+ 'package_name').order_by('package_name', '-distroseries__version')
102+ current_package_name = None
103+ to_update = []
104+ for app_id, app_package_name in packages:
105+ if current_package_name != app_package_name:
106+ to_update.append(app_id)
107+ if len(to_update) >= BATCH_SIZE:
108+ Application.objects.filter(id__in=to_update).update(
109+ is_latest=True)
110+ to_update = []
111+ current_package_name = app_package_name
112+ if to_update:
113+ Application.objects.filter(id__in=to_update).update(is_latest=True)
114
115=== modified file 'src/webcatalog/management/commands/import_for_purchase_apps.py'
116--- src/webcatalog/management/commands/import_for_purchase_apps.py 2012-01-06 17:54:47 +0000
117+++ src/webcatalog/management/commands/import_for_purchase_apps.py 2012-03-16 04:28:18 +0000
118@@ -57,6 +57,8 @@
119 raise Exception("Couldn't connect to server at %s" % url)
120 app_list = json.loads(response.read())
121 for app_data in app_list:
122+ # get icon data once per app (not per app per distroseries!)
123+ icon_data = self.get_icon_data(app_data)
124 for series_name in app_data['series']:
125 distroseries, created = DistroSeries.objects.get_or_create(
126 code_name=series_name)
127@@ -64,9 +66,10 @@
128 self.output(
129 "Created a DistroSeries record called '{0}'.\n".format(
130 series_name), 1)
131- self.import_app_from_data(app_data, distroseries)
132+ self.import_app_from_data(app_data, icon_data, distroseries)
133+ Application.objects.check_latest(app_data['package_name'])
134
135- def import_app_from_data(self, app_data, distroseries):
136+ def import_app_from_data(self, app_data, icon_data, distroseries):
137 form = ForPurchaseApplicationForm.from_json(app_data, distroseries)
138 if form.is_valid():
139 app = form.save(commit=False)
140@@ -74,15 +77,20 @@
141 app.for_purchase = True
142 app.save()
143 app.update_departments()
144- self.add_icon_to_app(app, data=app_data.get('icon_data', ''),
145- url=app_data.get('icon_url', ''))
146+ self.add_icon_to_app(app, data=icon_data)
147 self.output(u"{0} created.\n".format(app.name).encode('utf-8'), 1)
148
149- def add_icon_to_app(self, app, data, url):
150- if data:
151- pngfile = StringIO(data.decode('base64'))
152+ def get_icon_data(self, app_data):
153+ icon_data = app_data.get('icon_data', '')
154+ if icon_data:
155+ data = icon_data.decode('base64')
156 else:
157- pngfile = urllib.urlopen(url)
158+ url = app_data.get('icon_url', '')
159+ data = urllib.urlopen(url).read()
160+ return data
161+
162+ def add_icon_to_app(self, app, data):
163+ pngfile = StringIO(data)
164 try:
165 fd, filename = mkstemp(prefix=app.package_name, suffix='.png')
166 iconfile = os.fdopen(fd, 'w')
167
168=== modified file 'src/webcatalog/managers.py'
169--- src/webcatalog/managers.py 2012-03-08 18:00:09 +0000
170+++ src/webcatalog/managers.py 2012-03-16 04:28:18 +0000
171@@ -29,6 +29,7 @@
172 ]
173
174 from django.db import models
175+from django.http import Http404
176
177
178 class ApplicationManager(models.Manager):
179@@ -36,3 +37,20 @@
180 options = self.filter(**kwargs).order_by('-distroseries__version')
181 if options.exists():
182 return options[0]
183+
184+ def find_best_or_404(self, **kwargs):
185+ app = self.find_best(**kwargs)
186+ if app is None:
187+ raise Http404()
188+ return app
189+
190+ def check_latest(self, package_name):
191+ """Check which app with the given package_name is the latest"""
192+ options = self.filter(package_name=package_name).order_by(
193+ '-distroseries__version')
194+ is_latest = True
195+ for option in options:
196+ if option.is_latest != is_latest:
197+ option.is_latest = is_latest
198+ option.save()
199+ is_latest = False
200
201=== added file 'src/webcatalog/migrations/0013_add_is_latest.py'
202--- src/webcatalog/migrations/0013_add_is_latest.py 1970-01-01 00:00:00 +0000
203+++ src/webcatalog/migrations/0013_add_is_latest.py 2012-03-16 04:28:18 +0000
204@@ -0,0 +1,162 @@
205+# encoding: utf-8
206+import datetime
207+from south.db import db
208+from south.v2 import SchemaMigration
209+from django.db import models
210+
211+class Migration(SchemaMigration):
212+
213+ def forwards(self, orm):
214+
215+ # Adding field 'Application.version'
216+ db.add_column('webcatalog_application', 'version', self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True), keep_default=False)
217+
218+ # Adding field 'Application.is_latest'
219+ db.add_column('webcatalog_application', 'is_latest', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
220+
221+
222+ def backwards(self, orm):
223+
224+ # Deleting field 'Application.version'
225+ db.delete_column('webcatalog_application', 'version')
226+
227+ # Deleting field 'Application.is_latest'
228+ db.delete_column('webcatalog_application', 'is_latest')
229+
230+
231+ models = {
232+ 'auth.group': {
233+ 'Meta': {'object_name': 'Group'},
234+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
235+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
236+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
237+ },
238+ 'auth.permission': {
239+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
240+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
241+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
242+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
243+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
244+ },
245+ 'auth.user': {
246+ 'Meta': {'object_name': 'User'},
247+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
248+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
249+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
250+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
251+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
252+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
253+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
254+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
255+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
256+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
257+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
258+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
259+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
260+ },
261+ 'contenttypes.contenttype': {
262+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
263+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
264+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
265+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
266+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
267+ },
268+ 'webcatalog.application': {
269+ 'Meta': {'unique_together': "(('distroseries', 'archive_id'),)", 'object_name': 'Application'},
270+ 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
271+ 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
272+ 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}),
273+ 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
274+ 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
275+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
276+ 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}),
277+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
278+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}),
279+ 'for_purchase': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
280+ 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
281+ 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
282+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
283+ 'is_latest': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
284+ 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
285+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}),
286+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
287+ 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
288+ 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
289+ 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2', 'blank': 'True'}),
290+ 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}),
291+ 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
292+ 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
293+ 'screenshot_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
294+ 'section': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
295+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'})
296+ },
297+ 'webcatalog.consumer': {
298+ 'Meta': {'object_name': 'Consumer'},
299+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
300+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
301+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
302+ 'secret': ('django.db.models.fields.CharField', [], {'default': "'UAjOLIWzoDHCXiWfHxeeBYrsCQxFVx'", 'max_length': '255', 'blank': 'True'}),
303+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
304+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"})
305+ },
306+ 'webcatalog.department': {
307+ 'Meta': {'object_name': 'Department'},
308+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
309+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
310+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'}),
311+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'})
312+ },
313+ 'webcatalog.distroseries': {
314+ 'Meta': {'object_name': 'DistroSeries'},
315+ 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
316+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
317+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'})
318+ },
319+ 'webcatalog.exhibit': {
320+ 'Meta': {'object_name': 'Exhibit'},
321+ 'banner_url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
322+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
323+ 'display': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
324+ 'distroseries': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.DistroSeries']", 'symmetrical': 'False'}),
325+ 'html': ('django.db.models.fields.TextField', [], {}),
326+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
327+ 'package_names': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
328+ 'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
329+ 'sca_id': ('django.db.models.fields.IntegerField', [], {})
330+ },
331+ 'webcatalog.machine': {
332+ 'Meta': {'unique_together': "(('owner', 'uuid'),)", 'object_name': 'Machine'},
333+ 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
334+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
335+ 'logo_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56', 'blank': 'True'}),
336+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
337+ 'package_list': ('django.db.models.fields.TextField', [], {}),
338+ 'packages_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56'}),
339+ 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
340+ },
341+ 'webcatalog.nonce': {
342+ 'Meta': {'object_name': 'Nonce'},
343+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
344+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
345+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
346+ 'nonce': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
347+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Token']"})
348+ },
349+ 'webcatalog.reviewstatsimport': {
350+ 'Meta': {'object_name': 'ReviewStatsImport'},
351+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']", 'unique': 'True'}),
352+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
353+ 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
354+ },
355+ 'webcatalog.token': {
356+ 'Meta': {'object_name': 'Token'},
357+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
358+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
359+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
360+ 'token': ('django.db.models.fields.CharField', [], {'default': "'hAZAjuVIwJwrqUVxBCgObaNCbFPGNcijNfArcynJqXKUAOhQkv'", 'max_length': '50', 'primary_key': 'True'}),
361+ 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'wDrSVtSgATKkTXkRuufxIrtZEgllyWkLREKktgdfBtDDEoHgKs'", 'max_length': '50'}),
362+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
363+ }
364+ }
365+
366+ complete_apps = ['webcatalog']
367
368=== modified file 'src/webcatalog/models/applications.py'
369--- src/webcatalog/models/applications.py 2012-03-15 13:08:02 +0000
370+++ src/webcatalog/models/applications.py 2012-03-16 04:28:18 +0000
371@@ -23,7 +23,6 @@
372 )
373
374 import logging
375-import re
376 from datetime import datetime
377
378 from django.contrib.auth.models import User
379@@ -95,6 +94,7 @@
380 ratings_average = models.DecimalField(max_digits=3, decimal_places=2,
381 null=True, blank=True)
382 ratings_histogram = models.CharField(max_length=128, blank=True)
383+ is_latest = models.BooleanField()
384
385 # Other desktop fields used by s-c
386 # x-gnome-fullname
387
388=== modified file 'src/webcatalog/tests/factory.py'
389--- src/webcatalog/tests/factory.py 2012-03-15 13:08:02 +0000
390+++ src/webcatalog/tests/factory.py 2012-03-16 04:28:18 +0000
391@@ -94,7 +94,7 @@
392 comment=None, description=None, icon_name='', icon=None,
393 distroseries=None, arch='i686', ratings_average=None,
394 ratings_total=None, ratings_histogram='', screenshot_url=None,
395- archive_id=None, version=''):
396+ archive_id=None, version='', is_latest=False):
397 if name is None:
398 name = self.get_unique_string(prefix='Readable Name')
399 if package_name is None:
400@@ -112,7 +112,7 @@
401 icon_name=icon_name, distroseries=distroseries, architectures=arch,
402 ratings_average=ratings_average, ratings_total=ratings_total,
403 ratings_histogram=ratings_histogram, screenshot_url=screenshot_url,
404- archive_id=archive_id, version=version)
405+ archive_id=archive_id, version=version, is_latest=is_latest)
406
407 def make_department(self, name, parent=None, slug=None):
408 if slug is None:
409
410=== modified file 'src/webcatalog/tests/test_commands.py'
411--- src/webcatalog/tests/test_commands.py 2012-03-13 14:09:29 +0000
412+++ src/webcatalog/tests/test_commands.py 2012-03-16 04:28:18 +0000
413@@ -62,6 +62,7 @@
414 'ImportExhibitsTestCase',
415 'ImportForPurchaseAppsTestCase',
416 'ImportRatingsTestCase',
417+ 'CheckAllLatestTestCase',
418 ]
419
420
421@@ -406,7 +407,10 @@
422 super(ImportForPurchaseAppsTestCase, self).setUp()
423 curdir = os.path.dirname(__file__)
424 sca_apps_file = os.path.join(curdir, 'test_data', 'sca_apps.txt')
425- self.distroseries = DistroSeries.objects.create(code_name='natty')
426+ self.natty = self.factory.make_distroseries(
427+ code_name='natty', version='11.04')
428+ self.maverick = self.factory.make_distroseries(
429+ code_name='maverick', version='10.10')
430 with open(sca_apps_file) as content:
431 self.response_content = content.read()
432 mock_response = Mock()
433@@ -425,7 +429,7 @@
434 call_command('import_for_purchase_apps')
435
436 app_for_purchase = Application.objects.get(name='MyApp',
437- distroseries=self.distroseries)
438+ distroseries=self.natty)
439 self.assertEqual(True, app_for_purchase.for_purchase)
440 self.assertTrue(app_for_purchase.description.find('hello') > -1)
441
442@@ -433,23 +437,22 @@
443 call_command('import_for_purchase_apps')
444
445 app = Application.objects.get(name='MyApp',
446- distroseries=self.distroseries)
447+ distroseries=self.natty)
448 self.assertEqual(2, len(app.available_distroseries()))
449
450 def test_app_gets_price(self):
451 call_command('import_for_purchase_apps')
452
453 app = Application.objects.get(name='MyApp',
454- distroseries=self.distroseries)
455+ distroseries=self.natty)
456 self.assertEqual(Decimal('2.50'), app.price)
457
458 def test_existing_app_gets_updated_by_archive_id(self):
459 self.factory.make_application(archive_id='launchpad_zematynnad2/myppa',
460- package_name='somethingelse',
461- distroseries=self.factory.make_distroseries(code_name='maverick'))
462+ package_name='somethingelse', distroseries=self.maverick)
463 self.factory.make_application(archive_id='launchpad_zematynnad2/myppa',
464 package_name='somethingelse',
465- distroseries=self.distroseries)
466+ distroseries=self.natty)
467 self.assertEqual(2, Application.objects.count())
468
469 call_command('import_for_purchase_apps')
470@@ -463,16 +466,23 @@
471 call_command('import_for_purchase_apps')
472
473 app = Application.objects.get(name='MyApp',
474- distroseries=self.distroseries)
475+ distroseries=self.natty)
476 self.assertEqual(6461, app.icon.size)
477
478 def test_app_gets_version(self):
479 call_command('import_for_purchase_apps')
480
481 app = Application.objects.get(package_name='hello',
482- distroseries=self.distroseries)
483+ distroseries=self.natty)
484 self.assertEqual('1.2.3', app.version)
485
486+ def test_checks_latest(self):
487+ call_command('import_for_purchase_apps')
488+
489+ app = Application.objects.get(package_name='hello',
490+ distroseries=self.natty)
491+ self.assertTrue(app.is_latest)
492+
493
494 class ImportRatingsTestCase(TestCaseWithFactory):
495
496@@ -661,3 +671,24 @@
497
498 retrieved = Exhibit.objects.get()
499 self.assertEqual(expected, retrieved.html)
500+
501+
502+class CheckAllLatestTestCase(TestCaseWithFactory):
503+ def test_updates_all(self):
504+ natty = self.factory.make_distroseries(code_name='natty',
505+ version='11.04')
506+ oneiric = self.factory.make_distroseries(code_name='oneiric',
507+ version='11.10')
508+ self.factory.make_application(package_name='foo', distroseries=natty)
509+ self.factory.make_application(package_name='foo', distroseries=oneiric)
510+ self.factory.make_application(package_name='bar', distroseries=natty,
511+ is_latest=True)
512+ self.factory.make_application(package_name='baz', distroseries=oneiric)
513+
514+ call_command('check_all_latest')
515+
516+ retrieved = Application.objects.filter(package_name='foo').order_by(
517+ '-distroseries__code_name')
518+ self.assertEqual([True, False], [app.is_latest for app in retrieved])
519+ self.assertTrue(Application.objects.get(package_name='bar').is_latest)
520+ self.assertTrue(Application.objects.get(package_name='baz').is_latest)
521
522=== modified file 'src/webcatalog/tests/test_managers.py'
523--- src/webcatalog/tests/test_managers.py 2012-03-08 18:00:09 +0000
524+++ src/webcatalog/tests/test_managers.py 2012-03-16 04:28:18 +0000
525@@ -48,3 +48,16 @@
526 package_name=expected.package_name)
527
528 self.assertEqual(expected.id, retrieved.id)
529+
530+ def test_check_latest(self):
531+ for code_name in ['lucid', 'maverick', 'natty', 'oneiric']:
532+ dseries = self.factory.make_distroseries(code_name=code_name)
533+ self.factory.make_application(package_name='foobar',
534+ distroseries=dseries)
535+
536+ Application.objects.check_latest('foobar')
537+
538+ retrieved = Application.objects.filter(package_name='foobar').order_by(
539+ '-distroseries__code_name')
540+ self.assertEqual([True, False, False, False], [app.is_latest
541+ for app in retrieved])
542
543=== modified file 'src/webcatalog/tests/test_models.py'
544--- src/webcatalog/tests/test_models.py 2012-03-12 15:30:15 +0000
545+++ src/webcatalog/tests/test_models.py 2012-03-16 04:28:18 +0000
546@@ -25,7 +25,6 @@
547 from django.core.urlresolvers import reverse
548
549 from webcatalog.tests.factory import TestCaseWithFactory
550-from webcatalog.models import Department
551
552 __metaclass__ = type
553 __all__ = [
554
555=== modified file 'src/webcatalog/tests/test_views.py'
556--- src/webcatalog/tests/test_views.py 2012-03-15 13:08:02 +0000
557+++ src/webcatalog/tests/test_views.py 2012-03-16 04:28:18 +0000
558@@ -309,46 +309,23 @@
559
560
561 class ApplicationDetailNoSeriesTestCase(TestCaseWithFactory):
562-
563- def test_defaults_to_specified_series(self):
564- # If a distroseries is not included in the url,
565- # and we cannot determine it from the user agent,
566- # we redirect to the one specified in the configuration.
567- natty = self.factory.make_distroseries(code_name='natty')
568- lucid = self.factory.make_distroseries(code_name='lucid')
569- natty_app = self.factory.make_application(
570- package_name='pkgfoo', distroseries=natty)
571- lucid_app = self.factory.make_application(
572- package_name='pkgfoo', distroseries=lucid)
573-
574- for default_distro in ('natty', 'lucid'):
575- with patch_settings(DEFAULT_DISTRO=default_distro):
576- url = reverse('wc-package-detail', args=['pkgfoo'])
577- response = self.client.get(url)
578-
579- self.assertRedirects(
580- response, reverse(
581- 'wc-package-detail', args=[default_distro, 'pkgfoo']))
582-
583- def test_redirects_to_ua_distroseries(self):
584- # If a distroseries is not included in the url, but we
585- # know it from the user agent, and the app exists for that
586- # distro series also, we redirect there.
587- natty = self.factory.make_distroseries(code_name='natty')
588- lucid = self.factory.make_distroseries(code_name='lucid')
589- natty_app = self.factory.make_application(
590- package_name='pkgfoo', distroseries=natty)
591- lucid_app = self.factory.make_application(
592- package_name='pkgfoo', distroseries=lucid)
593+ def test_renders_latest(self):
594+ # If a distroseries is not included in the url, we always render the
595+ # latest available app
596+ natty = self.factory.make_distroseries(code_name='natty',
597+ version='11.04')
598+ lucid = self.factory.make_distroseries(code_name='lucid',
599+ version='10.04')
600+ for dseries in [natty, lucid]:
601+ self.factory.make_application(package_name='pkgfoo',
602+ distroseries=dseries)
603
604 url = reverse('wc-package-detail', args=['pkgfoo'])
605- with patch_settings(DEFAULT_DISTRO='natty'):
606- response = self.client.get(
607- url, HTTP_USER_AGENT='blah X11; Linux Ubuntu/10.04 blah blah')
608+ response = self.client.get(url)
609
610- self.assertRedirects(
611- response, reverse(
612- 'wc-package-detail', args=['lucid', 'pkgfoo']))
613+ self.assertEqual(200, response.status_code)
614+ used = response.context[0]['application']
615+ self.assertEqual(natty, used.distroseries)
616
617
618 class SearchTestCase(TestCaseWithFactory):
619
620=== modified file 'src/webcatalog/views.py'
621--- src/webcatalog/views.py 2012-03-12 15:30:15 +0000
622+++ src/webcatalog/views.py 2012-03-16 04:28:18 +0000
623@@ -30,7 +30,6 @@
624 from convoy.combo import combine_files, parse_qs
625 from django.conf import settings
626 from django.contrib import messages
627-from django.core.mail import EmailMultiAlternatives
628 from django.core.paginator import Paginator
629 from django.core.urlresolvers import reverse
630 from django.db.models import Q
631@@ -43,7 +42,6 @@
632 render_to_response,
633 )
634 from django.template import RequestContext
635-from django.template.loader import render_to_string
636 from django.utils.translation import ugettext as _
637
638 from webcatalog.forms import EmailDownloadLinkForm
639@@ -179,47 +177,22 @@
640
641
642 def application_detail(request, package_name, distro=None):
643-
644+ if distro is None:
645+ app = Application.objects.find_best_or_404(package_name=package_name)
646+ else:
647+ app = get_object_or_404(Application, package_name=package_name,
648+ distroseries__code_name=distro)
649 if request.POST:
650 form = EmailDownloadLinkForm(request.POST)
651- app = get_object_or_404(Application, package_name=package_name,
652- distroseries__code_name=distro)
653 if form.is_valid():
654- subject = 'Link for {0}'.format(app.name)
655- link = request.build_absolute_uri(reverse('wc-package-detail',
656- args=[distro, package_name]))
657- context = {'name': app.name, 'link': link}
658- html = render_to_string('webcatalog/email_download_link.html',
659- context)
660- text = render_to_string('webcatalog/email_download_link.txt',
661- context)
662- sender = ("Ubuntu Application Directory <%s>" %
663- settings.NOREPLY_FROM_ADDRESS)
664- recipient = [form.cleaned_data['email']]
665- message = EmailMultiAlternatives(subject, text, sender, recipient)
666- message.attach_alternative(html, 'text/html')
667- message.send()
668-
669+ link = request.build_absolute_uri()
670+ form.send_email(app.name, link)
671 messages.success(request, _('Success. Your download link has been'
672 ' sent.'))
673 return HttpResponseRedirect(link)
674 else:
675 form = EmailDownloadLinkForm()
676
677- if distro is None:
678- useragent = UserAgentString(request.META.get('HTTP_USER_AGENT', ''))
679- # Check for the distroseries in the useragent, if we have it,
680- # redirect there.
681- if useragent.distroseries:
682- distro = useragent.distroseries
683- else:
684- distro = settings.DEFAULT_DISTRO
685- return HttpResponseRedirect(
686- reverse('wc-package-detail',
687- args=[distro, package_name]))
688-
689- app = get_object_or_404(Application, package_name=package_name,
690- distroseries__code_name=distro)
691 atts = {'application': app,
692 'available_distroseries': app.available_distroseries(),
693 'breadcrumbs': app.crumbs(),

Subscribers

People subscribed via source and target branches