Merge lp:~elachuni/ubuntu-webcatalog/fix-import-for-purchase-apps into lp:ubuntu-webcatalog

Proposed by Anthony Lenton
Status: Merged
Approved by: Anthony Lenton
Approved revision: 36
Merged at revision: 33
Proposed branch: lp:~elachuni/ubuntu-webcatalog/fix-import-for-purchase-apps
Merge into: lp:ubuntu-webcatalog
Diff against target: 793 lines (+364/-87)
18 files modified
src/webcatalog/admin.py (+2/-2)
src/webcatalog/forms.py (+22/-1)
src/webcatalog/management/commands/import_app_install_data.py (+14/-7)
src/webcatalog/management/commands/import_for_purchase_apps.py (+50/-10)
src/webcatalog/migrations/0004_add_price_and_make_archive_id_not_unique.py (+72/-0)
src/webcatalog/models/applications.py (+14/-17)
src/webcatalog/static/css/webcatalog.css (+14/-26)
src/webcatalog/templates/404.html (+16/-0)
src/webcatalog/templates/light/404.html (+73/-0)
src/webcatalog/templates/webcatalog/application_detail.html (+4/-1)
src/webcatalog/templates/webcatalog/install_options_snippet.html (+5/-8)
src/webcatalog/templatetags/webcatalog.py (+1/-1)
src/webcatalog/tests/test_commands.py (+38/-1)
src/webcatalog/tests/test_data/sca_apps.txt (+3/-3)
src/webcatalog/tests/test_models.py (+15/-4)
src/webcatalog/tests/test_templatetags.py (+1/-1)
src/webcatalog/tests/test_views.py (+17/-5)
src/webcatalog/views.py (+3/-0)
To merge this branch: bzr merge lp:~elachuni/ubuntu-webcatalog/fix-import-for-purchase-apps
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Approve
Review via email: mp+66786@code.launchpad.net

Commit message

Fix several issues with the import_for_purchase_apps command, and several other smaller issues.

Description of the change

Overview
========
Fix several issues with the import_for_purchase_apps command,
and several other smaller issues.

Details
=======
In order of appearance (in the diff):
 - Improves the Application admin screens, by allowing you to
   filter by distroseries, as well as displaying the
   distroseries for each app in the list display.
 - Adds ForPurchaseApplicationForm, used by
   import_for_purchase_apps to validate retrieved json,
   instead of validating manually.
 - Adds support for tiff icons to import_app_install_data, by
   allowing gtk2 to handle the conversion to png like we were
   doing with svg and xpm icons already.
 - Drastically improves the speed of import_app_install_data
   by prefetching all existing apps for a distroseries from
   the database, instead of querying the db for each one, and
   skipping an app's update if there's nothing to update.
 - Improves import_for_purchase_apps. The price attribute
   wasn't being imported and neither were available
   distroseries or the icon data, and apps were never being
   updated once created.
 - Provides a migration to create the 'price' field on
   for-purchase apps, that wasn't being tracked at all.
 - Removes the unique constraint on archive_id, as it's
   actually (archive_id, distroseries) that's unique.
 - Allows crumbs() to produce distroseries-specific urls, to
   display on distroseries-specific views.
 - Retouches the css for the "Install" button to make it more
   similar to USC.
 - Provides a themed 404 template.
 - Adds tests for everything.
 - Makes invalid distroseries return a 404 response in the
   department overview view.

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

Two small fixes per code review.

Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

LGTM. Just update the copyright date on the templates before the final approval.

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 2011-07-01 16:18:58 +0000
3+++ src/webcatalog/admin.py 2011-07-04 15:54:51 +0000
4@@ -35,9 +35,9 @@
5
6
7 class ApplicationAdmin(admin.ModelAdmin):
8- list_display = ('package_name', 'name', 'comment')
9+ list_display = ('package_name', 'name', 'comment', 'distroseries')
10 search_fields = ('package_name', 'name', 'comment')
11- list_filter = ('departments',)
12+ list_filter = ('distroseries', 'departments')
13 exclude = ('for_purchase', 'archive_id')
14
15 admin.site.register(Application, ApplicationAdmin)
16
17=== modified file 'src/webcatalog/forms.py'
18--- src/webcatalog/forms.py 2011-06-24 07:44:24 +0000
19+++ src/webcatalog/forms.py 2011-07-04 15:54:51 +0000
20@@ -53,7 +53,7 @@
21
22 class Meta:
23 model = Application
24- exclude = ('distroseries','for_purchase', 'archive_id')
25+ exclude = ('distroseries','for_purchase', 'archive_id', 'price')
26
27 @classmethod
28 def get_form_from_desktop_data(cls, str_data, distroseries):
29@@ -85,3 +85,24 @@
30 cleaned_data[key] = instance_value or value
31
32 return cleaned_data
33+
34+class ForPurchaseApplicationForm(forms.ModelForm):
35+ class Meta:
36+ model = Application
37+ exclude = ('distroseries', 'section', 'popcon')
38+
39+ @classmethod
40+ def from_json(cls, app_data, distroseries):
41+ app_data = app_data.copy()
42+ if 'description' in app_data:
43+ tagline, _, description = app_data['description'].partition('\n')
44+ app_data['comment'] = tagline
45+ app_data['description'] = description
46+ try:
47+ instance = Application.objects.get(
48+ archive_id=app_data.get('archive_id'),
49+ distroseries=distroseries)
50+ except Application.DoesNotExist:
51+ instance = None
52+
53+ return cls(data=app_data, instance=instance)
54
55=== modified file 'src/webcatalog/management/commands/import_app_install_data.py'
56--- src/webcatalog/management/commands/import_app_install_data.py 2011-07-01 15:27:44 +0000
57+++ src/webcatalog/management/commands/import_app_install_data.py 2011-07-04 15:54:51 +0000
58@@ -217,8 +217,9 @@
59 break
60
61 if icon_path:
62- if icon_path.endswith('.xpm') or icon_path.endswith('.svg'):
63- target_path = icon_path[:-4] + '.png'
64+ parts = os.path.splitext(icon_path)
65+ if parts[1] in ['.xpm', '.svg', '.tiff']:
66+ target_path = parts[0] + '.png'
67 if create_png_from_file(target_path, icon_path, 32, 32):
68 icon_path = target_path
69 with open(icon_path, "r") as icon_file:
70@@ -236,6 +237,9 @@
71 self.output("Created a DistroSeries record called '{0}'.\n".format(
72 distroseries_name), 1)
73
74+ prefetched_apps = dict((app.package_name, app) for app in
75+ Application.objects.filter(distroseries=distroseries))
76+
77 # For each package in the apt-cache, we update (or possibly
78 # create) a matching db entry.
79 for package in cache:
80@@ -252,17 +256,20 @@
81 if not app_comment:
82 app_comment = package.name
83
84- try:
85- app = Application.objects.get(
86- package_name=package.name,
87- distroseries=distroseries)
88+ if package.name in prefetched_apps:
89+ app = prefetched_apps[package.name]
90+ if (app.description == candidate.description and
91+ app.section == candidate.section and
92+ app.name == app_name and
93+ app.comment == app_comment):
94+ continue
95 app.description = candidate.description
96 app.section = candidate.section
97 app.name = app_name
98 app.comment = app_comment
99 app.save()
100
101- except Application.DoesNotExist:
102+ else:
103 app = Application.objects.create(
104 package_name=package.name,
105 distroseries=distroseries,
106
107=== modified file 'src/webcatalog/management/commands/import_for_purchase_apps.py'
108--- src/webcatalog/management/commands/import_for_purchase_apps.py 2011-06-17 18:12:02 +0000
109+++ src/webcatalog/management/commands/import_for_purchase_apps.py 2011-07-04 15:54:51 +0000
110@@ -23,13 +23,19 @@
111 )
112
113 import json
114+import os
115 import urllib
116+from StringIO import StringIO
117+from tempfile import mkstemp
118
119 from django.conf import settings
120+from django.core.files.images import ImageFile
121 from django.core.management.base import BaseCommand
122
123+from webcatalog.forms import ForPurchaseApplicationForm
124 from webcatalog.models import (
125 Application,
126+ DistroSeries,
127 )
128
129 __metaclass__ = type
130@@ -49,15 +55,49 @@
131 raise Exception("Couldn't connect to server at %s" % url)
132 app_list = json.loads(response.read())
133 for app_data in app_list:
134- archive_id = app_data['archive_id']
135- # see if we've already imported it
136- try:
137- app = Application.objects.get(archive_id=archive_id)
138- continue
139- except Application.DoesNotExist:
140- pass
141- # not already imported - save to webcatalog
142- app = Application.from_json(app_data)
143- # mark it as for purchase since it came from sca
144+ for series_name in app_data['series']:
145+ distroseries, created = DistroSeries.objects.get_or_create(
146+ code_name=series_name)
147+ if created:
148+ self.output(
149+ "Created a DistroSeries record called '{0}'.\n".format(
150+ series_name), 1)
151+ self.import_app_from_data(app_data, distroseries)
152+
153+ def import_app_from_data(self, app_data, distroseries):
154+ form = ForPurchaseApplicationForm.from_json(app_data, distroseries)
155+ if form.is_valid():
156+ app = form.save(commit=False)
157+ app.distroseries = distroseries
158 app.for_purchase = True
159 app.save()
160+ app.update_departments()
161+ self.add_icon_to_app(app, data=app_data.get('icon_data', ''),
162+ url=app_data.get('icon_url', ''))
163+ self.output(u"{0} created.\n".format(app.name).encode('utf-8'), 1)
164+
165+ def add_icon_to_app(self, app, data, url):
166+ if data:
167+ pngfile = StringIO(data.decode('base64'))
168+ else:
169+ pngfile = urllib.urlopen(url)
170+ try:
171+ fd, filename = mkstemp(prefix=app.package_name, suffix='.png')
172+ iconfile = os.fdopen(fd, 'w')
173+ iconfile.write(pngfile.read())
174+ iconfile.close()
175+ with open(filename) as f:
176+ app.icon = ImageFile(f)
177+ app.save()
178+ finally:
179+ if os.path.exists(filename):
180+ os.unlink(filename)
181+
182+ def output(self, message, level=None, flush=False):
183+ if hasattr(self, 'stdout'):
184+ if level is None or self.verbosity >= level:
185+ self.stdout.write(message)
186+ if flush:
187+ self.stdout.flush()
188+
189+
190\ No newline at end of file
191
192=== added file 'src/webcatalog/migrations/0004_add_price_and_make_archive_id_not_unique.py'
193--- src/webcatalog/migrations/0004_add_price_and_make_archive_id_not_unique.py 1970-01-01 00:00:00 +0000
194+++ src/webcatalog/migrations/0004_add_price_and_make_archive_id_not_unique.py 2011-07-04 15:54:51 +0000
195@@ -0,0 +1,72 @@
196+# encoding: utf-8
197+import datetime
198+from south.db import db
199+from south.v2 import SchemaMigration
200+from django.db import models
201+
202+class Migration(SchemaMigration):
203+
204+ def forwards(self, orm):
205+
206+ # Removing unique constraint on 'Application', fields ['archive_id']
207+ db.delete_unique('webcatalog_application', ['archive_id'])
208+
209+ # Adding field 'Application.price'
210+ db.add_column('webcatalog_application', 'price', self.gf('django.db.models.fields.DecimalField')(null=True, max_digits=7, decimal_places=2), keep_default=False)
211+
212+ # Adding unique constraint on 'Application', fields ['archive_id', 'distroseries']
213+ db.create_unique('webcatalog_application', ['archive_id', 'distroseries_id'])
214+
215+
216+ def backwards(self, orm):
217+
218+ # Removing unique constraint on 'Application', fields ['archive_id', 'distroseries']
219+ db.delete_unique('webcatalog_application', ['archive_id', 'distroseries_id'])
220+
221+ # Deleting field 'Application.price'
222+ db.delete_column('webcatalog_application', 'price')
223+
224+ # Adding unique constraint on 'Application', fields ['archive_id']
225+ db.create_unique('webcatalog_application', ['archive_id'])
226+
227+
228+ models = {
229+ 'webcatalog.application': {
230+ 'Meta': {'unique_together': "(('distroseries', 'archive_id'),)", 'object_name': 'Application'},
231+ 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
232+ 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
233+ 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}),
234+ 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
235+ 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
236+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
237+ 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}),
238+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
239+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}),
240+ 'for_purchase': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
241+ 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
242+ 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
243+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
244+ 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
245+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}),
246+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
247+ 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
248+ 'popcon': ('django.db.models.fields.IntegerField', [], {}),
249+ 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2'}),
250+ 'screenshot_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
251+ 'section': ('django.db.models.fields.CharField', [], {'max_length': '32'})
252+ },
253+ 'webcatalog.department': {
254+ 'Meta': {'object_name': 'Department'},
255+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
256+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
257+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'})
258+ },
259+ 'webcatalog.distroseries': {
260+ 'Meta': {'object_name': 'DistroSeries'},
261+ 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
262+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
263+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'})
264+ }
265+ }
266+
267+ complete_apps = ['webcatalog']
268
269=== modified file 'src/webcatalog/models/applications.py'
270--- src/webcatalog/models/applications.py 2011-07-01 16:18:58 +0000
271+++ src/webcatalog/models/applications.py 2011-07-04 15:54:51 +0000
272@@ -54,7 +54,7 @@
273 # will potentially have a different name) and names/descriptions can
274 # change for a package for different series.
275 # We may need to have one application item per language, depending
276- # whether we have geettext data available that we could use at
277+ # whether we have gettext data available that we could use at
278 # runtime (much preferred)
279 distroseries = models.ForeignKey(DistroSeries)
280
281@@ -78,7 +78,9 @@
282 null=True, blank=True)
283 for_purchase = models.BooleanField(default=False)
284 archive_id = models.CharField(max_length=64, null=True,
285- db_index=True, blank=True, unique=True)
286+ db_index=True, blank=True)
287+ price = models.DecimalField(max_digits=7, decimal_places=2, null=True,
288+ help_text="For-purchase applications (in US Dollars).")
289
290 # Other desktop fields used by s-c
291 # x-gnome-fullname
292@@ -104,16 +106,6 @@
293 return DistroSeries.objects.filter(
294 application__package_name=self.package_name).order_by('version')
295
296- @classmethod
297- def from_json(cls, data):
298- app = Application()
299- for name in app._meta.get_all_field_names():
300- try:
301- setattr(app, name, data[name])
302- except KeyError:
303- pass
304- return app
305-
306 def update_departments(self):
307 """Update the list of departments for this app"""
308 self.departments.clear()
309@@ -131,7 +123,7 @@
310 def crumbs(self):
311 depts = self.departments.order_by('-parent')
312 if depts.count():
313- crumbs = depts[0].crumbs()
314+ crumbs = depts[0].crumbs(distro=self.distroseries.code_name)
315 else:
316 crumbs = [{'name': 'Get Software', 'url': reverse('wc-index')}]
317 crumbs.append({'name': self.name, 'url': reverse('wc-package-detail',
318@@ -144,6 +136,7 @@
319
320 class Meta:
321 app_label = 'webcatalog'
322+ unique_together = ('distroseries', 'archive_id')
323
324
325 class Department(models.Model):
326@@ -157,13 +150,17 @@
327 def normalized_name(self):
328 return re.sub(r'[-&.,;" \']', '', self.name)
329
330- def crumbs(self):
331+ def crumbs(self, distro=None):
332 if self.parent:
333- crumbs = self.parent.crumbs()
334+ crumbs = self.parent.crumbs(distro=distro)
335 else:
336 crumbs = [{'name': 'Get Software', 'url': reverse('wc-index')}]
337- crumbs.append({'name': self.name, 'url': reverse('wc-department',
338- args=[self.id])})
339+ args = []
340+ if distro is not None:
341+ args = [distro]
342+ args.append(self.id)
343+ url =reverse('wc-department', args=args)
344+ crumbs.append({'name': self.name, 'url': url})
345 return crumbs
346
347 class Meta:
348
349=== modified file 'src/webcatalog/static/css/webcatalog.css'
350--- src/webcatalog/static/css/webcatalog.css 2011-07-01 16:18:58 +0000
351+++ src/webcatalog/static/css/webcatalog.css 2011-07-04 15:54:51 +0000
352@@ -162,33 +162,21 @@
353 width: 300px;
354 }
355
356-.awesome, .awesome:visited {
357- background-color: #222222;
358- border-bottom: 1px solid rgba(0, 0, 0, 0.25);
359- border-radius: 5px 5px 5px 5px;
360- box-shadow: 0 1px 1px rgba(0, 0, 0, 0.15);
361- cursor: pointer;
362- display: inline-block;
363- font-size: 20px;
364- font-weight: bold;
365- line-height: 1;
366- margin: 5px;
367+.install-bar {
368+ background: #B1CEE6;
369+ border: 1px solid #639BCE;
370+ height: 28px;
371+ margin: 0 16px;
372+ padding: 4px;
373+}
374+.install-bar p {
375+ font-size: 14px;
376 padding: 5px;
377- position: relative;
378- text-decoration: none;
379- text-shadow: 0 -1px 1px rgba(0, 0, 0, 0.25);
380-}
381-.awesome:hover {
382- background-color: #D45500;
383- color: #FFFFFF;
384- text-decoration: none;
385-}
386-.awesome:active {
387- top: 1px;
388-}
389-.awesome, .awesome:visited {
390- background-color: #FF5C00;
391- color: #FFFFFF;
392+}
393+.install-bar div.install-button {
394+ float: right;
395+ height: 28px;
396+ line-height: 28px;
397 }
398
399 .dept-icon {
400
401=== added file 'src/webcatalog/templates/404.html'
402--- src/webcatalog/templates/404.html 1970-01-01 00:00:00 +0000
403+++ src/webcatalog/templates/404.html 2011-07-04 15:54:51 +0000
404@@ -0,0 +1,16 @@
405+{% extends "light/404.html" %}
406+{% load i18n %}
407+
408+{% comment %}
409+Copyright 2010 Canonical Ltd. This software is licensed under the
410+GNU Affero General Public License version 3 (see the file LICENSE).
411+{% endcomment %}
412+
413+{% block content_container_extra %}
414+ <div>
415+ <p>
416+ {% blocktrans %}(Error <abbr>ID</abbr>: {{ oopsid }}){% endblocktrans %}
417+ </p>
418+ </div>
419+{% endblock %}
420+
421
422=== added file 'src/webcatalog/templates/light/404.html'
423--- src/webcatalog/templates/light/404.html 1970-01-01 00:00:00 +0000
424+++ src/webcatalog/templates/light/404.html 2011-07-04 15:54:51 +0000
425@@ -0,0 +1,73 @@
426+{% extends 'light/index.1col.html' %}
427+
428+{% block title %}Error | Ubuntu{% endblock %}
429+
430+{% block header %}{% endblock %}
431+
432+{% block head_extra %}
433+ <link rel="stylesheet" type="text/css" href="{{ ASSET_ROOT }}/assets/light/css/404.css"/>
434+{% endblock %}
435+
436+{% block content_container %}
437+<div id="template-err2">
438+ <div id="row-main-row" >
439+ <div class="pane-content">
440+ <div class="img_404">
441+ <h1>404</h1>
442+ </div>
443+ <h2>This page does not exist.</h2>
444+ <h3>Well, obviously <em>this</em> page exists. But the
445+ page you requested does not exist. This page is just
446+ here to tell you that the page you requested does not exist.
447+ </h3>
448+ {% block content_container_default %}
449+ <p>You can use the search box above to find what you need.
450+ Or you can make a fresh start at the
451+ <a href="http://www.ubuntu.com">Ubuntu home page</a>.
452+ </p>
453+ <p>Still can't find what you're looking for?
454+ <a href="/contact-us">Get in touch today</a>.
455+ </p>
456+ {% endblock %}
457+ </div>
458+ </div>
459+ {% block content_container_extra %}{% endblock %}
460+
461+ <div class="clear"> </div>
462+
463+ <div id="row-2" class="row-wrapper-2">
464+
465+ <div class="grid-4 err2_pane_4" >
466+ <div>
467+ <h2><a href="http://www.ubuntu.com/project">About Ubuntu</a></h2>
468+ <p></p>
469+ <p>Find out more about the Ubuntu Project and how you can contribute.</p>
470+ <p></p>
471+ <p><img src="http://www.ubuntu.com/sites/default/files/active/error_page_imagery/pictogram_knowledge_medium.png" alt="pictogram_knowledge_medium" /></p>
472+ </div>
473+ </div>
474+
475+ <div class="grid-4 err2_pane_4" >
476+ <div>
477+ <h2><a href="http://www.ubuntu.com/desktop/get-ubuntu">Get Ubuntu</a></h2>
478+ <p></p>
479+ <p>Download Ubuntu today.</p>
480+ <p></p>
481+ <p><img src="http://www.ubuntu.com/sites/default/files/active/03_global/02_pictograms/pictogram_download_medium.png" alt="pictogram_download_medium" /></p>
482+ </div>
483+ </div>
484+
485+ <div class="grid-4 err2_pane_4" >
486+ <div>
487+ <h2><a href="http://shop.canonical.com">Visit the shop</a></h2>
488+ <p></p>
489+ <p>Buy Ubuntu CDs, accessories or training at the Canonical shop.</p>
490+ <p></p>
491+ <p><img src="http://www.ubuntu.com/sites/default/files/active/error_page_imagery/pictogram_shop_medium.png" alt="pictogram_shop_medium.png" /></p>
492+ </div>
493+ </div>
494+
495+ <div class="clear"> </div>
496+ </div>
497+</div>
498+{% endblock %}
499
500=== modified file 'src/webcatalog/templates/webcatalog/application_detail.html'
501--- src/webcatalog/templates/webcatalog/application_detail.html 2011-07-01 16:18:58 +0000
502+++ src/webcatalog/templates/webcatalog/application_detail.html 2011-07-04 15:54:51 +0000
503@@ -33,6 +33,10 @@
504 <h2>{{ application.name }}</h2>
505 <p>{{ application.comment }}</p>
506 </div>
507+ <div class="install-bar">
508+ <div class="install-button">{% install_options application %}</div>
509+ <p>{% if application.price %}US$ {{ application.price }}{% else %}Free{% endif %}</p>
510+ </div>
511 <div class="description">
512 <div class="screenshot">
513 {% comment %}
514@@ -42,7 +46,6 @@
515 <img src="http://screenshots.ubuntu.com/thumbnail-with-version/{{ application.package_name }}/ignored" />
516 </div>
517 <p>{{ application.description }}</p>
518- {% install_options application %}
519 </div>
520 <div class="license">
521 <table>
522
523=== modified file 'src/webcatalog/templates/webcatalog/install_options_snippet.html'
524--- src/webcatalog/templates/webcatalog/install_options_snippet.html 2011-06-29 14:43:55 +0000
525+++ src/webcatalog/templates/webcatalog/install_options_snippet.html 2011-07-04 15:54:51 +0000
526@@ -2,14 +2,11 @@
527 {% if message_text %}
528 {{ message_text }}
529 {% endif %}{% if display_install_button %}
530- <a href="apt://{{ application.package_name }}" class="awesome">
531- {% if application.for_purchase %}
532- Purchase {{ application.name }}
533- {% else %}
534- Install {{ application.name }}
535- {% endif %}
536- </a>
537-{% endif %}{% if display_ubuntu_download %}
538+ <form action="apt://{{ application.package_name }}" method="GET">
539+ <input type="submit" value="{% if application.for_purchase %}Purchase{% else %}Install{% endif %} {{ application.name }}" />
540+ </form>
541+{% endif %}
542+{% if display_ubuntu_download %}
543 <a href="http://www.ubuntu.com/download" class="awesome">{% trans "Download Ubuntu" %}</a>
544 {% endif %}
545
546
547=== modified file 'src/webcatalog/templatetags/webcatalog.py'
548--- src/webcatalog/templatetags/webcatalog.py 2011-06-29 15:37:50 +0000
549+++ src/webcatalog/templatetags/webcatalog.py 2011-07-04 15:54:51 +0000
550@@ -70,7 +70,7 @@
551 distroseries__code_name=useragent.distroseries)
552 app_in_series_url = reverse(
553 'wc-package-detail',
554- args=[application.package_name, useragent.distroseries])
555+ args=[useragent.distroseries, application.package_name])
556 template_context['message_text'] = _(
557 '{application_name} is also available for '
558 '<a href="{app_in_series_url}">your version of '
559
560=== modified file 'src/webcatalog/tests/test_commands.py'
561--- src/webcatalog/tests/test_commands.py 2011-06-30 16:38:56 +0000
562+++ src/webcatalog/tests/test_commands.py 2011-07-04 15:54:51 +0000
563@@ -367,6 +367,7 @@
564 def setUp(self):
565 curdir = os.path.dirname(__file__)
566 sca_apps_file = os.path.join(curdir, 'test_data', 'sca_apps.txt')
567+ self.distroseries = DistroSeries.objects.create(code_name='natty')
568 with open(sca_apps_file) as content:
569 self.response_content = content.read()
570 mock_response = Mock()
571@@ -381,7 +382,8 @@
572 apps_for_purchase = Application.objects.filter(for_purchase=True)
573 self.assertEqual(0, len(apps_for_purchase))
574 call_command('import_for_purchase_apps')
575- app_for_purchase = Application.objects.get(name='MyApp')
576+ app_for_purchase = Application.objects.get(name='MyApp',
577+ distroseries=self.distroseries)
578 self.assertEqual(True, app_for_purchase.for_purchase)
579 self.assertTrue(app_for_purchase.description.find('hello') > -1)
580
581@@ -399,3 +401,38 @@
582 self.assertTrue(second > first)
583 # number of apps should not increase
584 self.assertEqual(second, third)
585+
586+ @patch('urllib.urlopen')
587+ def test_app_gets_distroseries(self, mock_urllib):
588+ mock_urllib.return_value = self.mock_response
589+ call_command('import_for_purchase_apps')
590+ app = Application.objects.get(name='MyApp',
591+ distroseries=self.distroseries)
592+ self.assertEqual(2, len(app.available_distroseries()))
593+
594+ @patch('urllib.urlopen')
595+ def test_app_gets_price(self, mock_urllib):
596+ mock_urllib.return_value = self.mock_response
597+ call_command('import_for_purchase_apps')
598+ app = Application.objects.get(name='MyApp',
599+ distroseries=self.distroseries)
600+ self.assertEqual(2.50, app.price)
601+
602+ @patch('urllib.urlopen')
603+ def test_app_gets_updated(self, mock_urllib):
604+ mock_urllib.return_value = self.mock_response
605+ call_command('import_for_purchase_apps')
606+ updated = self.response_content.replace('hello', 'bye')
607+ self.mock_response.read.return_value = updated
608+ call_command('import_for_purchase_apps')
609+ app = Application.objects.get(name='MyApp',
610+ distroseries=self.distroseries)
611+ self.assertEqual('bye', app.package_name)
612+
613+ @patch('urllib.urlopen')
614+ def test_app_gets_icon(self, mock_urllib):
615+ mock_urllib.return_value = self.mock_response
616+ call_command('import_for_purchase_apps')
617+ app = Application.objects.get(name='MyApp',
618+ distroseries=self.distroseries)
619+ self.assertEqual(6461, app.icon.size)
620
621=== modified file 'src/webcatalog/tests/test_data/sca_apps.txt'
622--- src/webcatalog/tests/test_data/sca_apps.txt 2011-06-17 21:27:33 +0000
623+++ src/webcatalog/tests/test_data/sca_apps.txt 2011-07-04 15:54:51 +0000
624@@ -4,14 +4,14 @@
625 "signing_key_id": "",
626 "description": "MyAppTagline\nThe classic greeting, and a good example\r\n The GNU hello program produces a familiar, friendly greeting. It\r\n allows non-programmers to use a classic computer science tool which\r\n would otherwise be unavailable to them.\r\n .\r\n Seriously, though: this is an example of how to do a Debian package.\r\n It is the Debian version of the GNU Project's `hello world' program\r\n (which is itself an example for the GNU Project).",
627 "package_name": "hello",
628- "series": {},
629- "price": "1",
630+ "series": {"maverick": ["i386", "amd64"], "natty": ["i386", "amd64"]},
631+ "price": "2.50",
632 "archive_id": "launchpad_zematynnad2/myppa",
633 "icon_data": "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",
634 "screenshot_url": "",
635 "archive_root": "",
636 "tos_url": "",
637- "icon_url": "http://localhost:8000/site_media/icons/2011/06/eg_64x64_______.png",
638+ "icon_url": "http://localhost:8000/site_media/icons/2011/06/eg_64x64.png",
639 "categories": "Audio",
640 "name": "MyApp"
641 }
642
643=== modified file 'src/webcatalog/tests/test_models.py'
644--- src/webcatalog/tests/test_models.py 2011-06-28 15:03:12 +0000
645+++ src/webcatalog/tests/test_models.py 2011-07-04 15:54:51 +0000
646@@ -76,7 +76,7 @@
647 dept = app.departments.get()
648 expected = [{'name': 'Get Software', 'url': reverse('wc-index')},
649 {'name': dept.name, 'url': reverse('wc-department',
650- args=[dept.id])},
651+ args=[app.distroseries.code_name, dept.id])},
652 {'name': app.name, 'url': reverse('wc-package-detail',
653 args=[app.distroseries.code_name, app.package_name])}]
654
655@@ -89,16 +89,16 @@
656 dept = app.departments.get()
657 expected = [{'name': 'Get Software', 'url': reverse('wc-index')},
658 {'name': dept.parent.name, 'url': reverse('wc-department',
659- args=[dept.parent.id])},
660+ args=[app.distroseries.code_name, dept.parent.id])},
661 {'name': dept.name, 'url': reverse('wc-department',
662- args=[dept.id])},
663+ args=[app.distroseries.code_name, dept.id])},
664 {'name': app.name, 'url': reverse('wc-package-detail',
665 args=[app.distroseries.code_name, app.package_name])}]
666
667 self.assertEquals(expected, app.crumbs())
668
669
670-def DepartmentTestCse(TestCaseWithFactory):
671+def DepartmentTestCase(TestCaseWithFactory):
672 def test_normalized_name(self):
673 cases = {
674 'Foo': 'Foo',
675@@ -129,3 +129,14 @@
676 args=[dept.id])}]
677
678 self.assertEquals(expected, dept.crumbs())
679+
680+ def test_crumbs_with_distroseries(self):
681+ parent = self.factory.make_department('Foo')
682+ dept = self.factory.make_department('Bar', parent=parent)
683+ expected = [{'name': 'Get Software', 'url': reverse('wc-index')},
684+ {'name': dept.parent.name, 'url': reverse('wc-department',
685+ args=['frobbly', dept.parent.id])},
686+ {'name': dept.name, 'url': reverse('wc-department',
687+ args=['frobbly', dept.id])}]
688+
689+ self.assertEquals(expected, dept.crumbs(distro='frobbly'))
690
691=== modified file 'src/webcatalog/tests/test_templatetags.py'
692--- src/webcatalog/tests/test_templatetags.py 2011-06-29 14:09:36 +0000
693+++ src/webcatalog/tests/test_templatetags.py 2011-07-04 15:54:51 +0000
694@@ -94,7 +94,7 @@
695 context = Context(
696 dict(user_agent=self.make_useragent(ubuntu_version='10.04')))
697
698- lucid_app_url = reverse('wc-package-detail', args=['pkgfoo', 'lucid'])
699+ lucid_app_url = reverse('wc-package-detail', args=['lucid', 'pkgfoo'])
700 self.assertEqual(dict(
701 application=natty_application, display_install_button=False,
702 message_text='Package Foo is also available for '
703
704=== modified file 'src/webcatalog/tests/test_views.py'
705--- src/webcatalog/tests/test_views.py 2011-07-01 17:04:53 +0000
706+++ src/webcatalog/tests/test_views.py 2011-07-04 15:54:51 +0000
707@@ -103,7 +103,7 @@
708 def test_link_to_apt_package(self):
709 response, app = self.get_app_and_response()
710
711- self.assertContains(response, '<a href="apt://pkgfoo"')
712+ self.assertContains(response, 'action="apt://pkgfoo"')
713
714 def test_button_for_non_puchase_app(self):
715 response, app = self.get_app_and_response()
716@@ -129,7 +129,7 @@
717 response, app = self.get_app_and_response(name="<a>Escape me",
718 useragent="blah Ubuntu/10.04 blah")
719
720- lucid_app_url = reverse('wc-package-detail', args=['pkgfoo', 'lucid'])
721+ lucid_app_url = reverse('wc-package-detail', args=['lucid', 'pkgfoo'])
722 self.assertContains(response,
723 '&lt;a&gt;Escape me is also available for '
724 '<a href="{lucid_app_url}">your version of Ubuntu</a>.'.format(
725@@ -313,9 +313,10 @@
726 def test_department_contains_links_to_subdepartments(self):
727 dept = self.factory.make_department('foo')
728 subdept = self.factory.make_department('bar', parent=dept)
729+ distro = self.factory.make_distroseries(code_name='lucid')
730
731 response = self.client.get(reverse('wc-department', args=[
732- settings.DEFAULT_DISTRO, dept.id]))
733+ 'lucid', dept.id]))
734
735 self.assertContains(response, reverse('wc-department',
736 args=[subdept.id]))
737@@ -333,9 +334,10 @@
738
739 def test_department_with_no_subdepts_doesnt_contain_header(self):
740 dept = self.factory.make_department('bar')
741+ distro = self.factory.make_distroseries(code_name='lucid')
742
743 response = self.client.get(reverse('wc-department', args=[
744- settings.DEFAULT_DISTRO, dept.id]))
745+ 'lucid', dept.id]))
746
747 self.assertNotContains(response, 'Subsections')
748
749@@ -361,7 +363,8 @@
750
751 def test_invalid_page_doesnt_error(self):
752 dept = self.factory.make_department('bar')
753- url = reverse('wc-department', args=[settings.DEFAULT_DISTRO, dept.id])
754+ distro = self.factory.make_distroseries(code_name='lucid')
755+ url = reverse('wc-department', args=['lucid', dept.id])
756 response = self.client.get(url, data={'page': 'aef8'})
757 page = response.context['page']
758 self.assertEqual(1, page.number)
759@@ -376,6 +379,8 @@
760
761 def test_department_includes_right_navigation(self):
762 dept = self.factory.make_department('bar')
763+ default = self.factory.make_distroseries(
764+ code_name=settings.DEFAULT_DISTRO, version='11.04')
765 lucid = self.factory.make_distroseries(code_name='lucid',
766 version='10.04')
767 maverick = self.factory.make_distroseries(code_name='maverick',
768@@ -388,3 +393,10 @@
769 url = reverse('wc-department', args=[ds, dept.id])
770 self.assertContains(response, '<a href="{0}">Ubuntu'.format(url))
771
772+ def test_invalid_distroseries_returns_404(self):
773+ dept = self.factory.make_department('bar')
774+
775+ response = self.client.get(reverse('wc-department', args=[
776+ 'amnesiac', dept.id]))
777+
778+ self.assertEqual(404, response.status_code)
779\ No newline at end of file
780
781=== modified file 'src/webcatalog/views.py'
782--- src/webcatalog/views.py 2011-07-01 16:18:58 +0000
783+++ src/webcatalog/views.py 2011-07-04 15:54:51 +0000
784@@ -108,6 +108,9 @@
785 return HttpResponseRedirect(
786 reverse('wc-department', args=[distro, dept_id]))
787
788+ # Attempt to retrieve the DistroSeries to ensure that it actually exists
789+ get_object_or_404(DistroSeries, code_name=distro)
790+
791 dept = get_object_or_404(Department, pk=dept_id)
792 subdepts = Department.objects.filter(parent=dept)
793 subdepts = subdepts.order_by('name')

Subscribers

People subscribed via source and target branches