Merge lp:~zematynnad/ubuntu-webcatalog/display_hardware_953286 into lp:ubuntu-webcatalog

Proposed by Danny Tamez
Status: Merged
Approved by: Danny Tamez
Approved revision: 89
Merged at revision: 91
Proposed branch: lp:~zematynnad/ubuntu-webcatalog/display_hardware_953286
Merge into: lp:ubuntu-webcatalog
Diff against target: 544 lines (+368/-7)
12 files modified
src/webcatalog/api/handlers.py (+0/-3)
src/webcatalog/forms.py (+5/-0)
src/webcatalog/hw.py (+147/-0)
src/webcatalog/management/commands/import_app_install_data.py (+0/-1)
src/webcatalog/migrations/0019_add_debtags_to_application.py (+162/-0)
src/webcatalog/models/applications.py (+1/-0)
src/webcatalog/static/css/webcatalog.css (+8/-1)
src/webcatalog/templates/webcatalog/application_detail.html (+10/-0)
src/webcatalog/tests/factory.py (+3/-2)
src/webcatalog/tests/test_forms.py (+13/-0)
src/webcatalog/tests/test_views.py (+15/-0)
src/webcatalog/views.py (+4/-0)
To merge this branch: bzr merge lp:~zematynnad/ubuntu-webcatalog/display_hardware_953286
Reviewer Review Type Date Requested Status
Łukasz Czyżykowski (community) Approve
Review via email: mp+99417@code.launchpad.net

Commit message

Adds display of hardware requirements to app detail page

Description of the change

Overview
=========
This branch adds a list of hardware requirements for apps to the application detail page.

Details
=========
The requirements are displayed in bullet fashion in the sidebar. Screenshot: http://simplest-image-hosting.net/png-0-qy2199
The requirements are gathered from the degtags. Since the debtags are not currently used in free applications we are not displaying hardware requirements for free apps at this time. Once they are then the import_app_install_data script can be updated. For the purchased apps, the debtags are available and are brought in during the import_for_purchase_apps script.

To Test
=======
$fab bootstrap test

To post a comment you must log in.
Revision history for this message
Łukasz Czyżykowski (lukasz-czyzykowski) wrote :

LGTM

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/webcatalog/api/handlers.py'
2--- src/webcatalog/api/handlers.py 2012-01-06 17:54:47 +0000
3+++ src/webcatalog/api/handlers.py 2012-03-26 20:58:01 +0000
4@@ -24,9 +24,6 @@
5 'ServerStatusHandler',
6 ]
7
8-import json
9-
10-from django.contrib.auth.models import User
11 from django.http import (
12 HttpResponse,
13 HttpResponseBadRequest,
14
15=== modified file 'src/webcatalog/forms.py'
16--- src/webcatalog/forms.py 2012-03-19 14:38:02 +0000
17+++ src/webcatalog/forms.py 2012-03-26 20:58:01 +0000
18@@ -23,6 +23,7 @@
19 )
20
21 import apt
22+import json
23 from ConfigParser import ConfigParser
24 from StringIO import StringIO
25
26@@ -32,6 +33,7 @@
27 from django.core.validators import URLValidator
28 from django.template.loader import render_to_string
29
30+from webcatalog.hw import get_hw_short_description
31 from webcatalog.models import Application
32
33 __metaclass__ = type
34@@ -137,6 +139,9 @@
35 app_data['description'] = description
36 if 'screenshot_urls' in app_data:
37 app_data['screenshot_urls'] = ",".join(app_data['screenshot_urls'])
38+ if 'debtags' in app_data and app_data['debtags']:
39+ app_data['debtags'] = json.dumps([get_hw_short_description(x)
40+ for x in app_data['debtags']])
41 try:
42 instance = Application.objects.get(
43 archive_id=app_data.get('archive_id'),
44
45=== added file 'src/webcatalog/hw.py'
46--- src/webcatalog/hw.py 1970-01-01 00:00:00 +0000
47+++ src/webcatalog/hw.py 2012-03-26 20:58:01 +0000
48@@ -0,0 +1,147 @@
49+# Copyright (C) 2012 Canonical
50+# -*- coding: utf-8 -*-
51+#
52+# Authors:
53+# Michael Vogt
54+#
55+# This program is free software; you can redistribute it and/or modify it under
56+# the terms of the GNU General Public License as published by the Free Software
57+# Foundation; version 3.
58+#
59+# This program is distributed in the hope that it will be useful, but WITHOUT
60+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
61+# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
62+# details.
63+#
64+# You should have received a copy of the GNU General Public License along with
65+# this program; if not, write to the Free Software Foundation, Inc.,
66+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
67+
68+from gettext import gettext as _
69+
70+# private extension over the debtagshw stuff
71+OPENGL_DRIVER_BLACKLIST_TAG = "x-hardware::opengl-driver-blacklist:"
72+
73+
74+TAG_DESCRIPTION = {
75+ # normal tags
76+ 'hardware::webcam': _('webcam'),
77+ 'hardware::digicam': _('digicam'),
78+ 'hardware::input:mouse': _('mouse'),
79+ 'hardware::input:joystick': _('joystick'),
80+ 'hardware::input:touchscreen': _('touchscreen'),
81+ 'hardware::gps': _('GPS'),
82+ 'hardware::laptop': _('notebook computer'),
83+ 'hardware::printer': _('printer'),
84+ 'hardware::scanner': _('scanner'),
85+ 'hardware::storage:cd': _('CD drive'),
86+ 'hardware::storage:cd-writer': _('CD burner'),
87+ 'hardware::storage:dvd': _('DVD drive'),
88+ 'hardware::storage:dvd-writer': _('DVD burner'),
89+ 'hardware::storage:floppy': _('floppy disk drive'),
90+ 'hardware::video:opengl': _('OpenGL hardware acceleration'),
91+ # "special" private tag extenstion that needs special handling
92+ OPENGL_DRIVER_BLACKLIST_TAG: _('Graphics driver that is not %s'),
93+}
94+
95+TAG_MISSING_DESCRIPTION = {
96+ 'hardware::digicam': _('This software requires a digital camera, but none '
97+ 'are currently connected'),
98+ 'hardware::webcam': _('This software requires a video camera, but none '
99+ 'are currently connected'),
100+ 'hardware::input:mouse': _('This software requires a mouse, '
101+ 'but none is currently setup.'),
102+ 'hardware::input:joystick': _('This software requires a joystick, '
103+ 'but none are currently connected.'),
104+ 'hardware::input:touchscreen': _('This software requires a touchscreen, '
105+ 'but the computer does not have one.'),
106+ 'hardware::gps': _('This software requires a GPS, '
107+ 'but the computer does not have one.'),
108+ 'hardware::laptop': _('This software is for notebook computers.'),
109+ 'hardware::printer': _('This software requires a printer, but none '
110+ 'are currently set up.'),
111+ 'hardware::scanner': _('This software requires a scanner, but none are '
112+ 'currently set up.'),
113+ 'hardware::stoarge:cd': _('This software requires a CD drive, but none '
114+ 'are currently connected.'),
115+ 'hardware::storage:cd-writer': _('This software requires a CD burner, '
116+ 'but none are currently connected.'),
117+ 'hardware::storage:dvd': _('This software requires a DVD drive, but none '
118+ 'are currently connected.'),
119+ 'hardware::storage:dvd-writer': _('This software requires a DVD burner, '
120+ 'but none are currently connected.'),
121+ 'hardware::storage:floppy': _('This software requires a floppy disk '
122+ 'drive, but none are currently connected.'),
123+ 'hardware::video:opengl': _('This computer does not have graphics fast '
124+ 'enough for this software.'),
125+ # private extension
126+ OPENGL_DRIVER_BLACKLIST_TAG: _(u'This software does not work with the '
127+ u'\u201c%s\u201D graphics driver this '
128+ u'computer is using.'),
129+}
130+
131+
132+def get_hw_short_description(tag):
133+ # FIXME: deal with OPENGL_DRIVER_BLACKLIST_TAG as this needs rsplit(":")
134+ # and a view of all available tags
135+ s = TAG_DESCRIPTION.get(tag)
136+ return utf8(s)
137+
138+
139+def get_hw_missing_long_description(tags):
140+ s = ""
141+ # build string
142+ for tag, supported in tags.iteritems():
143+ if supported == "no":
144+ descr = TAG_MISSING_DESCRIPTION.get(tag)
145+ if descr:
146+ s += "%s\n" % descr
147+ else:
148+ # deal with generic tags
149+ prefix, sep, postfix = tag.rpartition(":")
150+ descr = TAG_MISSING_DESCRIPTION.get(prefix + sep)
151+ descr = descr % postfix
152+ if descr:
153+ s += "%s\n" % descr
154+ # ensure that the last \n is gone
155+ if s:
156+ s = s[:-1]
157+ return utf8(s)
158+
159+
160+def get_private_extensions_hardware_support_for_tags(tags):
161+ import debtagshw
162+ res = {}
163+ for tag in tags:
164+ if tag.startswith(OPENGL_DRIVER_BLACKLIST_TAG):
165+ prefix, sep, driver = tag.rpartition(":")
166+ if driver == debtagshw.opengl.get_driver():
167+ res[tag] = debtagshw.enums.HardwareSupported.NO
168+ else:
169+ res[tag] = debtagshw.enums.HardwareSupported.YES
170+ return res
171+
172+
173+def get_hardware_support_for_tags(tags):
174+ """ wrapper around the DebtagsAvailalbeHW to support adding our own
175+ private tag extension (like opengl-driver)
176+ """
177+ from debtagshw.debtagshw import DebtagsAvailableHW
178+ hw = DebtagsAvailableHW()
179+ support = hw.get_hardware_support_for_tags(tags)
180+ private_extensions = get_private_extensions_hardware_support_for_tags(
181+ tags)
182+ support.update(private_extensions)
183+ return support
184+
185+
186+def utf8(s):
187+ """
188+ Takes a string or unicode object and returns a utf-8 encoded
189+ string, errors are ignored
190+ """
191+ if s is None:
192+ return None
193+ if isinstance(s, unicode):
194+ return s.encode("utf-8", "ignore")
195+ return unicode(s, "utf8", "ignore").encode("utf8")
196
197=== modified file 'src/webcatalog/management/commands/import_app_install_data.py'
198--- src/webcatalog/management/commands/import_app_install_data.py 2012-03-13 11:54:51 +0000
199+++ src/webcatalog/management/commands/import_app_install_data.py 2012-03-26 20:58:01 +0000
200@@ -34,7 +34,6 @@
201 from apt_inst import DebFile
202 from django.conf import settings
203 from django.core.files.images import ImageFile
204-from django.forms.models import construct_instance
205 from django.core.management.base import LabelCommand
206
207 from webcatalog.forms import ApplicationForm
208
209=== added file 'src/webcatalog/migrations/0019_add_debtags_to_application.py'
210--- src/webcatalog/migrations/0019_add_debtags_to_application.py 1970-01-01 00:00:00 +0000
211+++ src/webcatalog/migrations/0019_add_debtags_to_application.py 2012-03-26 20:58:01 +0000
212@@ -0,0 +1,162 @@
213+# encoding: utf-8
214+import datetime
215+from south.db import db
216+from south.v2 import SchemaMigration
217+from django.db import models
218+
219+class Migration(SchemaMigration):
220+
221+ def forwards(self, orm):
222+ # Adding field 'Application.debtags'
223+ db.add_column('webcatalog_application', 'debtags', self.gf('django.db.models.fields.CharField')(default='', max_length=255, blank=True), keep_default=False)
224+
225+
226+ def backwards(self, orm):
227+ # Deleting field 'Application.debtags'
228+ db.delete_column('webcatalog_application', 'debtags')
229+
230+
231+ models = {
232+ 'auth.group': {
233+ 'Meta': {'object_name': 'Group'},
234+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
235+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
236+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
237+ },
238+ 'auth.permission': {
239+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
240+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
241+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
242+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
243+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
244+ },
245+ 'auth.user': {
246+ 'Meta': {'object_name': 'User'},
247+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
248+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
249+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
250+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
251+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
252+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
253+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
254+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
255+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
256+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
257+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
258+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
259+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
260+ },
261+ 'contenttypes.contenttype': {
262+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
263+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
264+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
265+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
266+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
267+ },
268+ 'webcatalog.application': {
269+ 'Meta': {'unique_together': "(('distroseries', 'archive_id'),)", 'object_name': 'Application'},
270+ 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
271+ 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
272+ 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}),
273+ 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
274+ 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
275+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
276+ 'debtags': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
277+ 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}),
278+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
279+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}),
280+ 'for_purchase': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
281+ 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
282+ 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
283+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
284+ 'is_latest': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
285+ 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
286+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}),
287+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
288+ 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
289+ 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
290+ 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2', 'blank': 'True'}),
291+ 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}),
292+ 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
293+ 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
294+ 'section': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
295+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
296+ 'wilson_score': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
297+ },
298+ 'webcatalog.applicationmedia': {
299+ 'Meta': {'unique_together': "(('application', 'url'),)", 'object_name': 'ApplicationMedia'},
300+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Application']"}),
301+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
302+ 'media_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
303+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
304+ },
305+ 'webcatalog.consumer': {
306+ 'Meta': {'object_name': 'Consumer'},
307+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
308+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
309+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
310+ 'secret': ('django.db.models.fields.CharField', [], {'default': "'yzsqToDGmPExFPUjYEAKVLhdKqiBTJ'", 'max_length': '255', 'blank': 'True'}),
311+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
312+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"})
313+ },
314+ 'webcatalog.department': {
315+ 'Meta': {'object_name': 'Department'},
316+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
317+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
318+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'}),
319+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'})
320+ },
321+ 'webcatalog.distroseries': {
322+ 'Meta': {'object_name': 'DistroSeries'},
323+ 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
324+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
325+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'})
326+ },
327+ 'webcatalog.exhibit': {
328+ 'Meta': {'object_name': 'Exhibit'},
329+ 'banner_url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
330+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
331+ 'display': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
332+ 'distroseries': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.DistroSeries']", 'symmetrical': 'False'}),
333+ 'html': ('django.db.models.fields.TextField', [], {}),
334+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
335+ 'package_names': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
336+ 'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
337+ 'sca_id': ('django.db.models.fields.IntegerField', [], {})
338+ },
339+ 'webcatalog.machine': {
340+ 'Meta': {'unique_together': "(('owner', 'uuid'),)", 'object_name': 'Machine'},
341+ 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
342+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
343+ 'logo_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56', 'blank': 'True'}),
344+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
345+ 'package_list': ('django.db.models.fields.TextField', [], {}),
346+ 'packages_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56'}),
347+ 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
348+ },
349+ 'webcatalog.nonce': {
350+ 'Meta': {'object_name': 'Nonce'},
351+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
352+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
353+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
354+ 'nonce': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
355+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Token']"})
356+ },
357+ 'webcatalog.reviewstatsimport': {
358+ 'Meta': {'object_name': 'ReviewStatsImport'},
359+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']", 'unique': 'True'}),
360+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
361+ 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
362+ },
363+ 'webcatalog.token': {
364+ 'Meta': {'object_name': 'Token'},
365+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
366+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
367+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
368+ 'token': ('django.db.models.fields.CharField', [], {'default': "'WpQuqCbyDhCIPBHfRBAmMXnzfFswoqprEeBUpnPcqklaYvcxcg'", 'max_length': '50', 'primary_key': 'True'}),
369+ 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'JZElFyNGqqPBDSdRJudXfvcSLLCJExZriHUKnZFAGgrJNlLIqV'", 'max_length': '50'}),
370+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
371+ }
372+ }
373+
374+ complete_apps = ['webcatalog']
375
376=== modified file 'src/webcatalog/models/applications.py'
377--- src/webcatalog/models/applications.py 2012-03-20 14:09:05 +0000
378+++ src/webcatalog/models/applications.py 2012-03-26 20:58:01 +0000
379@@ -96,6 +96,7 @@
380 ratings_histogram = models.CharField(max_length=128, blank=True)
381 is_latest = models.BooleanField()
382 wilson_score = models.FloatField(null=True, blank=True, db_index=True)
383+ debtags = models.CharField(max_length=255, blank=True)
384
385 # Other desktop fields used by s-c
386 # x-gnome-fullname
387
388=== modified file 'src/webcatalog/static/css/webcatalog.css'
389--- src/webcatalog/static/css/webcatalog.css 2012-03-16 17:18:41 +0000
390+++ src/webcatalog/static/css/webcatalog.css 2012-03-26 20:58:01 +0000
391@@ -359,7 +359,7 @@
392 border-bottom: 1px dotted gray;
393 }
394
395-.emaillinkportlet .portletheader{
396+.emaillinkportlet .portletheader, .debtags .portletheader {
397 background: #bbb;
398 margin-bottom: 8px;
399 }
400@@ -393,3 +393,10 @@
401 line-height: 60px;
402 padding-right: 5px;
403 }
404+.debtags {
405+ padding-top: 12px;
406+}
407+.debtags li {
408+ list-style-type: circle;
409+ margin-left: 16px;
410+}
411
412=== modified file 'src/webcatalog/templates/webcatalog/application_detail.html'
413--- src/webcatalog/templates/webcatalog/application_detail.html 2012-03-20 10:04:41 +0000
414+++ src/webcatalog/templates/webcatalog/application_detail.html 2012-03-26 20:58:01 +0000
415@@ -58,6 +58,16 @@
416 </form>
417 </div>
418 </div>
419+ {% if debtags %}
420+ <div class='portlet debtags'>
421+ <div class='portletheader'>Hardware requirements:</div>
422+ <ul>
423+ {% for tag in debtags %}
424+ <li>{{ tag }}</li>
425+ {% endfor %}
426+ </ul>
427+ </div>
428+ {% endif %}
429 </div>
430 <div id="sc-mockup">
431 <div class="header">
432
433=== modified file 'src/webcatalog/tests/factory.py'
434--- src/webcatalog/tests/factory.py 2012-03-20 14:09:05 +0000
435+++ src/webcatalog/tests/factory.py 2012-03-26 20:58:01 +0000
436@@ -95,7 +95,8 @@
437 comment=None, description=None, icon_name='', icon=None,
438 distroseries=None, arch='i686', ratings_average=None,
439 ratings_total=None, ratings_histogram='', screenshot_url='',
440- archive_id=None, version='', is_latest=False, wilson_score=0.0):
441+ archive_id=None, version='', is_latest=False, wilson_score=0.0,
442+ debtags=[]):
443 if name is None:
444 name = self.get_unique_string(prefix='Readable Name')
445 if package_name is None:
446@@ -114,7 +115,7 @@
447 ratings_average=ratings_average, ratings_total=ratings_total,
448 ratings_histogram=ratings_histogram,
449 archive_id=archive_id, version=version, is_latest=is_latest,
450- wilson_score=wilson_score)
451+ wilson_score=wilson_score, debtags=debtags)
452
453 if screenshot_url:
454 ApplicationMedia.objects.create(
455
456=== modified file 'src/webcatalog/tests/test_forms.py'
457--- src/webcatalog/tests/test_forms.py 2012-03-19 12:20:51 +0000
458+++ src/webcatalog/tests/test_forms.py 2012-03-26 20:58:01 +0000
459@@ -27,6 +27,7 @@
460 from webcatalog.forms import (
461 ApplicationForm,
462 desktop_field_mappings,
463+ ForPurchaseApplicationForm,
464 )
465 from webcatalog.models import Application, ApplicationMedia
466 from webcatalog.tests.factory import TestCaseWithFactory
467@@ -34,6 +35,7 @@
468 __metaclass__ = type
469 __all__ = [
470 'ApplicationFormTestCase',
471+ 'ForPurchaseApplicationFormTestCase',
472 ]
473
474
475@@ -161,3 +163,14 @@
476 form = ApplicationForm(dict(screenshot_url='http://foo.com:42/broken',
477 section='required', name='required', package_name='required'))
478 self.assertTrue(form.is_valid())
479+
480+
481+class ForPurchaseApplicationFormTestCase(TestCaseWithFactory):
482+
483+ def test_from_json_shortens_debtag(self):
484+ app = self.factory.make_application()
485+ data = {'debtags': ['hardware::storage:cd-writer']}
486+
487+ form = ForPurchaseApplicationForm.from_json(data, app.distroseries)
488+
489+ self.assertEqual(form.data['debtags'], '["CD burner"]')
490
491=== modified file 'src/webcatalog/tests/test_views.py'
492--- src/webcatalog/tests/test_views.py 2012-03-26 17:01:37 +0000
493+++ src/webcatalog/tests/test_views.py 2012-03-26 20:58:01 +0000
494@@ -318,6 +318,21 @@
495
496 self.assertContains(response, 'id="screenshots-carousel"')
497
498+ def test_debtags_displayed(self):
499+ app = self.factory.make_application(debtags='["joystick"]')
500+
501+ response = self.client.get(self.get_app_details_url(app))
502+
503+ self.assertContains(response, 'Hardware requirements')
504+ self.assertContains(response, 'joystick')
505+
506+ def test_debtags_not_displayed(self):
507+ app = self.factory.make_application(debtags='')
508+
509+ response = self.client.get(self.get_app_details_url(app))
510+
511+ self.assertNotContains(response, 'Hardware requirements')
512+
513
514 class ApplicationDetailNoSeriesTestCase(TestCaseWithFactory):
515 def test_renders_latest(self):
516
517=== modified file 'src/webcatalog/views.py'
518--- src/webcatalog/views.py 2012-03-20 22:23:31 +0000
519+++ src/webcatalog/views.py 2012-03-26 20:58:01 +0000
520@@ -22,6 +22,7 @@
521 with_statement,
522 )
523
524+import json
525 import operator
526 import os
527 from random import shuffle
528@@ -176,6 +177,8 @@
529 else:
530 form = EmailDownloadLinkForm()
531
532+ debtags = None if not app.debtags else json.loads(app.debtags)
533+
534 atts = {'application': app,
535 'available_distroseries': app.available_distroseries(),
536 'breadcrumbs': app.crumbs(),
537@@ -183,6 +186,7 @@
538 'absolute_url': request.build_absolute_uri(
539 reverse('wc-package-detail', args=[distro, package_name])),
540 'email_form': form,
541+ 'debtags': debtags,
542 }
543
544 return render_to_response(

Subscribers

People subscribed via source and target branches