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
=== modified file 'src/webcatalog/admin.py'
--- src/webcatalog/admin.py 2012-03-09 15:11:21 +0000
+++ src/webcatalog/admin.py 2012-03-16 04:28:18 +0000
@@ -40,7 +40,7 @@
40class ApplicationAdmin(admin.ModelAdmin):40class ApplicationAdmin(admin.ModelAdmin):
41 list_display = ('package_name', 'name', 'comment', 'distroseries')41 list_display = ('package_name', 'name', 'comment', 'distroseries')
42 search_fields = ('package_name', 'name', 'comment')42 search_fields = ('package_name', 'name', 'comment')
43 list_filter = ('distroseries', 'departments')43 list_filter = ('distroseries', 'for_purchase', 'is_latest', 'departments')
44 exclude = ('for_purchase', 'archive_id')44 exclude = ('for_purchase', 'archive_id')
4545
4646
4747
=== modified file 'src/webcatalog/forms.py'
--- src/webcatalog/forms.py 2012-03-13 15:16:22 +0000
+++ src/webcatalog/forms.py 2012-03-16 04:28:18 +0000
@@ -27,6 +27,9 @@
27from StringIO import StringIO27from StringIO import StringIO
2828
29from django import forms29from django import forms
30from django.conf import settings
31from django.core.mail import EmailMultiAlternatives
32from django.template.loader import render_to_string
3033
31from webcatalog.models import Application34from webcatalog.models import Application
3235
@@ -116,3 +119,17 @@
116119
117class EmailDownloadLinkForm(forms.Form):120class EmailDownloadLinkForm(forms.Form):
118 email = forms.EmailField()121 email = forms.EmailField()
122
123 def send_email(self, app_name, link):
124 subject = 'Link for {0}'.format(app_name)
125 context = {'name': app_name, 'link': link}
126 html = render_to_string('webcatalog/email_download_link.html',
127 context)
128 text = render_to_string('webcatalog/email_download_link.txt',
129 context)
130 sender = ("Ubuntu Application Directory <%s>" %
131 settings.NOREPLY_FROM_ADDRESS)
132 recipient = [self.cleaned_data['email']]
133 message = EmailMultiAlternatives(subject, text, sender, recipient)
134 message.attach_alternative(html, 'text/html')
135 message.send()
119136
=== added file 'src/webcatalog/management/commands/check_all_latest.py'
--- src/webcatalog/management/commands/check_all_latest.py 1970-01-01 00:00:00 +0000
+++ src/webcatalog/management/commands/check_all_latest.py 2012-03-16 04:28:18 +0000
@@ -0,0 +1,64 @@
1# -*- coding: utf-8 -*-
2# This file is part of the Apps Directory
3# Copyright (C) 2011 Canonical Ltd.
4#
5# This program is free software: you can redistribute it and/or modify
6# it under the terms of the GNU Affero General Public License as
7# published by the Free Software Foundation, either version 3 of the
8# License, or (at your option) any later version.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU Affero General Public License for more details.
14#
15# You should have received a copy of the GNU Affero General Public License
16# along with this program. If not, see <http://www.gnu.org/licenses/>.
17
18"""Management command to update the 'is_latest' bit on *all* Applications."""
19
20from __future__ import (
21 absolute_import,
22 )
23
24from django.core.management.base import BaseCommand
25
26from webcatalog.models import Application
27
28__metaclass__ = type
29__all__ = [
30 'Command',
31 ]
32
33BATCH_SIZE = 200
34
35
36class Command(BaseCommand):
37 help = "Updates the 'is_latest' bit on all Applications."
38
39 def handle(self, *args, **options):
40 """A naive implementation of this command could be:
41
42 for p in Application.objects.values_list('package_name',
43 flat=True).distinct(): Application.objects.check_latest(p)
44
45 This command should do exactly that, only much faster.
46 """
47 # 1. Clear all is_latest bits
48 Application.objects.all().update(is_latest=False)
49
50 # 2. Update is_latest in batches of BATCH_SIZE.
51 packages = Application.objects.values_list('id',
52 'package_name').order_by('package_name', '-distroseries__version')
53 current_package_name = None
54 to_update = []
55 for app_id, app_package_name in packages:
56 if current_package_name != app_package_name:
57 to_update.append(app_id)
58 if len(to_update) >= BATCH_SIZE:
59 Application.objects.filter(id__in=to_update).update(
60 is_latest=True)
61 to_update = []
62 current_package_name = app_package_name
63 if to_update:
64 Application.objects.filter(id__in=to_update).update(is_latest=True)
065
=== modified file 'src/webcatalog/management/commands/import_for_purchase_apps.py'
--- src/webcatalog/management/commands/import_for_purchase_apps.py 2012-01-06 17:54:47 +0000
+++ src/webcatalog/management/commands/import_for_purchase_apps.py 2012-03-16 04:28:18 +0000
@@ -57,6 +57,8 @@
57 raise Exception("Couldn't connect to server at %s" % url)57 raise Exception("Couldn't connect to server at %s" % url)
58 app_list = json.loads(response.read())58 app_list = json.loads(response.read())
59 for app_data in app_list:59 for app_data in app_list:
60 # get icon data once per app (not per app per distroseries!)
61 icon_data = self.get_icon_data(app_data)
60 for series_name in app_data['series']:62 for series_name in app_data['series']:
61 distroseries, created = DistroSeries.objects.get_or_create(63 distroseries, created = DistroSeries.objects.get_or_create(
62 code_name=series_name)64 code_name=series_name)
@@ -64,9 +66,10 @@
64 self.output(66 self.output(
65 "Created a DistroSeries record called '{0}'.\n".format(67 "Created a DistroSeries record called '{0}'.\n".format(
66 series_name), 1)68 series_name), 1)
67 self.import_app_from_data(app_data, distroseries)69 self.import_app_from_data(app_data, icon_data, distroseries)
70 Application.objects.check_latest(app_data['package_name'])
6871
69 def import_app_from_data(self, app_data, distroseries):72 def import_app_from_data(self, app_data, icon_data, distroseries):
70 form = ForPurchaseApplicationForm.from_json(app_data, distroseries)73 form = ForPurchaseApplicationForm.from_json(app_data, distroseries)
71 if form.is_valid():74 if form.is_valid():
72 app = form.save(commit=False)75 app = form.save(commit=False)
@@ -74,15 +77,20 @@
74 app.for_purchase = True77 app.for_purchase = True
75 app.save()78 app.save()
76 app.update_departments()79 app.update_departments()
77 self.add_icon_to_app(app, data=app_data.get('icon_data', ''),80 self.add_icon_to_app(app, data=icon_data)
78 url=app_data.get('icon_url', ''))
79 self.output(u"{0} created.\n".format(app.name).encode('utf-8'), 1)81 self.output(u"{0} created.\n".format(app.name).encode('utf-8'), 1)
8082
81 def add_icon_to_app(self, app, data, url):83 def get_icon_data(self, app_data):
82 if data:84 icon_data = app_data.get('icon_data', '')
83 pngfile = StringIO(data.decode('base64'))85 if icon_data:
86 data = icon_data.decode('base64')
84 else:87 else:
85 pngfile = urllib.urlopen(url)88 url = app_data.get('icon_url', '')
89 data = urllib.urlopen(url).read()
90 return data
91
92 def add_icon_to_app(self, app, data):
93 pngfile = StringIO(data)
86 try:94 try:
87 fd, filename = mkstemp(prefix=app.package_name, suffix='.png')95 fd, filename = mkstemp(prefix=app.package_name, suffix='.png')
88 iconfile = os.fdopen(fd, 'w')96 iconfile = os.fdopen(fd, 'w')
8997
=== modified file 'src/webcatalog/managers.py'
--- src/webcatalog/managers.py 2012-03-08 18:00:09 +0000
+++ src/webcatalog/managers.py 2012-03-16 04:28:18 +0000
@@ -29,6 +29,7 @@
29 ]29 ]
3030
31from django.db import models31from django.db import models
32from django.http import Http404
3233
3334
34class ApplicationManager(models.Manager):35class ApplicationManager(models.Manager):
@@ -36,3 +37,20 @@
36 options = self.filter(**kwargs).order_by('-distroseries__version')37 options = self.filter(**kwargs).order_by('-distroseries__version')
37 if options.exists():38 if options.exists():
38 return options[0]39 return options[0]
40
41 def find_best_or_404(self, **kwargs):
42 app = self.find_best(**kwargs)
43 if app is None:
44 raise Http404()
45 return app
46
47 def check_latest(self, package_name):
48 """Check which app with the given package_name is the latest"""
49 options = self.filter(package_name=package_name).order_by(
50 '-distroseries__version')
51 is_latest = True
52 for option in options:
53 if option.is_latest != is_latest:
54 option.is_latest = is_latest
55 option.save()
56 is_latest = False
3957
=== added file 'src/webcatalog/migrations/0013_add_is_latest.py'
--- src/webcatalog/migrations/0013_add_is_latest.py 1970-01-01 00:00:00 +0000
+++ src/webcatalog/migrations/0013_add_is_latest.py 2012-03-16 04:28:18 +0000
@@ -0,0 +1,162 @@
1# encoding: utf-8
2import datetime
3from south.db import db
4from south.v2 import SchemaMigration
5from django.db import models
6
7class Migration(SchemaMigration):
8
9 def forwards(self, orm):
10
11 # Adding field 'Application.version'
12 db.add_column('webcatalog_application', 'version', self.gf('django.db.models.fields.CharField')(default='', max_length=32, blank=True), keep_default=False)
13
14 # Adding field 'Application.is_latest'
15 db.add_column('webcatalog_application', 'is_latest', self.gf('django.db.models.fields.BooleanField')(default=False), keep_default=False)
16
17
18 def backwards(self, orm):
19
20 # Deleting field 'Application.version'
21 db.delete_column('webcatalog_application', 'version')
22
23 # Deleting field 'Application.is_latest'
24 db.delete_column('webcatalog_application', 'is_latest')
25
26
27 models = {
28 'auth.group': {
29 'Meta': {'object_name': 'Group'},
30 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
31 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
32 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
33 },
34 'auth.permission': {
35 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
36 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
37 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
38 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
39 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
40 },
41 'auth.user': {
42 'Meta': {'object_name': 'User'},
43 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
44 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
45 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
46 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
47 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
48 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
49 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
50 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
51 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
52 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
53 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
54 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
55 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
56 },
57 'contenttypes.contenttype': {
58 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
59 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
60 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
61 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
62 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
63 },
64 'webcatalog.application': {
65 'Meta': {'unique_together': "(('distroseries', 'archive_id'),)", 'object_name': 'Application'},
66 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
67 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
68 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}),
69 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
70 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
71 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
72 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}),
73 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
74 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}),
75 'for_purchase': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
76 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
77 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
78 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
79 'is_latest': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
80 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
81 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}),
82 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
83 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
84 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
85 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2', 'blank': 'True'}),
86 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}),
87 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
88 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
89 'screenshot_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
90 'section': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
91 'version': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'})
92 },
93 'webcatalog.consumer': {
94 'Meta': {'object_name': 'Consumer'},
95 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
96 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
97 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
98 'secret': ('django.db.models.fields.CharField', [], {'default': "'UAjOLIWzoDHCXiWfHxeeBYrsCQxFVx'", 'max_length': '255', 'blank': 'True'}),
99 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
100 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"})
101 },
102 'webcatalog.department': {
103 'Meta': {'object_name': 'Department'},
104 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
105 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
106 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'}),
107 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'})
108 },
109 'webcatalog.distroseries': {
110 'Meta': {'object_name': 'DistroSeries'},
111 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
112 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
113 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'})
114 },
115 'webcatalog.exhibit': {
116 'Meta': {'object_name': 'Exhibit'},
117 'banner_url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
118 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
119 'display': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
120 'distroseries': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.DistroSeries']", 'symmetrical': 'False'}),
121 'html': ('django.db.models.fields.TextField', [], {}),
122 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
123 'package_names': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
124 'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
125 'sca_id': ('django.db.models.fields.IntegerField', [], {})
126 },
127 'webcatalog.machine': {
128 'Meta': {'unique_together': "(('owner', 'uuid'),)", 'object_name': 'Machine'},
129 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
130 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
131 'logo_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56', 'blank': 'True'}),
132 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
133 'package_list': ('django.db.models.fields.TextField', [], {}),
134 'packages_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56'}),
135 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
136 },
137 'webcatalog.nonce': {
138 'Meta': {'object_name': 'Nonce'},
139 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
140 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
141 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
142 'nonce': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
143 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Token']"})
144 },
145 'webcatalog.reviewstatsimport': {
146 'Meta': {'object_name': 'ReviewStatsImport'},
147 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']", 'unique': 'True'}),
148 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
149 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
150 },
151 'webcatalog.token': {
152 'Meta': {'object_name': 'Token'},
153 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
154 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
155 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
156 'token': ('django.db.models.fields.CharField', [], {'default': "'hAZAjuVIwJwrqUVxBCgObaNCbFPGNcijNfArcynJqXKUAOhQkv'", 'max_length': '50', 'primary_key': 'True'}),
157 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'wDrSVtSgATKkTXkRuufxIrtZEgllyWkLREKktgdfBtDDEoHgKs'", 'max_length': '50'}),
158 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
159 }
160 }
161
162 complete_apps = ['webcatalog']
0163
=== modified file 'src/webcatalog/models/applications.py'
--- src/webcatalog/models/applications.py 2012-03-15 13:08:02 +0000
+++ src/webcatalog/models/applications.py 2012-03-16 04:28:18 +0000
@@ -23,7 +23,6 @@
23 )23 )
2424
25import logging25import logging
26import re
27from datetime import datetime26from datetime import datetime
2827
29from django.contrib.auth.models import User28from django.contrib.auth.models import User
@@ -95,6 +94,7 @@
95 ratings_average = models.DecimalField(max_digits=3, decimal_places=2,94 ratings_average = models.DecimalField(max_digits=3, decimal_places=2,
96 null=True, blank=True)95 null=True, blank=True)
97 ratings_histogram = models.CharField(max_length=128, blank=True)96 ratings_histogram = models.CharField(max_length=128, blank=True)
97 is_latest = models.BooleanField()
9898
99 # Other desktop fields used by s-c99 # Other desktop fields used by s-c
100 # x-gnome-fullname100 # x-gnome-fullname
101101
=== modified file 'src/webcatalog/tests/factory.py'
--- src/webcatalog/tests/factory.py 2012-03-15 13:08:02 +0000
+++ src/webcatalog/tests/factory.py 2012-03-16 04:28:18 +0000
@@ -94,7 +94,7 @@
94 comment=None, description=None, icon_name='', icon=None,94 comment=None, description=None, icon_name='', icon=None,
95 distroseries=None, arch='i686', ratings_average=None,95 distroseries=None, arch='i686', ratings_average=None,
96 ratings_total=None, ratings_histogram='', screenshot_url=None,96 ratings_total=None, ratings_histogram='', screenshot_url=None,
97 archive_id=None, version=''):97 archive_id=None, version='', is_latest=False):
98 if name is None:98 if name is None:
99 name = self.get_unique_string(prefix='Readable Name')99 name = self.get_unique_string(prefix='Readable Name')
100 if package_name is None:100 if package_name is None:
@@ -112,7 +112,7 @@
112 icon_name=icon_name, distroseries=distroseries, architectures=arch,112 icon_name=icon_name, distroseries=distroseries, architectures=arch,
113 ratings_average=ratings_average, ratings_total=ratings_total,113 ratings_average=ratings_average, ratings_total=ratings_total,
114 ratings_histogram=ratings_histogram, screenshot_url=screenshot_url,114 ratings_histogram=ratings_histogram, screenshot_url=screenshot_url,
115 archive_id=archive_id, version=version)115 archive_id=archive_id, version=version, is_latest=is_latest)
116116
117 def make_department(self, name, parent=None, slug=None):117 def make_department(self, name, parent=None, slug=None):
118 if slug is None:118 if slug is None:
119119
=== modified file 'src/webcatalog/tests/test_commands.py'
--- src/webcatalog/tests/test_commands.py 2012-03-13 14:09:29 +0000
+++ src/webcatalog/tests/test_commands.py 2012-03-16 04:28:18 +0000
@@ -62,6 +62,7 @@
62 'ImportExhibitsTestCase',62 'ImportExhibitsTestCase',
63 'ImportForPurchaseAppsTestCase',63 'ImportForPurchaseAppsTestCase',
64 'ImportRatingsTestCase',64 'ImportRatingsTestCase',
65 'CheckAllLatestTestCase',
65 ]66 ]
6667
6768
@@ -406,7 +407,10 @@
406 super(ImportForPurchaseAppsTestCase, self).setUp()407 super(ImportForPurchaseAppsTestCase, self).setUp()
407 curdir = os.path.dirname(__file__)408 curdir = os.path.dirname(__file__)
408 sca_apps_file = os.path.join(curdir, 'test_data', 'sca_apps.txt')409 sca_apps_file = os.path.join(curdir, 'test_data', 'sca_apps.txt')
409 self.distroseries = DistroSeries.objects.create(code_name='natty')410 self.natty = self.factory.make_distroseries(
411 code_name='natty', version='11.04')
412 self.maverick = self.factory.make_distroseries(
413 code_name='maverick', version='10.10')
410 with open(sca_apps_file) as content:414 with open(sca_apps_file) as content:
411 self.response_content = content.read()415 self.response_content = content.read()
412 mock_response = Mock()416 mock_response = Mock()
@@ -425,7 +429,7 @@
425 call_command('import_for_purchase_apps')429 call_command('import_for_purchase_apps')
426430
427 app_for_purchase = Application.objects.get(name='MyApp',431 app_for_purchase = Application.objects.get(name='MyApp',
428 distroseries=self.distroseries)432 distroseries=self.natty)
429 self.assertEqual(True, app_for_purchase.for_purchase)433 self.assertEqual(True, app_for_purchase.for_purchase)
430 self.assertTrue(app_for_purchase.description.find('hello') > -1)434 self.assertTrue(app_for_purchase.description.find('hello') > -1)
431435
@@ -433,23 +437,22 @@
433 call_command('import_for_purchase_apps')437 call_command('import_for_purchase_apps')
434438
435 app = Application.objects.get(name='MyApp',439 app = Application.objects.get(name='MyApp',
436 distroseries=self.distroseries)440 distroseries=self.natty)
437 self.assertEqual(2, len(app.available_distroseries()))441 self.assertEqual(2, len(app.available_distroseries()))
438442
439 def test_app_gets_price(self):443 def test_app_gets_price(self):
440 call_command('import_for_purchase_apps')444 call_command('import_for_purchase_apps')
441445
442 app = Application.objects.get(name='MyApp',446 app = Application.objects.get(name='MyApp',
443 distroseries=self.distroseries)447 distroseries=self.natty)
444 self.assertEqual(Decimal('2.50'), app.price)448 self.assertEqual(Decimal('2.50'), app.price)
445449
446 def test_existing_app_gets_updated_by_archive_id(self):450 def test_existing_app_gets_updated_by_archive_id(self):
447 self.factory.make_application(archive_id='launchpad_zematynnad2/myppa',451 self.factory.make_application(archive_id='launchpad_zematynnad2/myppa',
448 package_name='somethingelse',452 package_name='somethingelse', distroseries=self.maverick)
449 distroseries=self.factory.make_distroseries(code_name='maverick'))
450 self.factory.make_application(archive_id='launchpad_zematynnad2/myppa',453 self.factory.make_application(archive_id='launchpad_zematynnad2/myppa',
451 package_name='somethingelse',454 package_name='somethingelse',
452 distroseries=self.distroseries)455 distroseries=self.natty)
453 self.assertEqual(2, Application.objects.count())456 self.assertEqual(2, Application.objects.count())
454457
455 call_command('import_for_purchase_apps')458 call_command('import_for_purchase_apps')
@@ -463,16 +466,23 @@
463 call_command('import_for_purchase_apps')466 call_command('import_for_purchase_apps')
464467
465 app = Application.objects.get(name='MyApp',468 app = Application.objects.get(name='MyApp',
466 distroseries=self.distroseries)469 distroseries=self.natty)
467 self.assertEqual(6461, app.icon.size)470 self.assertEqual(6461, app.icon.size)
468471
469 def test_app_gets_version(self):472 def test_app_gets_version(self):
470 call_command('import_for_purchase_apps')473 call_command('import_for_purchase_apps')
471474
472 app = Application.objects.get(package_name='hello',475 app = Application.objects.get(package_name='hello',
473 distroseries=self.distroseries)476 distroseries=self.natty)
474 self.assertEqual('1.2.3', app.version)477 self.assertEqual('1.2.3', app.version)
475478
479 def test_checks_latest(self):
480 call_command('import_for_purchase_apps')
481
482 app = Application.objects.get(package_name='hello',
483 distroseries=self.natty)
484 self.assertTrue(app.is_latest)
485
476486
477class ImportRatingsTestCase(TestCaseWithFactory):487class ImportRatingsTestCase(TestCaseWithFactory):
478488
@@ -661,3 +671,24 @@
661671
662 retrieved = Exhibit.objects.get()672 retrieved = Exhibit.objects.get()
663 self.assertEqual(expected, retrieved.html)673 self.assertEqual(expected, retrieved.html)
674
675
676class CheckAllLatestTestCase(TestCaseWithFactory):
677 def test_updates_all(self):
678 natty = self.factory.make_distroseries(code_name='natty',
679 version='11.04')
680 oneiric = self.factory.make_distroseries(code_name='oneiric',
681 version='11.10')
682 self.factory.make_application(package_name='foo', distroseries=natty)
683 self.factory.make_application(package_name='foo', distroseries=oneiric)
684 self.factory.make_application(package_name='bar', distroseries=natty,
685 is_latest=True)
686 self.factory.make_application(package_name='baz', distroseries=oneiric)
687
688 call_command('check_all_latest')
689
690 retrieved = Application.objects.filter(package_name='foo').order_by(
691 '-distroseries__code_name')
692 self.assertEqual([True, False], [app.is_latest for app in retrieved])
693 self.assertTrue(Application.objects.get(package_name='bar').is_latest)
694 self.assertTrue(Application.objects.get(package_name='baz').is_latest)
664695
=== modified file 'src/webcatalog/tests/test_managers.py'
--- src/webcatalog/tests/test_managers.py 2012-03-08 18:00:09 +0000
+++ src/webcatalog/tests/test_managers.py 2012-03-16 04:28:18 +0000
@@ -48,3 +48,16 @@
48 package_name=expected.package_name)48 package_name=expected.package_name)
4949
50 self.assertEqual(expected.id, retrieved.id)50 self.assertEqual(expected.id, retrieved.id)
51
52 def test_check_latest(self):
53 for code_name in ['lucid', 'maverick', 'natty', 'oneiric']:
54 dseries = self.factory.make_distroseries(code_name=code_name)
55 self.factory.make_application(package_name='foobar',
56 distroseries=dseries)
57
58 Application.objects.check_latest('foobar')
59
60 retrieved = Application.objects.filter(package_name='foobar').order_by(
61 '-distroseries__code_name')
62 self.assertEqual([True, False, False, False], [app.is_latest
63 for app in retrieved])
5164
=== modified file 'src/webcatalog/tests/test_models.py'
--- src/webcatalog/tests/test_models.py 2012-03-12 15:30:15 +0000
+++ src/webcatalog/tests/test_models.py 2012-03-16 04:28:18 +0000
@@ -25,7 +25,6 @@
25from django.core.urlresolvers import reverse25from django.core.urlresolvers import reverse
2626
27from webcatalog.tests.factory import TestCaseWithFactory27from webcatalog.tests.factory import TestCaseWithFactory
28from webcatalog.models import Department
2928
30__metaclass__ = type29__metaclass__ = type
31__all__ = [30__all__ = [
3231
=== modified file 'src/webcatalog/tests/test_views.py'
--- src/webcatalog/tests/test_views.py 2012-03-15 13:08:02 +0000
+++ src/webcatalog/tests/test_views.py 2012-03-16 04:28:18 +0000
@@ -309,46 +309,23 @@
309309
310310
311class ApplicationDetailNoSeriesTestCase(TestCaseWithFactory):311class ApplicationDetailNoSeriesTestCase(TestCaseWithFactory):
312312 def test_renders_latest(self):
313 def test_defaults_to_specified_series(self):313 # If a distroseries is not included in the url, we always render the
314 # If a distroseries is not included in the url,314 # latest available app
315 # and we cannot determine it from the user agent,315 natty = self.factory.make_distroseries(code_name='natty',
316 # we redirect to the one specified in the configuration.316 version='11.04')
317 natty = self.factory.make_distroseries(code_name='natty')317 lucid = self.factory.make_distroseries(code_name='lucid',
318 lucid = self.factory.make_distroseries(code_name='lucid')318 version='10.04')
319 natty_app = self.factory.make_application(319 for dseries in [natty, lucid]:
320 package_name='pkgfoo', distroseries=natty)320 self.factory.make_application(package_name='pkgfoo',
321 lucid_app = self.factory.make_application(321 distroseries=dseries)
322 package_name='pkgfoo', distroseries=lucid)
323
324 for default_distro in ('natty', 'lucid'):
325 with patch_settings(DEFAULT_DISTRO=default_distro):
326 url = reverse('wc-package-detail', args=['pkgfoo'])
327 response = self.client.get(url)
328
329 self.assertRedirects(
330 response, reverse(
331 'wc-package-detail', args=[default_distro, 'pkgfoo']))
332
333 def test_redirects_to_ua_distroseries(self):
334 # If a distroseries is not included in the url, but we
335 # know it from the user agent, and the app exists for that
336 # distro series also, we redirect there.
337 natty = self.factory.make_distroseries(code_name='natty')
338 lucid = self.factory.make_distroseries(code_name='lucid')
339 natty_app = self.factory.make_application(
340 package_name='pkgfoo', distroseries=natty)
341 lucid_app = self.factory.make_application(
342 package_name='pkgfoo', distroseries=lucid)
343322
344 url = reverse('wc-package-detail', args=['pkgfoo'])323 url = reverse('wc-package-detail', args=['pkgfoo'])
345 with patch_settings(DEFAULT_DISTRO='natty'):324 response = self.client.get(url)
346 response = self.client.get(
347 url, HTTP_USER_AGENT='blah X11; Linux Ubuntu/10.04 blah blah')
348325
349 self.assertRedirects(326 self.assertEqual(200, response.status_code)
350 response, reverse(327 used = response.context[0]['application']
351 'wc-package-detail', args=['lucid', 'pkgfoo']))328 self.assertEqual(natty, used.distroseries)
352329
353330
354class SearchTestCase(TestCaseWithFactory):331class SearchTestCase(TestCaseWithFactory):
355332
=== modified file 'src/webcatalog/views.py'
--- src/webcatalog/views.py 2012-03-12 15:30:15 +0000
+++ src/webcatalog/views.py 2012-03-16 04:28:18 +0000
@@ -30,7 +30,6 @@
30from convoy.combo import combine_files, parse_qs30from convoy.combo import combine_files, parse_qs
31from django.conf import settings31from django.conf import settings
32from django.contrib import messages32from django.contrib import messages
33from django.core.mail import EmailMultiAlternatives
34from django.core.paginator import Paginator33from django.core.paginator import Paginator
35from django.core.urlresolvers import reverse34from django.core.urlresolvers import reverse
36from django.db.models import Q35from django.db.models import Q
@@ -43,7 +42,6 @@
43 render_to_response,42 render_to_response,
44 )43 )
45from django.template import RequestContext44from django.template import RequestContext
46from django.template.loader import render_to_string
47from django.utils.translation import ugettext as _45from django.utils.translation import ugettext as _
4846
49from webcatalog.forms import EmailDownloadLinkForm47from webcatalog.forms import EmailDownloadLinkForm
@@ -179,47 +177,22 @@
179177
180178
181def application_detail(request, package_name, distro=None):179def application_detail(request, package_name, distro=None):
182180 if distro is None:
181 app = Application.objects.find_best_or_404(package_name=package_name)
182 else:
183 app = get_object_or_404(Application, package_name=package_name,
184 distroseries__code_name=distro)
183 if request.POST:185 if request.POST:
184 form = EmailDownloadLinkForm(request.POST)186 form = EmailDownloadLinkForm(request.POST)
185 app = get_object_or_404(Application, package_name=package_name,
186 distroseries__code_name=distro)
187 if form.is_valid():187 if form.is_valid():
188 subject = 'Link for {0}'.format(app.name)188 link = request.build_absolute_uri()
189 link = request.build_absolute_uri(reverse('wc-package-detail',189 form.send_email(app.name, link)
190 args=[distro, package_name]))
191 context = {'name': app.name, 'link': link}
192 html = render_to_string('webcatalog/email_download_link.html',
193 context)
194 text = render_to_string('webcatalog/email_download_link.txt',
195 context)
196 sender = ("Ubuntu Application Directory <%s>" %
197 settings.NOREPLY_FROM_ADDRESS)
198 recipient = [form.cleaned_data['email']]
199 message = EmailMultiAlternatives(subject, text, sender, recipient)
200 message.attach_alternative(html, 'text/html')
201 message.send()
202
203 messages.success(request, _('Success. Your download link has been'190 messages.success(request, _('Success. Your download link has been'
204 ' sent.'))191 ' sent.'))
205 return HttpResponseRedirect(link)192 return HttpResponseRedirect(link)
206 else:193 else:
207 form = EmailDownloadLinkForm()194 form = EmailDownloadLinkForm()
208195
209 if distro is None:
210 useragent = UserAgentString(request.META.get('HTTP_USER_AGENT', ''))
211 # Check for the distroseries in the useragent, if we have it,
212 # redirect there.
213 if useragent.distroseries:
214 distro = useragent.distroseries
215 else:
216 distro = settings.DEFAULT_DISTRO
217 return HttpResponseRedirect(
218 reverse('wc-package-detail',
219 args=[distro, package_name]))
220
221 app = get_object_or_404(Application, package_name=package_name,
222 distroseries__code_name=distro)
223 atts = {'application': app,196 atts = {'application': app,
224 'available_distroseries': app.available_distroseries(),197 'available_distroseries': app.available_distroseries(),
225 'breadcrumbs': app.crumbs(),198 'breadcrumbs': app.crumbs(),

Subscribers

People subscribed via source and target branches