Merge lp:~zematynnad/ubuntu-webcatalog/display_hardware_953286 into lp:ubuntu-webcatalog
- display_hardware_953286
- Merge into trunk
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 |
Related bugs: |
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://
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_
To Test
=======
$fab bootstrap test
To post a comment you must log in.
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( |
LGTM