Merge lp:~elachuni/ubuntu-webcatalog/exhibit-widget into lp:ubuntu-webcatalog

Proposed by Anthony Lenton
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
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-center-agent's exhibit web API and storing them locally.

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.

To post a comment you must log in.
Revision history for this message
Michael Nelson (michael.nelson) wrote :
Download full text (20.8 KiB)

On Thu, Mar 1, 2012 at 11:49 PM, Anthony Lenton
<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-center-agent's exhibit web API and storing them locally.
>

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
> =======
> 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://code.launchpad.net/~elachuni/ubuntu-webcatalog/exhibit-widget/+merge/95484
> You are subscribed to branch lp:ubuntu-webcatalog.
>
> === modified file 'django_project/co...

Revision history for this message
Anthony Lenton (elachuni) wrote :
Download full text (3.7 KiB)

> 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 ...

Read more...

Revision history for this message
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.

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'django_project/config/main.cfg'
2--- django_project/config/main.cfg 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

Subscribers

People subscribed via source and target branches