Merge lp:~elachuni/ubuntu-webcatalog/latest-app into lp:ubuntu-webcatalog
- latest-app
- Merge into trunk
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 |
Related bugs: |
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-
Description of the change
Overview
========
This branch makes the default URL for apps (the distroseries-
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-
- 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_
- 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 EmailDownloadLi
- Refactored import_
- 83. By Anthony Lenton
-
Refactored check_all_latest to remove sillyness.
Łukasz Czyżykowski (lukasz-czyzykowski) : | # |
Preview Diff
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(), |