Merge lp:~elachuni/ubuntu-webcatalog/exhibit-widget into lp:ubuntu-webcatalog
- exhibit-widget
- Merge into trunk
Status: | Merged | ||||
---|---|---|---|---|---|
Approved by: | Anthony Lenton | ||||
Approved revision: | 64 | ||||
Merged at revision: | 65 | ||||
Proposed branch: | lp:~elachuni/ubuntu-webcatalog/exhibit-widget | ||||
Merge into: | lp:ubuntu-webcatalog | ||||
Diff against target: |
759 lines (+528/-5) 15 files modified
django_project/config/main.cfg (+1/-1) src/webcatalog/admin.py (+6/-0) src/webcatalog/management/commands/import_exhibits.py (+82/-0) src/webcatalog/migrations/0009_add_exhibit.py (+174/-0) src/webcatalog/models/__init__.py (+2/-0) src/webcatalog/models/applications.py (+23/-0) src/webcatalog/static/css/webcatalog.css (+11/-0) src/webcatalog/templates/webcatalog/exhibits_widget.html (+41/-0) src/webcatalog/templates/webcatalog/index.html (+6/-0) src/webcatalog/templatetags/webcatalog.py (+0/-1) src/webcatalog/tests/factory.py (+16/-0) src/webcatalog/tests/test_commands.py (+99/-1) src/webcatalog/tests/test_models.py (+18/-0) src/webcatalog/tests/test_views.py (+39/-1) src/webcatalog/views.py (+10/-1) |
||||
To merge this branch: | bzr merge lp:~elachuni/ubuntu-webcatalog/exhibit-widget | ||||
Related bugs: |
|
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Michael Nelson (community) | Approve | ||
Review via email: mp+95484@code.launchpad.net |
Commit message
Added a basic exhibit widget to the front page of the site
Description of the change
Overview
========
This branch adds an exhibit widget to the front page of the site, pulling in the data from software-
Details
=======
A local Exhibit model was added to store retrieved models. You can modify an Exhibit locally to override the data retrieved from sca, forcing an Exhibit to remain hidden or published, or slightly changing the data retrieved from sca.
The exhibits are displayed on the front page, cycling between each exhibit every 10 seconds. If Javascript is disabled a random exhibit will be (statically) displayed each time the page is loaded.
When the user clicks on an exhibit they're taken to the application details page for that application.
Still missing:
No javascript framework was used so the transitions are quite stark currently (suddenly jumping from showing one exhibit to the other), a bit of YUI love would be necessary to have smoother scrolled or blended transitions of some sort, and little dots or arrows to manually switch between exhibits.
Michael Nelson (michael.nelson) wrote : | # |
Anthony Lenton (elachuni) wrote : | # |
> Hi Anthony, the code and tests look excellent, I've got some small
> comments inline below, but I've got two bigger questions:
>
> 1) I don't understand generally why we'd want to store the imported
> exhibits as models... I don't see any advantage? You mention the
> ability to modify local exhibits via the admin, or unpublish them
> etc., but why *should* we need to do that? If there was a problem with
> them, wouldn't it be better to first update the actual source of the
> exhibit and then re-import them so that we'd fix whatever the problem
> was on all the clients also? It seems like we're trying to build in a
> security buffer so that we don't have to trust our own api
> infrastructure, even though the USC clients will have no choice? The
> disadvantages without giving it much thought are (1) more code to
> maintain, (2) more complexity - in that differences between clients
> and website are not transparent, (3) changes/additions to the exhibits
> api on sca will require schema changes for us too.
I don't really see what we'd do if we *don't* store them in the DB -- fetch them for each request? Cache them on the file system on each app server? Let the browser fetch them directly via Javascript? All these alternatives sound less attractive.
Beyond that, there could be several reasons to want locally modified exhibits:
- If we want web-only exhibits, exhibits that don't make sense in USC
- If SCA publishes some exhibits we don't want on the web, like exhibits for older distroseries or eventually specific regions or that for some reason shouldn't be displayed on the web site
- Similar to the one above, if only a subset of the exhibits published by SCA want to be displayed via web to give extra promotion to certain apps
- For if we need to edit the web version for some reason. If it's something that can be fixed in the exhibits on sca definitely we should do it there, but possibly it's something we should fix on the server(s, sca or uwc), in any case it'll be slow to fix (even more if the fix depends on a fix in the desktop client) and in some cases we might not want to touch the exhibit in sca at all. For instance, if an exhibit is linked to several packages, in uwc it will currently link to the first package in the list (because ubuntu-webcatalog doesn't provide a list view for *these* packages. We could add one eventually...), we might want to change that to a different one. Or we want to change the banner image for the exhibit a bit on the web because it works better there, but leave the desktop client version untouched.
> 2) If you have good reason for storing the Exhibit model, why do you
> need the published attribute? It seems to be set to True during the
> import (as it's not part of the imported data - we only export
> published exhibits I think?), and I don't see why you'd ever need to
> set it to False, given that you've got the display attribute. What
> does it allow you to do that you can't do with your display attribute?
> (on its own, the display attribute could still give you the three
> options of a) displayed, b) hidden but display after next import and
> c) hidden and keep hidden after next import?
Check ...
Michael Nelson (michael.nelson) wrote : | # |
Thanks for the answers achuni. I understand now why both published and display are needed. I'm still not sold on the idea of replicated the implementation of exhibits in uwc..., but as you said, if we need to modify certain exhibits specifically for display on uwc, then we need the model with the extra display field so we can maintain those differences between imports.
Preview Diff
1 | === modified file 'django_project/config/main.cfg' |
2 | --- django_project/config/main.cfg 2012-01-06 14:35:07 +0000 |
3 | +++ django_project/config/main.cfg 2012-03-01 22:48:21 +0000 |
4 | @@ -78,7 +78,7 @@ |
5 | openid_create_users = true |
6 | openid_update_details_from_sreg = true |
7 | openid_launchpad_teams_mapping = openid_team_mapping |
8 | - |
9 | +openid_launchpad_staff_teams = canonical-ca-hackers |
10 | |
11 | [openid_team_mapping] |
12 | # Structure here is: LP Team = Django Group |
13 | |
14 | === modified file 'src/webcatalog/admin.py' |
15 | --- src/webcatalog/admin.py 2012-01-06 17:54:47 +0000 |
16 | +++ src/webcatalog/admin.py 2012-03-01 22:48:21 +0000 |
17 | @@ -26,6 +26,7 @@ |
18 | Application, |
19 | Department, |
20 | DistroSeries, |
21 | + Exhibit, |
22 | Machine, |
23 | ) |
24 | |
25 | @@ -47,7 +48,12 @@ |
26 | search_fields = ('owner__username', 'hostname', 'uuid') |
27 | list_display = ('hostname', 'uuid', 'owner') |
28 | |
29 | + |
30 | +class ExhibitAdmin(admin.ModelAdmin): |
31 | + list_display = ('package_names', 'published', 'display') |
32 | + |
33 | admin.site.register(Application, ApplicationAdmin) |
34 | admin.site.register(Department) |
35 | admin.site.register(DistroSeries) |
36 | +admin.site.register(Exhibit, ExhibitAdmin) |
37 | admin.site.register(Machine, MachineAdmin) |
38 | |
39 | === added file 'src/webcatalog/management/commands/import_exhibits.py' |
40 | --- src/webcatalog/management/commands/import_exhibits.py 1970-01-01 00:00:00 +0000 |
41 | +++ src/webcatalog/management/commands/import_exhibits.py 2012-03-01 22:48:21 +0000 |
42 | @@ -0,0 +1,82 @@ |
43 | +# -*- coding: utf-8 -*- |
44 | +# This file is part of the Apps Directory |
45 | +# Copyright (C) 2011 Canonical Ltd. |
46 | +# |
47 | +# This program is free software: you can redistribute it and/or modify |
48 | +# it under the terms of the GNU Affero General Public License as |
49 | +# published by the Free Software Foundation, either version 3 of the |
50 | +# License, or (at your option) any later version. |
51 | +# |
52 | +# This program is distributed in the hope that it will be useful, |
53 | +# but WITHOUT ANY WARRANTY; without even the implied warranty of |
54 | +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
55 | +# GNU Affero General Public License for more details. |
56 | +# |
57 | +# You should have received a copy of the GNU Affero General Public License |
58 | +# along with this program. If not, see <http://www.gnu.org/licenses/>. |
59 | + |
60 | +"""Management command to import exhibits from Software Center Agent's API.""" |
61 | + |
62 | +from __future__ import ( |
63 | + absolute_import, |
64 | + ) |
65 | + |
66 | +import json |
67 | +import urllib |
68 | +from urlparse import urlparse |
69 | + |
70 | +from django.conf import settings |
71 | +from django.core.management.base import BaseCommand |
72 | + |
73 | +from webcatalog.models import ( |
74 | + DistroSeries, |
75 | + Exhibit, |
76 | + ) |
77 | + |
78 | +__metaclass__ = type |
79 | +__all__ = [ |
80 | + 'Command', |
81 | + ] |
82 | + |
83 | + |
84 | +class Command(BaseCommand): |
85 | + help = "Import exhibits from Software Center Agent's API." |
86 | + verbosity = 0 |
87 | + |
88 | + def handle(self, *args, **options): |
89 | + url = '%sexhibits/en/' % settings.SCA_API_URL |
90 | + # call api and get exhibits |
91 | + response = urllib.urlopen(url) |
92 | + if response.code != 200: |
93 | + raise Exception("Couldn't connect to server at %s" % url) |
94 | + exhibits = json.loads(response.read()) |
95 | + |
96 | + for exhibit in exhibits: |
97 | + args = {'published': True} |
98 | + for field in ['package_names', 'html', 'banner_url']: |
99 | + args[field] = exhibit.get(field, '') |
100 | + |
101 | + # Munge banner_url into html |
102 | + banner_url = args['banner_url'] |
103 | + if banner_url: |
104 | + parsed = urlparse(banner_url) |
105 | + args['html'] = args['html'].replace(parsed.path, banner_url) |
106 | + |
107 | + instance, created = Exhibit.objects.get_or_create( |
108 | + sca_id=exhibit['id'], defaults=args) |
109 | + |
110 | + if not created: |
111 | + Exhibit.objects.filter(pk=instance.pk).update(**args) |
112 | + |
113 | + distroseries = [DistroSeries.objects.get_or_create(**ds)[0] |
114 | + for ds in exhibit.get('distroseries', [])] |
115 | + |
116 | + to_remove = instance.distroseries.exclude( |
117 | + pk__in=[x.id for x in distroseries]) |
118 | + instance.distroseries.remove(*to_remove) |
119 | + instance.distroseries.add(*distroseries) |
120 | + |
121 | + # Unpublish other exhibits |
122 | + Exhibit.objects.filter(published=True).exclude( |
123 | + sca_id__in=[xibit['id'] for xibit in exhibits]).update( |
124 | + published=False) |
125 | |
126 | === added file 'src/webcatalog/migrations/0009_add_exhibit.py' |
127 | --- src/webcatalog/migrations/0009_add_exhibit.py 1970-01-01 00:00:00 +0000 |
128 | +++ src/webcatalog/migrations/0009_add_exhibit.py 2012-03-01 22:48:21 +0000 |
129 | @@ -0,0 +1,174 @@ |
130 | +# encoding: utf-8 |
131 | +import datetime |
132 | +from south.db import db |
133 | +from south.v2 import SchemaMigration |
134 | +from django.db import models |
135 | + |
136 | +class Migration(SchemaMigration): |
137 | + |
138 | + def forwards(self, orm): |
139 | + |
140 | + # Adding model 'Exhibit' |
141 | + db.create_table('webcatalog_exhibit', ( |
142 | + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), |
143 | + ('sca_id', self.gf('django.db.models.fields.IntegerField')()), |
144 | + ('package_names', self.gf('django.db.models.fields.CharField')(max_length=1024)), |
145 | + ('banner_url', self.gf('django.db.models.fields.CharField')(max_length=1024)), |
146 | + ('html', self.gf('django.db.models.fields.TextField')()), |
147 | + ('date_created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)), |
148 | + ('published', self.gf('django.db.models.fields.BooleanField')(default=False)), |
149 | + ('display', self.gf('django.db.models.fields.NullBooleanField')(null=True, blank=True)), |
150 | + )) |
151 | + db.send_create_signal('webcatalog', ['Exhibit']) |
152 | + |
153 | + # Adding M2M table for field distroseries on 'Exhibit' |
154 | + db.create_table('webcatalog_exhibit_distroseries', ( |
155 | + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), |
156 | + ('exhibit', models.ForeignKey(orm['webcatalog.exhibit'], null=False)), |
157 | + ('distroseries', models.ForeignKey(orm['webcatalog.distroseries'], null=False)) |
158 | + )) |
159 | + db.create_unique('webcatalog_exhibit_distroseries', ['exhibit_id', 'distroseries_id']) |
160 | + |
161 | + |
162 | + def backwards(self, orm): |
163 | + |
164 | + # Deleting model 'Exhibit' |
165 | + db.delete_table('webcatalog_exhibit') |
166 | + |
167 | + # Removing M2M table for field distroseries on 'Exhibit' |
168 | + db.delete_table('webcatalog_exhibit_distroseries') |
169 | + |
170 | + |
171 | + models = { |
172 | + 'auth.group': { |
173 | + 'Meta': {'object_name': 'Group'}, |
174 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
175 | + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), |
176 | + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) |
177 | + }, |
178 | + 'auth.permission': { |
179 | + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, |
180 | + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
181 | + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), |
182 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
183 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) |
184 | + }, |
185 | + 'auth.user': { |
186 | + 'Meta': {'object_name': 'User'}, |
187 | + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
188 | + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), |
189 | + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
190 | + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), |
191 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
192 | + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), |
193 | + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
194 | + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
195 | + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), |
196 | + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), |
197 | + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), |
198 | + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), |
199 | + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) |
200 | + }, |
201 | + 'contenttypes.contenttype': { |
202 | + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, |
203 | + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
204 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
205 | + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
206 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) |
207 | + }, |
208 | + 'webcatalog.application': { |
209 | + 'Meta': {'unique_together': "(('distroseries', 'archive_id'),)", 'object_name': 'Application'}, |
210 | + 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), |
211 | + 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
212 | + 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}), |
213 | + 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
214 | + 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
215 | + 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
216 | + 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}), |
217 | + 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), |
218 | + 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}), |
219 | + 'for_purchase': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
220 | + 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), |
221 | + 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
222 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
223 | + 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
224 | + 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}), |
225 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), |
226 | + 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), |
227 | + 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
228 | + 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2', 'blank': 'True'}), |
229 | + 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}), |
230 | + 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), |
231 | + 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), |
232 | + 'screenshot_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}), |
233 | + 'section': ('django.db.models.fields.CharField', [], {'max_length': '32'}) |
234 | + }, |
235 | + 'webcatalog.consumer': { |
236 | + 'Meta': {'object_name': 'Consumer'}, |
237 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
238 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
239 | + 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), |
240 | + 'secret': ('django.db.models.fields.CharField', [], {'default': "'tBQfPJnnuuGIiTleUFfYPcCapqovnK'", 'max_length': '255', 'blank': 'True'}), |
241 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), |
242 | + 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"}) |
243 | + }, |
244 | + 'webcatalog.department': { |
245 | + 'Meta': {'object_name': 'Department'}, |
246 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
247 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}), |
248 | + 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'}) |
249 | + }, |
250 | + 'webcatalog.distroseries': { |
251 | + 'Meta': {'object_name': 'DistroSeries'}, |
252 | + 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}), |
253 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
254 | + 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}) |
255 | + }, |
256 | + 'webcatalog.exhibit': { |
257 | + 'Meta': {'object_name': 'Exhibit'}, |
258 | + 'banner_url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), |
259 | + 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
260 | + 'display': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), |
261 | + 'distroseries': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.DistroSeries']", 'symmetrical': 'False'}), |
262 | + 'html': ('django.db.models.fields.TextField', [], {}), |
263 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
264 | + 'package_names': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), |
265 | + 'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), |
266 | + 'sca_id': ('django.db.models.fields.IntegerField', [], {}) |
267 | + }, |
268 | + 'webcatalog.machine': { |
269 | + 'Meta': {'unique_together': "(('owner', 'uuid'),)", 'object_name': 'Machine'}, |
270 | + 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '64'}), |
271 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
272 | + 'logo_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56', 'blank': 'True'}), |
273 | + 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), |
274 | + 'package_list': ('django.db.models.fields.TextField', [], {}), |
275 | + 'packages_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56'}), |
276 | + 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}) |
277 | + }, |
278 | + 'webcatalog.nonce': { |
279 | + 'Meta': {'object_name': 'Nonce'}, |
280 | + 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}), |
281 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
282 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
283 | + 'nonce': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}), |
284 | + 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Token']"}) |
285 | + }, |
286 | + 'webcatalog.reviewstatsimport': { |
287 | + 'Meta': {'object_name': 'ReviewStatsImport'}, |
288 | + 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']", 'unique': 'True'}), |
289 | + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), |
290 | + 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}) |
291 | + }, |
292 | + 'webcatalog.token': { |
293 | + 'Meta': {'object_name': 'Token'}, |
294 | + 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}), |
295 | + 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), |
296 | + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), |
297 | + 'token': ('django.db.models.fields.CharField', [], {'default': "'zyRClKkfkDWLAzwbnPWVQvhLPDsKNFnfvsoikGwYaFHtrcKghE'", 'max_length': '50', 'primary_key': 'True'}), |
298 | + 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'MPdwPvdsoPtibgEZbUQmMmtdkhuSlwjZLrmbvKcrccfTPBycXz'", 'max_length': '50'}), |
299 | + 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) |
300 | + } |
301 | + } |
302 | + |
303 | + complete_apps = ['webcatalog'] |
304 | |
305 | === modified file 'src/webcatalog/models/__init__.py' |
306 | --- src/webcatalog/models/__init__.py 2011-09-12 13:37:24 +0000 |
307 | +++ src/webcatalog/models/__init__.py 2012-03-01 22:48:21 +0000 |
308 | @@ -26,6 +26,7 @@ |
309 | 'DistroSeries', |
310 | 'Application', |
311 | 'Department', |
312 | + 'Exhibit', |
313 | 'ReviewStatsImport', |
314 | 'Machine', |
315 | ] |
316 | @@ -35,6 +36,7 @@ |
317 | Application, |
318 | Department, |
319 | DistroSeries, |
320 | + Exhibit, |
321 | Machine, |
322 | ReviewStatsImport, |
323 | ) |
324 | |
325 | === modified file 'src/webcatalog/models/applications.py' |
326 | --- src/webcatalog/models/applications.py 2012-01-06 17:54:47 +0000 |
327 | +++ src/webcatalog/models/applications.py 2012-03-01 22:48:21 +0000 |
328 | @@ -37,6 +37,7 @@ |
329 | 'Application', |
330 | 'Department', |
331 | 'DistroSeries', |
332 | + 'Exhibit', |
333 | 'Machine', |
334 | 'ReviewStatsImport', |
335 | ] |
336 | @@ -202,3 +203,25 @@ |
337 | class Meta: |
338 | app_label = 'webcatalog' |
339 | unique_together = ('owner', 'uuid') |
340 | + |
341 | + |
342 | +class Exhibit(models.Model): |
343 | + sca_id = models.IntegerField() |
344 | + package_names = models.CharField(max_length=1024) |
345 | + banner_url = models.CharField(max_length=1024) |
346 | + distroseries = models.ManyToManyField(DistroSeries) |
347 | + html = models.TextField() |
348 | + date_created = models.DateTimeField(auto_now_add=True) |
349 | + published = models.BooleanField() |
350 | + display = models.NullBooleanField( |
351 | + help_text="Yes: Always display. No: Never display. " |
352 | + "Unknown: Display if published") |
353 | + |
354 | + class Meta: |
355 | + app_label = 'webcatalog' |
356 | + |
357 | + def destination_url(self): |
358 | + package = self.package_names.split(',')[0] |
359 | + if not package: |
360 | + return '' |
361 | + return reverse('wc-package-detail', kwargs={'package_name': package}) |
362 | |
363 | === modified file 'src/webcatalog/static/css/webcatalog.css' |
364 | --- src/webcatalog/static/css/webcatalog.css 2011-09-19 21:20:57 +0000 |
365 | +++ src/webcatalog/static/css/webcatalog.css 2012-03-01 22:48:21 +0000 |
366 | @@ -316,3 +316,14 @@ |
367 | .paginator p, .paginator a { |
368 | font-size: 14px; |
369 | } |
370 | + |
371 | +.exhibit-container { |
372 | + width: 912px; |
373 | + display: none; |
374 | + overflow: hidden; |
375 | + height: 200px; |
376 | +} |
377 | + |
378 | +.exhibits-widget .enabled { |
379 | + display: block; |
380 | +} |
381 | |
382 | === added file 'src/webcatalog/templates/webcatalog/exhibits_widget.html' |
383 | --- src/webcatalog/templates/webcatalog/exhibits_widget.html 1970-01-01 00:00:00 +0000 |
384 | +++ src/webcatalog/templates/webcatalog/exhibits_widget.html 2012-03-01 22:48:21 +0000 |
385 | @@ -0,0 +1,41 @@ |
386 | +<script type="text/javascript"> |
387 | + var activeExhibit = {{ active }}; |
388 | + var images = new Array(); |
389 | + |
390 | + function switchExhibit() { |
391 | + var tags = document.getElementsByClassName('exhibit-container'); |
392 | + var nExhibits = tags.length; |
393 | + activeExhibit = (activeExhibit + 1) % nExhibits; |
394 | + for (var i in tags) { |
395 | + if (i == activeExhibit) { |
396 | + tags[i].classList.add('enabled'); |
397 | + } |
398 | + else { |
399 | + tags[i].classList.remove('enabled'); |
400 | + } |
401 | + } |
402 | + setTimeout(switchExhibit, 10000); |
403 | + }; |
404 | + |
405 | + function preLoadBanners() { |
406 | + for (var i = 0; i < preLoadBanners.arguments.length; i++) { |
407 | + images[i] = new Image() |
408 | + images[i].src = preLoadBanners.arguments[i] |
409 | + } |
410 | + } |
411 | + |
412 | + window.onload = function() { |
413 | + preLoadBanners({% for exhibit in exhibits %}"{{ exhibit.banner_url }}"{% if not forloop.last %}, {% endif %}{% endfor %}); |
414 | + setTimeout(switchExhibit, 10000); |
415 | + } |
416 | +</script> |
417 | + |
418 | +{% autoescape off %} |
419 | +{% for exhibit in exhibits %} |
420 | +<div class="exhibit-container{% ifequal forloop.counter0 active %} enabled{% endifequal %}"> |
421 | + <a href="{{ exhibit.destination_url }}"> |
422 | +{{ exhibit.html }} |
423 | + </a> |
424 | +</div> |
425 | +{% endfor %} |
426 | +{% endautoescape %} |
427 | \ No newline at end of file |
428 | |
429 | === modified file 'src/webcatalog/templates/webcatalog/index.html' |
430 | --- src/webcatalog/templates/webcatalog/index.html 2011-09-12 18:37:16 +0000 |
431 | +++ src/webcatalog/templates/webcatalog/index.html 2012-03-01 22:48:21 +0000 |
432 | @@ -7,6 +7,12 @@ |
433 | |
434 | {% block content %} |
435 | |
436 | +{% if exhibits %} |
437 | +<div class="exhibits-widget"> |
438 | + {% include "webcatalog/exhibits_widget.html" %} |
439 | +</div> |
440 | +{% endif %} |
441 | + |
442 | <h3>{% trans "Browse application departments" %}:</h3> |
443 | |
444 | {% for dept in depts %} |
445 | |
446 | === modified file 'src/webcatalog/templatetags/webcatalog.py' |
447 | --- src/webcatalog/templatetags/webcatalog.py 2012-01-06 17:54:47 +0000 |
448 | +++ src/webcatalog/templatetags/webcatalog.py 2012-03-01 22:48:21 +0000 |
449 | @@ -27,7 +27,6 @@ |
450 | 'install_options', |
451 | ] |
452 | |
453 | -import math |
454 | import string |
455 | |
456 | from django import template |
457 | |
458 | === modified file 'src/webcatalog/tests/factory.py' |
459 | --- src/webcatalog/tests/factory.py 2011-09-12 13:37:24 +0000 |
460 | +++ src/webcatalog/tests/factory.py 2012-03-01 22:48:21 +0000 |
461 | @@ -33,6 +33,7 @@ |
462 | Consumer, |
463 | Department, |
464 | DistroSeries, |
465 | + Exhibit, |
466 | Machine, |
467 | Token, |
468 | ) |
469 | @@ -114,6 +115,21 @@ |
470 | def make_department(self, name, parent=None): |
471 | return Department.objects.create(name=name, parent=parent) |
472 | |
473 | + def make_exhibit(self, package_names=None, published=True, display=None, |
474 | + distroseries=None): |
475 | + sca_id = self.get_unique_integer() |
476 | + if package_names is None: |
477 | + package_names = self.get_unique_string(prefix='package-') |
478 | + banner_url = self.get_unique_string(prefix='http://example/url') |
479 | + html = self.get_unique_string(prefix='html-') |
480 | + if distroseries is None: |
481 | + distroseries = self.make_distroseries() |
482 | + xibit = Exhibit.objects.create(sca_id=sca_id, |
483 | + package_names=package_names, banner_url=banner_url, html=html, |
484 | + published=published, display=display) |
485 | + xibit.distroseries.add(distroseries) |
486 | + return xibit |
487 | + |
488 | def make_distroseries(self, code_name=None, version=None): |
489 | if code_name is None: |
490 | code_name = self.get_unique_string(prefix='series-') |
491 | |
492 | === modified file 'src/webcatalog/tests/test_commands.py' |
493 | --- src/webcatalog/tests/test_commands.py 2012-01-06 17:54:47 +0000 |
494 | +++ src/webcatalog/tests/test_commands.py 2012-03-01 22:48:21 +0000 |
495 | @@ -22,6 +22,7 @@ |
496 | with_statement, |
497 | ) |
498 | import apt |
499 | +import json |
500 | import os |
501 | import shutil |
502 | import tempfile |
503 | @@ -43,6 +44,7 @@ |
504 | from webcatalog.models import ( |
505 | Application, |
506 | DistroSeries, |
507 | + Exhibit, |
508 | ReviewStatsImport, |
509 | ) |
510 | from webcatalog.management.commands.import_app_install_data import ( |
511 | @@ -57,6 +59,7 @@ |
512 | __metaclass__ = type |
513 | __all__ = [ |
514 | 'ImportAppInstallTestCase', |
515 | + 'ImportExhibitsTestCase', |
516 | 'ImportForPurchaseAppsTestCase', |
517 | 'ImportRatingsTestCase', |
518 | ] |
519 | @@ -384,7 +387,6 @@ |
520 | self.response_content = content.read() |
521 | mock_response = Mock() |
522 | mock_response.code = 200 |
523 | - mock_response.read = Mock() |
524 | mock_response.read.return_value = self.response_content |
525 | self.mock_response = mock_response |
526 | |
527 | @@ -541,3 +543,99 @@ |
528 | |
529 | # update_apps_with_stats returns None on success: |
530 | self.assertIsNone(command.update_apps_with_stats(natty, stats)) |
531 | + |
532 | + |
533 | +class ImportExhibitsTestCase(TestCaseWithFactory): |
534 | + def mock_response(self, exhibits, changes=None): |
535 | + response = Mock() |
536 | + response.code = 200 |
537 | + data = [] |
538 | + for exhibit in exhibits: |
539 | + ds = exhibit.distroseries.get() |
540 | + data.append({'package_names': exhibit.package_names, |
541 | + 'banner_url': exhibit.banner_url, |
542 | + 'version': ds.version, |
543 | + 'code_name': ds.code_name, |
544 | + 'html': exhibit.html, |
545 | + 'date_created': str(exhibit.date_created), |
546 | + 'id': exhibit.sca_id}) |
547 | + if changes: |
548 | + for atts, change in zip(data, changes): |
549 | + atts.update(change) |
550 | + for xibit in data: |
551 | + xibit['distroseries'] = [{'code_name': xibit.pop('code_name'), |
552 | + 'version': xibit.pop('version')}] |
553 | + return_value = json.dumps(data) |
554 | + response.read.return_value = return_value |
555 | + return response |
556 | + |
557 | + @patch('urllib.urlopen') |
558 | + def test_disables_current_exhibits(self, mock_urlopen): |
559 | + self.factory.make_exhibit(published=True) |
560 | + mock_urlopen.return_value = self.mock_response([]) |
561 | + |
562 | + call_command('import_exhibits') |
563 | + |
564 | + retrieved = Exhibit.objects.get() |
565 | + self.assertFalse(retrieved.published) |
566 | + |
567 | + @patch('urllib.urlopen') |
568 | + def test_updates_and_enables_current_exhibits(self, mock_urlopen): |
569 | + xibit = self.factory.make_exhibit(published=False) |
570 | + new_package_names = self.factory.get_unique_string(prefix='new-') |
571 | + new_banner_url = self.factory.get_unique_string(prefix='new-') |
572 | + new_html = self.factory.get_unique_string(prefix='new-') |
573 | + mock_urlopen.return_value = self.mock_response([xibit], changes=[ |
574 | + {'package_names': new_package_names, 'banner_url': new_banner_url, |
575 | + 'html': new_html}]) |
576 | + |
577 | + call_command('import_exhibits') |
578 | + |
579 | + retrieved = Exhibit.objects.get() |
580 | + self.assertTrue(retrieved.published) |
581 | + self.assertEqual(new_package_names, retrieved.package_names) |
582 | + self.assertEqual(new_banner_url, retrieved.banner_url) |
583 | + self.assertEqual(new_html, retrieved.html) |
584 | + |
585 | + @patch('urllib.urlopen') |
586 | + def test_creates_exhibit_if_scaid_does_not_match(self, mock_urlopen): |
587 | + xibit = self.factory.make_exhibit(published=False) |
588 | + new_id = xibit.sca_id + 20 |
589 | + mock_urlopen.return_value = self.mock_response([xibit], changes=[ |
590 | + {'id': new_id}]) |
591 | + |
592 | + call_command('import_exhibits') |
593 | + |
594 | + self.assertEqual(2, Exhibit.objects.all().count()) |
595 | + self.assertFalse(Exhibit.objects.get(sca_id=xibit.sca_id).published) |
596 | + self.assertTrue(Exhibit.objects.get(sca_id=new_id).published) |
597 | + |
598 | + @patch('urllib.urlopen') |
599 | + def test_display_is_left_untouched(self, mock_urlopen): |
600 | + xibits = [self.factory.make_exhibit(display=d) |
601 | + for d in [False, True, None]] |
602 | + |
603 | + mock_urlopen.return_value = self.mock_response(xibits) |
604 | + |
605 | + call_command('import_exhibits') |
606 | + |
607 | + retrieved = Exhibit.objects.all() |
608 | + self.assertEqual(3, len(retrieved)) |
609 | + self.assertIs(False, retrieved.get(pk=xibits[0].pk).display) |
610 | + self.assertIs(True, retrieved.get(pk=xibits[1].pk).display) |
611 | + self.assertIs(None, retrieved.get(pk=xibits[2].pk).display) |
612 | + |
613 | + @patch('urllib.urlopen') |
614 | + def test_banner_url_is_correctly_munged(self, mock_urlopen): |
615 | + xibit = self.factory.make_exhibit() |
616 | + html = '<img src="/foo/bar/baz.png">' |
617 | + banner_url = 'http://example.com/foo/bar/baz.png' |
618 | + expected = '<img src="http://example.com/foo/bar/baz.png">' |
619 | + |
620 | + mock_urlopen.return_value = self.mock_response([xibit], |
621 | + changes=[{'banner_url': banner_url, 'html': html}]) |
622 | + |
623 | + call_command('import_exhibits') |
624 | + |
625 | + retrieved = Exhibit.objects.get() |
626 | + self.assertEqual(expected, retrieved.html) |
627 | |
628 | === modified file 'src/webcatalog/tests/test_models.py' |
629 | --- src/webcatalog/tests/test_models.py 2012-01-06 17:54:47 +0000 |
630 | +++ src/webcatalog/tests/test_models.py 2012-03-01 22:48:21 +0000 |
631 | @@ -30,6 +30,8 @@ |
632 | __metaclass__ = type |
633 | __all__ = [ |
634 | 'ApplicationTestCase', |
635 | + 'DepartmentTestCase', |
636 | + 'ExhibitTestCase', |
637 | ] |
638 | |
639 | |
640 | @@ -140,3 +142,19 @@ |
641 | args=['frobbly', dept.id])}] |
642 | |
643 | self.assertEquals(expected, dept.crumbs(distro='frobbly')) |
644 | + |
645 | + |
646 | +class ExhibitTestCase(TestCaseWithFactory): |
647 | + def test_destination_url(self): |
648 | + exhibit = self.factory.make_exhibit(package_names='foobar') |
649 | + expected = reverse('wc-package-detail', args=['foobar']) |
650 | + self.assertEqual(expected, exhibit.destination_url()) |
651 | + |
652 | + def test_destination_url_multiple_packages(self): |
653 | + exhibit = self.factory.make_exhibit(package_names='foobar,baz') |
654 | + expected = reverse('wc-package-detail', args=['foobar']) |
655 | + self.assertEqual(expected, exhibit.destination_url()) |
656 | + |
657 | + def test_destination_url_blank_packages(self): |
658 | + exhibit = self.factory.make_exhibit(package_names='') |
659 | + self.assertEqual('', exhibit.destination_url()) |
660 | |
661 | === modified file 'src/webcatalog/tests/test_views.py' |
662 | --- src/webcatalog/tests/test_views.py 2011-09-19 21:20:57 +0000 |
663 | +++ src/webcatalog/tests/test_views.py 2012-03-01 22:48:21 +0000 |
664 | @@ -39,6 +39,7 @@ |
665 | 'ApplicationDetailNoSeriesTestCase', |
666 | 'ApplicationDetailTestCase', |
667 | 'ApplicationReviewsTestCase', |
668 | + 'IndexTestCase', |
669 | 'OverviewTestCase', |
670 | 'SearchTestCase', |
671 | ] |
672 | @@ -384,7 +385,7 @@ |
673 | self.assertEqual(404, response.status_code) |
674 | |
675 | |
676 | -class OverviewTestCase(TestCaseWithFactory): |
677 | +class IndexTestCase(TestCaseWithFactory): |
678 | def test_index_contains_links_to_departments(self): |
679 | dept = self.factory.make_department('foo') |
680 | |
681 | @@ -393,6 +394,43 @@ |
682 | self.assertContains(response, reverse('wc-department', |
683 | args=[dept.id])) |
684 | |
685 | + def test_exhibits_widget_doesnt_display_if_no_exhibits_published(self): |
686 | + self.factory.make_exhibit(published=False) |
687 | + self.factory.make_exhibit(published=True, display=False) |
688 | + |
689 | + response = self.client.get(reverse('wc-index')) |
690 | + |
691 | + self.assertNotContains(response, '<div class="exhibits-widget">') |
692 | + |
693 | + def test_exhibits_display_trumps_published(self): |
694 | + """If an exhibit has display=True it's shown even if published=False""" |
695 | + self.factory.make_exhibit(published=False, display=True) |
696 | + |
697 | + response = self.client.get(reverse('wc-index')) |
698 | + |
699 | + self.assertContains(response, '<div class="exhibits-widget">') |
700 | + |
701 | + def test_exhibits_falls_back_to_published(self): |
702 | + """If an exhibit has display=None, do what published says""" |
703 | + self.factory.make_exhibit(published=False, display=None) |
704 | + self.factory.make_exhibit(published=True, display=None) |
705 | + |
706 | + response = self.client.get(reverse('wc-index')) |
707 | + |
708 | + self.assertContains(response, '<div class="exhibit-container', |
709 | + count=1) |
710 | + |
711 | + def test_only_one_exhibit_enabled(self): |
712 | + for i in range(3): |
713 | + self.factory.make_exhibit(published=True) |
714 | + |
715 | + response = self.client.get(reverse('wc-index')) |
716 | + |
717 | + self.assertContains(response, |
718 | + '<div class="exhibit-container enabled">', count=1) |
719 | + |
720 | + |
721 | +class OverviewTestCase(TestCaseWithFactory): |
722 | def test_department_contains_links_to_subdepartments(self): |
723 | dept = self.factory.make_department('foo') |
724 | subdept = self.factory.make_department('bar', parent=dept) |
725 | |
726 | === modified file 'src/webcatalog/views.py' |
727 | --- src/webcatalog/views.py 2012-01-06 17:54:47 +0000 |
728 | +++ src/webcatalog/views.py 2012-03-01 22:48:21 +0000 |
729 | @@ -23,6 +23,7 @@ |
730 | ) |
731 | |
732 | import operator |
733 | +from random import randint |
734 | from urllib import urlencode |
735 | |
736 | from django.conf import settings |
737 | @@ -40,6 +41,7 @@ |
738 | Application, |
739 | Department, |
740 | DistroSeries, |
741 | + Exhibit, |
742 | ) |
743 | from webcatalog.utilities import ( |
744 | UserAgentString, |
745 | @@ -110,7 +112,14 @@ |
746 | def index(request): |
747 | depts = Department.objects.filter(parent=None).order_by('name') |
748 | depts = depts.order_by('name') |
749 | - context = RequestContext(request, dict={'depts': depts}) |
750 | + exhibits = Exhibit.objects.filter(Q(display=True) | |
751 | + Q(display=None, published=True,)) |
752 | + |
753 | + context = RequestContext(request, dict={ |
754 | + 'depts': depts, |
755 | + 'exhibits': exhibits, |
756 | + 'active': randint(0, max(exhibits.count() - 1, 1)), |
757 | + }) |
758 | return render_to_response('webcatalog/index.html', |
759 | context_instance=context) |
760 |
On Thu, Mar 1, 2012 at 11:49 PM, Anthony Lenton center- agent's exhibit web API and storing them locally.
<email address hidden> wrote:
> Anthony Lenton has proposed merging lp:~elachuni/ubuntu-webcatalog/exhibit-widget into lp:ubuntu-webcatalog.
> Overview
> ========
> This branch adds an exhibit widget to the front page of the site, pulling in the data from software-
>
Hi Anthony, the code and tests look excellent, I've got some small
comments inline below, but I've got two bigger questions:
1) I don't understand generally why we'd want to store the imported
exhibits as models... I don't see any advantage? You mention the
ability to modify local exhibits via the admin, or unpublish them
etc., but why *should* we need to do that? If there was a problem with
them, wouldn't it be better to first update the actual source of the
exhibit and then re-import them so that we'd fix whatever the problem
was on all the clients also? It seems like we're trying to build in a
security buffer so that we don't have to trust our own api
infrastructure, even though the USC clients will have no choice? The
disadvantages without giving it much thought are (1) more code to
maintain, (2) more complexity - in that differences between clients
and website are not transparent, (3) changes/additions to the exhibits
api on sca will require schema changes for us too.
2) If you have good reason for storing the Exhibit model, why do you
need the published attribute? It seems to be set to True during the
import (as it's not part of the imported data - we only export
published exhibits I think?), and I don't see why you'd ever need to
set it to False, given that you've got the display attribute. What
does it allow you to do that you can't do with your display attribute?
(on its own, the display attribute could still give you the three
options of a) displayed, b) hidden but display after next import and
c) hidden and keep hidden after next import?
Let me know if I've missed something... and feel free to land if
you're happy and have reasons for the above 2 points (just ping me and
I'll make it an approve vote).
> Details /code.launchpad .net/~elachuni/ ubuntu- webcatalog/ exhibit- widget/ +merge/ 95484 project/ co...
> =======
> A local Exhibit model was added to store retrieved models. You can modify an Exhibit locally to override the data retrieved from sca, forcing an Exhibit to remain hidden or published, or slightly changing the data retrieved from sca.
>
> The exhibits are displayed on the front page, cycling between each exhibit every 10 seconds. If Javascript is disabled a random exhibit will be (statically) displayed each time the page is loaded.
>
> When the user clicks on an exhibit they're taken to the application details page for that application.
>
> Still missing:
>
> No javascript framework was used so the transitions are quite stark currently (suddenly jumping from showing one exhibit to the other), a bit of YUI love would be necessary to have smoother scrolled or blended transitions of some sort, and little dots or arrows to manually switch between exhibits.
>
> --
> https:/
> You are subscribed to branch lp:ubuntu-webcatalog.
>
> === modified file 'django_