Merge lp:~canonical-isd-hackers/ubuntu-webcatalog/useragent_778562 into lp:ubuntu-webcatalog

Proposed by Danny Tamez
Status: Merged
Approved by: Danny Tamez
Approved revision: 26
Merged at revision: 26
Proposed branch: lp:~canonical-isd-hackers/ubuntu-webcatalog/useragent_778562
Merge into: lp:ubuntu-webcatalog
Diff against target: 507 lines (+217/-60)
10 files modified
django_project/config/main.cfg (+1/-0)
src/webcatalog/models.py (+5/-5)
src/webcatalog/schema.py (+1/-0)
src/webcatalog/templates/webcatalog/application_detail.html (+10/-1)
src/webcatalog/templates/webcatalog/application_overview_snippet.html (+1/-1)
src/webcatalog/tests/factory.py (+3/-3)
src/webcatalog/tests/test_models.py (+3/-3)
src/webcatalog/tests/test_views.py (+111/-34)
src/webcatalog/urls.py (+5/-4)
src/webcatalog/views.py (+77/-9)
To merge this branch: bzr merge lp:~canonical-isd-hackers/ubuntu-webcatalog/useragent_778562
Reviewer Review Type Date Requested Status
David Owen (community) Approve
Review via email: mp+65553@code.launchpad.net

Commit message

Enhances the application details page to show more helpful messages based on the useragent of the request.

Description of the change

This branch adds support for the application details page to display more helpful messages. In particular, the useragent in the request is examined to determine the user's OS and distroseries. Based on this information the app details page will display alternative links to other distros or a link to the ubuntu download page if the OS detected is not Linux. At this point there is no way to actually determine the distro the user is running so a default guess is used (which is configured in the settings).

To post a comment you must log in.
Revision history for this message
David Owen (dsowen) wrote :

I really like how you factored the tests!

Approved, with recommendation that Linux fields not be set in get_user_os() when not a Linux browser.

review: Approve
Revision history for this message
ISD Branch Mangler (isd-branches-mangler) wrote :

Attempt to merge into lp:ubuntu-webcatalog failed due to conflicts:

text conflict in src/webcatalog/models.py
text conflict in src/webcatalog/templates/webcatalog/department_overview.html
text conflict in src/webcatalog/templates/webcatalog/search_results.html
text conflict in src/webcatalog/urls.py
text conflict in src/webcatalog/views.py

Revision history for this message
Michael Nelson (michael.nelson) wrote :

Hey Danny! I was just pulling this up to find out why it hadn't landed yet (argh, conflicts), but noticed that you're not grabbing the distroseries from the user-agent when present... just wondering why?

Here's a screenshot showing Ubuntu/11.04 for me with chromium:
http://people.canonical.com/~michaeln/tmp/778562-headers.png

It won't be available in all browsers, but those that are patched by ubuntu should have it.

I really don't think we should be guessing at all, as it could provide a very confusing user experience. Either we know their distroseries and take advantage of that, or we don't and do some baseline functionality.

Revision history for this message
Michael Nelson (michael.nelson) wrote :

> Here's a screenshot showing Ubuntu/11.04 for me with chromium:
> http://people.canonical.com/~michaeln/tmp/778562-headers.png
>
> It won't be available in all browsers, but those that are patched by ubuntu
> should have it.

I just found bug 709125 which says that it'll no longer be supported in FF, but that ubufox package can add it for certain websites (as they have done for apt.ubuntu.com.

If we did ask them to also include (eventually) the web catalog, would it be SRUd to older series? Hrm.

26. By Danny Tamez

Merged trunk and resolved conflicts.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'django_project/config/main.cfg'
2--- django_project/config/main.cfg 2011-06-23 12:38:12 +0000
3+++ django_project/config/main.cfg 2011-06-28 15:06:35 +0000
4@@ -93,6 +93,7 @@
5 serve_site_media = True
6 sca_api_url = https://sc.staging.ubuntu.com/api/2.0/
7 disk_apt_cache_location = /tmp/webcat_cache
8+default_distro = natty
9
10 [google]
11 google_analytics_id = UA-1018242-24
12
13=== modified file 'src/webcatalog/models.py'
14--- src/webcatalog/models.py 2011-06-23 14:20:52 +0000
15+++ src/webcatalog/models.py 2011-06-28 15:06:35 +0000
16@@ -62,7 +62,7 @@
17 popcon = models.IntegerField()
18 channel = models.CharField(max_length=255, blank=True)
19 screenshot_url = models.URLField(blank=True,
20- help_text="Only use this if it is other than the normal screenshot url.")
21+ help_text="Only use this if it is other than the normal screenshot url.")
22 mimetype = models.CharField(max_length=2048, blank=True)
23 architectures = models.CharField(max_length=255, blank=True)
24 keywords = models.CharField(max_length=255, blank=True)
25@@ -73,6 +73,9 @@
26 icon_name = models.CharField(max_length=255, blank=True)
27 icon = models.ImageField(upload_to='icons/%Y/%m', max_length=200,
28 null=True, blank=True)
29+ for_purchase = models.BooleanField(default=False)
30+ archive_id = models.CharField(max_length=64, null=True,
31+ db_index=True, blank=True, unique=True)
32
33 # Other desktop fields used by s-c
34 # x-gnome-fullname
35@@ -83,9 +86,6 @@
36 # (using python-apt - as above we'll need access to info from different
37 # series etc.)
38 description = models.TextField(blank=True)
39- for_purchase = models.BooleanField(default=False)
40- archive_id = models.CharField(max_length=64, null=True,
41- db_index=True, blank=True, unique=True)
42
43 def __unicode__(self):
44 return u"{0} ({1})".format(self.name, self.package_name)
45@@ -127,7 +127,7 @@
46 else:
47 crumbs = [{'name': 'Get Software', 'url': reverse('wc-index')}]
48 crumbs.append({'name': self.name, 'url': reverse('wc-package-detail',
49- args=[self.package_name])})
50+ args=[self.distroseries.code_name, self.package_name])})
51 return crumbs
52
53 class Department(models.Model):
54
55=== modified file 'src/webcatalog/schema.py'
56--- src/webcatalog/schema.py 2011-06-27 12:07:30 +0000
57+++ src/webcatalog/schema.py 2011-06-28 15:06:35 +0000
58@@ -47,6 +47,7 @@
59 webcatalog.serve_site_media = BoolConfigOption(default=True)
60 webcatalog.sca_api_url = StringConfigOption()
61 webcatalog.disk_apt_cache_location = StringConfigOption()
62+ webcatalog.default_distro = StringConfigOption()
63 webcatalog.page_batch_size = IntConfigOption(default=20)
64
65 google = ConfigSection()
66
67=== modified file 'src/webcatalog/templates/webcatalog/application_detail.html'
68--- src/webcatalog/templates/webcatalog/application_detail.html 2011-06-23 14:20:52 +0000
69+++ src/webcatalog/templates/webcatalog/application_detail.html 2011-06-28 15:06:35 +0000
70@@ -27,7 +27,16 @@
71 <img src="http://screenshots.ubuntu.com/thumbnail-with-version/{{ application.package_name }}/ignored" />
72 </div>
73 <p>{{ application.description }}</p>
74- <a href="apt://{{ application.package_name }}" class="awesome">{%if application.for_purchase %}Install{% else %}Download{% endif %} {{ application.name }}</a>
75+ {% if hide_button %}
76+ {{ message_text }}
77+ {% else %}
78+ <a href="apt://{{ application.package_name }}" class="awesome">{{ button_text }} {{ application.name }}</a>
79+ {% endif %}
80+ {% if links %}
81+ {% for text, link in links %}
82+ <br><a href="{{link}}" class="awesome">{{ text }}</a>
83+ {% endfor %}
84+ {% endif %}
85 </div>
86 <div class="license">
87 <table>
88
89=== modified file 'src/webcatalog/templates/webcatalog/application_overview_snippet.html'
90--- src/webcatalog/templates/webcatalog/application_overview_snippet.html 2011-06-23 14:20:52 +0000
91+++ src/webcatalog/templates/webcatalog/application_overview_snippet.html 2011-06-28 15:06:35 +0000
92@@ -5,7 +5,7 @@
93 <img src="{{ STATIC_URL }}images/noicon_32.png"/>
94 {% endif %}
95 <h3>
96- <a href="{% url wc-package-detail app.package_name %}">{{ app.name }}</a>
97+ <a href="{% url wc-package-detail app.distroseries.code_name app.package_name %}">{{ app.name }}</a>
98 </h3>
99 <p>{{ app.comment }}</p>
100 </div>
101
102=== modified file 'src/webcatalog/tests/factory.py'
103--- src/webcatalog/tests/factory.py 2011-05-06 09:46:17 +0000
104+++ src/webcatalog/tests/factory.py 2011-06-28 15:06:35 +0000
105@@ -24,7 +24,7 @@
106 import os
107 from itertools import count
108
109-from django.core.files.images import ImageFile
110+from django.contrib.auth.models import User
111 from django.test import TestCase
112
113 from webcatalog.models import (
114@@ -76,7 +76,7 @@
115
116 def make_application(self, package_name=None, name=None,
117 comment=None, description=None, icon_name='', icon=None,
118- distroseries=None):
119+ distroseries=None, arch='i686'):
120 if name is None:
121 name = self.get_unique_string(prefix='Readable Name')
122 if package_name is None:
123@@ -91,7 +91,7 @@
124 return Application.objects.create(
125 package_name=package_name, name=name, comment=comment,
126 description=description, popcon=999, icon=icon,
127- icon_name=icon_name, distroseries=distroseries)
128+ icon_name=icon_name, distroseries=distroseries, architectures=arch)
129
130 def make_department(self, name, parent=None):
131 return Department.objects.create(name=name, parent=parent)
132
133=== modified file 'src/webcatalog/tests/test_models.py'
134--- src/webcatalog/tests/test_models.py 2011-06-23 14:49:53 +0000
135+++ src/webcatalog/tests/test_models.py 2011-06-28 15:06:35 +0000
136@@ -65,7 +65,7 @@
137 self.assertEquals([], list(app.departments.all()))
138 expected = [{'name': 'Get Software', 'url': reverse('wc-index')},
139 {'name': app.name, 'url': reverse('wc-package-detail',
140- args=[app.package_name])}]
141+ args=[app.distroseries.code_name, app.package_name])}]
142
143 self.assertEquals(expected, app.crumbs())
144
145@@ -78,7 +78,7 @@
146 {'name': dept.name, 'url': reverse('wc-department',
147 args=[dept.id])},
148 {'name': app.name, 'url': reverse('wc-package-detail',
149- args=[app.package_name])}]
150+ args=[app.distroseries.code_name, app.package_name])}]
151
152 self.assertEquals(expected, app.crumbs())
153
154@@ -93,7 +93,7 @@
155 {'name': dept.name, 'url': reverse('wc-department',
156 args=[dept.id])},
157 {'name': app.name, 'url': reverse('wc-package-detail',
158- args=[app.package_name])}]
159+ args=[app.distroseries.code_name, app.package_name])}]
160
161 self.assertEquals(expected, app.crumbs())
162
163
164=== modified file 'src/webcatalog/tests/test_views.py'
165--- src/webcatalog/tests/test_views.py 2011-06-27 13:39:39 +0000
166+++ src/webcatalog/tests/test_views.py 2011-06-28 15:06:35 +0000
167@@ -22,8 +22,12 @@
168 with_statement,
169 )
170
171+from django.conf import settings
172 from django.core.urlresolvers import reverse
173
174+from mock import patch
175+
176+from webcatalog.models import DistroSeries
177 from webcatalog.tests.factory import TestCaseWithFactory
178 from webcatalog.tests.helpers import patch_settings
179
180@@ -35,23 +39,52 @@
181 ]
182
183
184+WINDOWS_USERAGENT = ('Mozilla/5.0 (Windows NT 5.1; rv:2.0) '
185+ 'Gecko/20100101 Firefox/4.0')
186+
187+UBUNTU_USERAGENT = ('Mozilla/5.0 (X11; Linux x86_64; rv:2.0.1) '
188+ 'Gecko/20100101 Firefox/4.0.1')
189+
190+
191 class PackageDetailTestCase(TestCaseWithFactory):
192
193+ def get_app_and_response(self, code_name='natty', version='11.04',
194+ arch='x86_64', name=None, comment=None,
195+ description=None, detail_distro=None,
196+ detail_package=None, for_purchase=False,
197+ useragent=UBUNTU_USERAGENT):
198+ series = DistroSeries(code_name=code_name, version=version)
199+ series.save()
200+ app = self.factory.make_application(package_name='pkgfoo',
201+ distroseries=series, arch=arch,
202+ name=name, comment=comment,
203+ description=description)
204+ if for_purchase:
205+ app.for_purchase = True
206+ app.save()
207+ if not detail_distro:
208+ detail_distro = app.distroseries.code_name
209+ if not detail_package:
210+ detail_package = app.package_name
211+
212+ url = reverse('wc-package-detail', args=[detail_distro, detail_package])
213+
214+ if useragent:
215+ response = self.client.get(url, HTTP_USER_AGENT=useragent)
216+ else:
217+ response = self.client.get(url)
218+ return response, app
219+
220 def test_renders_correct_template(self):
221- self.factory.make_application(package_name='pkgfoo')
222- url = reverse('wc-package-detail', args=['pkgfoo'])
223-
224- response = self.client.get(url)
225+ response, app = self.get_app_and_response()
226
227 self.assertTemplateUsed(response, 'webcatalog/application_detail.html')
228
229 def test_includes_application_details(self):
230 # The details of an application are included in the rendered html.
231- self.factory.make_application(package_name='pkgfoo', name="My app foo",
232- comment="The best app eva.", description="A long description.")
233- url = reverse('wc-package-detail', args=['pkgfoo'])
234-
235- response = self.client.get(url)
236+ response, app = self.get_app_and_response(name='My app foo',
237+ comment='The best app eva.',
238+ description='A long description.')
239
240 self.assertContains(response, "My app foo")
241 self.assertContains(response, "The best app eva.")
242@@ -59,40 +92,83 @@
243
244 def test_screenshot_thumbnail_url(self):
245 # The thumbnail link to screenshots is included.
246- self.factory.make_application(package_name='pkgfoo')
247- url = reverse('wc-package-detail', args=['pkgfoo'])
248-
249- response = self.client.get(url)
250+ response, app = self.get_app_and_response()
251
252 self.assertContains(
253 response, '<img src="http://screenshots.ubuntu.com/'
254 'thumbnail-with-version/pkgfoo/ignored"')
255
256 def test_link_to_apt_package(self):
257- self.factory.make_application(package_name='pkgfoo')
258- url = reverse('wc-package-detail', args=['pkgfoo'])
259-
260- response = self.client.get(url)
261-
262- self.assertContains( response, '<a href="apt://pkgfoo"')
263+ response, app = self.get_app_and_response()
264+
265+ self.assertContains(response, '<a href="apt://pkgfoo"')
266
267 def test_button_for_non_puchase_app(self):
268- app = self.factory.make_application(package_name='pkgfoo')
269- url = reverse('wc-package-detail', args=['pkgfoo'])
270-
271- response = self.client.get(url)
272-
273- self.assertContains( response, 'Download %s' % app.name)
274+ response, app = self.get_app_and_response()
275+
276+ self.assertContains(response, 'Install %s' % app.name)
277+ self.assertNotContains(response, 'Purcchase %s' % app.name)
278
279 def test_button_for_for_puchase_app(self):
280- app = self.factory.make_application(package_name='pkgfoo')
281- app.for_purchase = True
282- app.save()
283- url = reverse('wc-package-detail', args=['pkgfoo'])
284-
285- response = self.client.get(url)
286-
287- self.assertContains( response, 'Install %s' % app.name)
288+ response, app = self.get_app_and_response(for_purchase=True)
289+
290+ self.assertContains(response, 'Purchase %s' % app.name)
291+ self.assertNotContains(response, 'Install %s' % app.name)
292+
293+ def test_button_for_matching_distroseries(self):
294+ response, app = self.get_app_and_response(for_purchase=True)
295+
296+ self.assertContains(response, 'Purchase %s' % app.name)
297+ self.assertNotContains(response, 'Install %s' % app.name)
298+
299+ def test_button_for_non_matching_distroseries_but_available(self):
300+ series = DistroSeries(code_name='natty', version='11.04')
301+ series.save()
302+ app2 = self.factory.make_application(package_name='pkgfoo',
303+ distroseries=series,
304+ arch='x86_64')
305+ app2.save()
306+ response, app = self.get_app_and_response(code_name='oneric',
307+ version='11.10',
308+ for_purchase=True,
309+ detail_distro='oneric')
310+
311+ self.assertContains(response, 'Not running %s?' %
312+ settings.DEFAULT_DISTRO)
313+ self.assertContains(response, 'Purchase %s for oneric' % app.name)
314+ self.assertContains(response, '/cat/applications/oneric/pkgfoo/')
315+
316+ @patch('webcatalog.views.get_user_os')
317+ def test_button_for_non_matching_distroseries_and_unavailable(self,
318+ mock_get_user_os):
319+ # patch user agent check so we can have distro is a guess be False
320+ # otherwise we'll get the 'Not using natty?' message
321+ fake = {'is_linux': True, 'distro': 'natty', 'distro_is_a_guess': False,
322+ 'arch': 'x86_64'}
323+ mock_get_user_os.return_value = fake
324+ response, app = self.get_app_and_response(code_name='oneric',
325+ version='11.10',
326+ for_purchase=True,
327+ detail_distro='oneric')
328+
329+ self.assertNotContains(response, 'Purchase %s' % app.name)
330+ not_avail_msg = 'Not available for your version of Ubuntu'
331+ self.assertContains(response, not_avail_msg)
332+
333+ def test_button_for_non_matching_platform(self):
334+ response, app = self.get_app_and_response(useragent=WINDOWS_USERAGENT)
335+
336+ self.assertNotContains(response, 'Purchase %s' % app.name)
337+ self.assertContains(response, 'Not available for your platform')
338+ self.assertContains(response, 'http://www.ubuntu.com/download')
339+ self.assertContains(response, 'Download Ubuntu')
340+
341+ def test_app_detail_with_no_useragent(self):
342+ # don't set the useragent
343+ response, app = self.get_app_and_response(useragent=None)
344+
345+ self.assertNotContains(response, 'Purchase %s' % app.name)
346+
347
348
349 class SearchTestCase(TestCaseWithFactory):
350@@ -201,6 +277,7 @@
351 self.assertEqual(1, page.number)
352
353
354+
355 class OverviewTestCase(TestCaseWithFactory):
356 def test_index_contains_links_to_departments(self):
357 dept = self.factory.make_department('foo')
358@@ -227,7 +304,7 @@
359 response = self.client.get(reverse('wc-department', args=[dept.id]))
360
361 self.assertContains(response, reverse('wc-package-detail',
362- args=[app.package_name]))
363+ args=[app.distroseries.code_name, app.package_name]))
364
365 def test_department_with_no_subdepts_doesnt_contain_header(self):
366 dept = self.factory.make_department('bar')
367
368=== modified file 'src/webcatalog/urls.py'
369--- src/webcatalog/urls.py 2011-06-23 14:20:52 +0000
370+++ src/webcatalog/urls.py 2011-06-28 15:06:35 +0000
371@@ -22,9 +22,8 @@
372 with_statement,
373 )
374 from django.conf.urls.defaults import patterns, url
375-from django.views.generic.detail import DetailView
376
377-from webcatalog.models import Application
378+from webcatalog.views import application_detail, application_detail_no_distro
379
380 __metaclass__ = type
381 __all__ = [
382@@ -36,7 +35,9 @@
383 url(r'^$', 'index', name='wc-index'),
384 url(r'^department/(?P<dept_id>\d+)/$', 'department_overview',
385 name='wc-department'),
386- url(r'^applications/(?P<package_name>[-.+\w]+)/$', 'application_detail',
387- name="wc-package-detail"),
388+ url(r'^applications/(?P<distro>[-.+\w]+)/(?P<slug>[-.+\w]+)/$',
389+ application_detail, name="wc-package-detail"),
390+ url(r'^applications/(?P<slug>[-.+\w]+)/$', application_detail_no_distro,
391+ name="wc-package-detail-no-distro"),
392 url(r'^search/$', 'search', name="wc-search"),
393 )
394
395=== modified file 'src/webcatalog/views.py'
396--- src/webcatalog/views.py 2011-06-27 14:08:41 +0000
397+++ src/webcatalog/views.py 2011-06-28 15:06:35 +0000
398@@ -55,6 +55,7 @@
399 return int(page_num)
400 return 1
401
402+
403 def search(request):
404 apps = []
405 query = ''
406@@ -82,12 +83,14 @@
407 return render_to_response('webcatalog/search_results.html',
408 context_instance=context)
409
410+
411 def index(request):
412 depts = Department.objects.filter(parent=None).order_by('name')
413 context = RequestContext(request, dict={'depts': depts})
414 return render_to_response('webcatalog/index.html',
415 context_instance=context)
416
417+
418 def department_overview(request, dept_id):
419 dept = get_object_or_404(Department, pk=dept_id)
420 subdepts = Department.objects.filter(parent=dept)
421@@ -104,12 +107,77 @@
422 context_instance=context)
423
424
425-def application_detail(request, package_name):
426- app = get_object_or_404(Application, package_name=package_name)
427-
428- context = RequestContext(request, {
429- 'application': app,
430- 'breadcrumbs': app.crumbs(),
431- })
432- return render_to_response('webcatalog/application_detail.html',
433- context_instance=context)
434+def application_detail_no_distro(request, slug):
435+ default_distro = settings.DEFAULT_DISTRO
436+ return application_detail(request, default_distro, slug)
437+
438+
439+def application_detail(request, distro, slug):
440+ os = get_user_os(request)
441+ app = get_object_or_404(Application, package_name=slug,
442+ distroseries__code_name=distro)
443+ button_text = 'Purchase' if app.for_purchase else 'Install'
444+ right_platform = os['is_linux']
445+ right_arch = app.architectures.find(os['arch']) > -1
446+ right_distro = os['distro'].find(app.distroseries.code_name) > -1
447+ perfect_match = (right_platform and right_distro and right_arch)
448+ message_text = 'Not available for your version of Ubuntu'
449+ links = []
450+ if not perfect_match:
451+ if not right_platform:
452+ message_text = "Not available for your platform"
453+ links.append(('Download Ubuntu', 'http://www.ubuntu.com/download'))
454+ elif not right_distro and os['distro_is_a_guess']:
455+ # see if we have this package for *other* distros
456+ apps_found = Application.objects.filter(package_name=slug).exclude(
457+ distroseries__code_name=os['distro'])
458+ if apps_found:
459+ message_text = ('Not running %s?' % os['distro'])
460+ for app_found in apps_found:
461+ args = [app.distroseries.code_name, app.package_name]
462+ url = reverse('wc-package-detail', args=args)
463+ text = "%s %s for %s" % (button_text, app.name,
464+ app_found.distroseries.code_name)
465+ links.append((text, url))
466+ elif not right_distro:
467+ app_found = Application.objects.filter(package_name=slug,
468+ distroseries__code_name=os['distro'])
469+ if app_found:
470+ args = [app.distroseries.code_name, app.package_name]
471+ url = reverse('wc-package-detail', args=args)
472+ message_text = ('%s is also available for your version '
473+ 'of Ubuntu' % app.name)
474+ links.append(("%s for os['distro']" % button_text, url))
475+
476+ ctx = {'application': app, 'button_text': button_text,
477+ 'hide_button': not perfect_match, 'message_text': message_text,
478+ 'breadcrumbs': app.crumbs(), 'links': links}
479+
480+ context = RequestContext(request, dict=ctx)
481+ return render_to_response('webcatalog/application_detail.html', context)
482+
483+
484+def get_user_os(request):
485+ os = {'is_linux': False, 'distro': 'unknown', 'arch': 'unknown'}
486+ try:
487+ ua = request.META['HTTP_USER_AGENT']
488+ except KeyError:
489+ return os
490+ os['is_linux'] = ua.find('X11; Linux') > -1
491+ # Until we can figure out a way to actually get the distro we'll
492+ # assume a default distro
493+ if os['is_linux']:
494+ os['distro'] = settings.DEFAULT_DISTRO
495+ os['distro_is_a_guess'] = True
496+ else:
497+ os['distro'] = ''
498+ os['distro_is_a_guess'] = False
499+
500+ is_x86_64 = ua.find('x86_64')
501+ is_686 = ua.find('i686')
502+ if is_x86_64:
503+ os['arch'] = 'x86_64'
504+ elif is_686:
505+ os['arch'] = 'i686'
506+ # TODO: DT Get other architectures
507+ return os

Subscribers

People subscribed via source and target branches