Merge lp:~michael.nelson/ubuntu-webcatalog/1045328-bundle-promotion-apps into lp:ubuntu-webcatalog
- 1045328-bundle-promotion-apps
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Łukasz Czyżykowski |
Approved revision: | 174 |
Merged at revision: | 170 |
Proposed branch: | lp:~michael.nelson/ubuntu-webcatalog/1045328-bundle-promotion-apps |
Merge into: | lp:ubuntu-webcatalog |
Diff against target: |
456 lines (+278/-19) 10 files modified
src/webcatalog/admin.py (+15/-0) src/webcatalog/migrations/0029_add_application_widget.py (+191/-0) src/webcatalog/models/__init__.py (+2/-0) src/webcatalog/models/applications.py (+14/-0) src/webcatalog/schema.py (+0/-2) src/webcatalog/templates/webcatalog/application_detail.html (+3/-6) src/webcatalog/templatetags/webcatalog.py (+7/-0) src/webcatalog/tests/factory.py (+11/-0) src/webcatalog/tests/test_views.py (+35/-8) src/webcatalog/views.py (+0/-3) |
To merge this branch: | bzr merge lp:~michael.nelson/ubuntu-webcatalog/1045328-bundle-promotion-apps |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Łukasz Czyżykowski (community) | Approve | ||
Review via email: mp+123082@code.launchpad.net |
Commit message
Add ApplicationWidget model for dynamic campaign widgets.
Description of the change
Overview
========
This branch adds the ApplicationWidget model which can be used to display custom widgets on any application detail page (see bug 1045328).
`fab test`
Notes:
* I tried a few options for the model-admin, like tabularinline, but raw_id_field was the only way to *not* present the whole list of apps. The good thing is it presents lots of info about each app to decide, the bad thing is it results in a list of ids, which are opaque once set. We may want to investigate further if it is difficult to use.
* I considered having a widget per application (implemented) and a widget per package_name (what the existing setting did). I went for the DB relationship on application, which is more general (ie. could potentially select only certain distroseries versions of an app to include the widget), but may be more fiddly to use (need to select multiple versions of each app).
- 174. By Michael Nelson
-
REFACTOR: Use inline for displaying widget on application.
Łukasz Czyżykowski (lukasz-czyzykowski) : | # |
Preview Diff
1 | === modified file 'src/webcatalog/admin.py' | |||
2 | --- src/webcatalog/admin.py 2012-07-02 21:25:30 +0000 | |||
3 | +++ src/webcatalog/admin.py 2012-09-06 14:09:20 +0000 | |||
4 | @@ -23,6 +23,7 @@ | |||
5 | 23 | from webcatalog.models import ( | 23 | from webcatalog.models import ( |
6 | 24 | Application, | 24 | Application, |
7 | 25 | ApplicationMedia, | 25 | ApplicationMedia, |
8 | 26 | ApplicationWidget, | ||
9 | 26 | Department, | 27 | Department, |
10 | 27 | DistroSeries, | 28 | DistroSeries, |
11 | 28 | Exhibit, | 29 | Exhibit, |
12 | @@ -36,12 +37,19 @@ | |||
13 | 36 | ] | 37 | ] |
14 | 37 | 38 | ||
15 | 38 | 39 | ||
16 | 40 | class ApplicationWidgetInline(admin.TabularInline): | ||
17 | 41 | model = ApplicationWidget.applications.through | ||
18 | 42 | |||
19 | 43 | |||
20 | 39 | class ApplicationAdmin(admin.ModelAdmin): | 44 | class ApplicationAdmin(admin.ModelAdmin): |
21 | 40 | list_display = ('package_name', 'name', 'comment', 'distroseries', | 45 | list_display = ('package_name', 'name', 'comment', 'distroseries', |
22 | 41 | 'wilson_score') | 46 | 'wilson_score') |
23 | 42 | search_fields = ('package_name', 'name', 'comment') | 47 | search_fields = ('package_name', 'name', 'comment') |
24 | 43 | list_filter = ('distroseries', 'imported_from_sca', 'is_latest', | 48 | list_filter = ('distroseries', 'imported_from_sca', 'is_latest', |
25 | 44 | 'departments') | 49 | 'departments') |
26 | 50 | inlines = [ | ||
27 | 51 | ApplicationWidgetInline, | ||
28 | 52 | ] | ||
29 | 45 | 53 | ||
30 | 46 | 54 | ||
31 | 47 | class MachineAdmin(admin.ModelAdmin): | 55 | class MachineAdmin(admin.ModelAdmin): |
32 | @@ -66,8 +74,15 @@ | |||
33 | 66 | def distroseries(self, obj): | 74 | def distroseries(self, obj): |
34 | 67 | return obj.application.distroseries | 75 | return obj.application.distroseries |
35 | 68 | 76 | ||
36 | 77 | |||
37 | 78 | class ApplicationWidgetAdmin(admin.ModelAdmin): | ||
38 | 79 | list_display = ('name', ) | ||
39 | 80 | raw_id_fields = ('applications',) | ||
40 | 81 | |||
41 | 82 | |||
42 | 69 | admin.site.register(Application, ApplicationAdmin) | 83 | admin.site.register(Application, ApplicationAdmin) |
43 | 70 | admin.site.register(ApplicationMedia, ApplicationMediaAdmin) | 84 | admin.site.register(ApplicationMedia, ApplicationMediaAdmin) |
44 | 85 | admin.site.register(ApplicationWidget, ApplicationWidgetAdmin) | ||
45 | 71 | admin.site.register(Department, DepartmentAdmin) | 86 | admin.site.register(Department, DepartmentAdmin) |
46 | 72 | admin.site.register(DistroSeries) | 87 | admin.site.register(DistroSeries) |
47 | 73 | admin.site.register(Exhibit, ExhibitAdmin) | 88 | admin.site.register(Exhibit, ExhibitAdmin) |
48 | 74 | 89 | ||
49 | === added file 'src/webcatalog/migrations/0029_add_application_widget.py' | |||
50 | --- src/webcatalog/migrations/0029_add_application_widget.py 1970-01-01 00:00:00 +0000 | |||
51 | +++ src/webcatalog/migrations/0029_add_application_widget.py 2012-09-06 14:09:20 +0000 | |||
52 | @@ -0,0 +1,191 @@ | |||
53 | 1 | # encoding: utf-8 | ||
54 | 2 | import datetime | ||
55 | 3 | from south.db import db | ||
56 | 4 | from south.v2 import SchemaMigration | ||
57 | 5 | from django.db import models | ||
58 | 6 | |||
59 | 7 | class Migration(SchemaMigration): | ||
60 | 8 | |||
61 | 9 | def forwards(self, orm): | ||
62 | 10 | |||
63 | 11 | # Adding model 'ApplicationWidget' | ||
64 | 12 | db.create_table('webcatalog_applicationwidget', ( | ||
65 | 13 | ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), | ||
66 | 14 | ('name', self.gf('django.db.models.fields.CharField')(max_length=32)), | ||
67 | 15 | ('template_snippet', self.gf('django.db.models.fields.TextField')()), | ||
68 | 16 | )) | ||
69 | 17 | db.send_create_signal('webcatalog', ['ApplicationWidget']) | ||
70 | 18 | |||
71 | 19 | # Adding M2M table for field applications on 'ApplicationWidget' | ||
72 | 20 | db.create_table('webcatalog_applicationwidget_applications', ( | ||
73 | 21 | ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), | ||
74 | 22 | ('applicationwidget', models.ForeignKey(orm['webcatalog.applicationwidget'], null=False)), | ||
75 | 23 | ('application', models.ForeignKey(orm['webcatalog.application'], null=False)) | ||
76 | 24 | )) | ||
77 | 25 | db.create_unique('webcatalog_applicationwidget_applications', ['applicationwidget_id', 'application_id']) | ||
78 | 26 | |||
79 | 27 | |||
80 | 28 | def backwards(self, orm): | ||
81 | 29 | |||
82 | 30 | # Deleting model 'ApplicationWidget' | ||
83 | 31 | db.delete_table('webcatalog_applicationwidget') | ||
84 | 32 | |||
85 | 33 | # Removing M2M table for field applications on 'ApplicationWidget' | ||
86 | 34 | db.delete_table('webcatalog_applicationwidget_applications') | ||
87 | 35 | |||
88 | 36 | |||
89 | 37 | models = { | ||
90 | 38 | 'auth.group': { | ||
91 | 39 | 'Meta': {'object_name': 'Group'}, | ||
92 | 40 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
93 | 41 | 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), | ||
94 | 42 | 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) | ||
95 | 43 | }, | ||
96 | 44 | 'auth.permission': { | ||
97 | 45 | 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, | ||
98 | 46 | 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
99 | 47 | 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), | ||
100 | 48 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
101 | 49 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) | ||
102 | 50 | }, | ||
103 | 51 | 'auth.user': { | ||
104 | 52 | 'Meta': {'object_name': 'User'}, | ||
105 | 53 | 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), | ||
106 | 54 | 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), | ||
107 | 55 | 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), | ||
108 | 56 | 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), | ||
109 | 57 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
110 | 58 | 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), | ||
111 | 59 | 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), | ||
112 | 60 | 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), | ||
113 | 61 | 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), | ||
114 | 62 | 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), | ||
115 | 63 | 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), | ||
116 | 64 | 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), | ||
117 | 65 | 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) | ||
118 | 66 | }, | ||
119 | 67 | 'contenttypes.contenttype': { | ||
120 | 68 | 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, | ||
121 | 69 | 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
122 | 70 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
123 | 71 | 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
124 | 72 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) | ||
125 | 73 | }, | ||
126 | 74 | 'webcatalog.application': { | ||
127 | 75 | 'Meta': {'ordering': "('-wilson_score', 'name')", 'unique_together': "(('distroseries', 'package_name'),)", 'object_name': 'Application'}, | ||
128 | 76 | 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), | ||
129 | 77 | 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), | ||
130 | 78 | 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}), | ||
131 | 79 | 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), | ||
132 | 80 | 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), | ||
133 | 81 | 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), | ||
134 | 82 | 'debtags': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), | ||
135 | 83 | 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}), | ||
136 | 84 | 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}), | ||
137 | 85 | 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}), | ||
138 | 86 | 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}), | ||
139 | 87 | 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), | ||
140 | 88 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
141 | 89 | 'imported_from_sca': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), | ||
142 | 90 | 'is_latest': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), | ||
143 | 91 | 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), | ||
144 | 92 | 'license': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), | ||
145 | 93 | 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}), | ||
146 | 94 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), | ||
147 | 95 | 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}), | ||
148 | 96 | 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), | ||
149 | 97 | 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2', 'blank': 'True'}), | ||
150 | 98 | 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}), | ||
151 | 99 | 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}), | ||
152 | 100 | 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}), | ||
153 | 101 | 'section': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}), | ||
154 | 102 | 'version': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}), | ||
155 | 103 | 'wilson_score': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'}) | ||
156 | 104 | }, | ||
157 | 105 | 'webcatalog.applicationmedia': { | ||
158 | 106 | 'Meta': {'ordering': "('url',)", 'unique_together': "(('application', 'url'),)", 'object_name': 'ApplicationMedia'}, | ||
159 | 107 | 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Application']"}), | ||
160 | 108 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
161 | 109 | 'media_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}), | ||
162 | 110 | 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'}) | ||
163 | 111 | }, | ||
164 | 112 | 'webcatalog.applicationwidget': { | ||
165 | 113 | 'Meta': {'object_name': 'ApplicationWidget'}, | ||
166 | 114 | 'applications': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Application']", 'symmetrical': 'False'}), | ||
167 | 115 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
168 | 116 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '32'}), | ||
169 | 117 | 'template_snippet': ('django.db.models.fields.TextField', [], {}) | ||
170 | 118 | }, | ||
171 | 119 | 'webcatalog.consumer': { | ||
172 | 120 | 'Meta': {'object_name': 'Consumer'}, | ||
173 | 121 | 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||
174 | 122 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
175 | 123 | 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}), | ||
176 | 124 | 'secret': ('django.db.models.fields.CharField', [], {'default': "'WYDKgmMYvbPrqsjqWjFpMMVGEfHfHS'", 'max_length': '255', 'blank': 'True'}), | ||
177 | 125 | 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}), | ||
178 | 126 | 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"}) | ||
179 | 127 | }, | ||
180 | 128 | 'webcatalog.department': { | ||
181 | 129 | 'Meta': {'object_name': 'Department'}, | ||
182 | 130 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
183 | 131 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}), | ||
184 | 132 | 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'}), | ||
185 | 133 | 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'}) | ||
186 | 134 | }, | ||
187 | 135 | 'webcatalog.distroseries': { | ||
188 | 136 | 'Meta': {'object_name': 'DistroSeries'}, | ||
189 | 137 | 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}), | ||
190 | 138 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
191 | 139 | 'prerelease': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), | ||
192 | 140 | 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'}) | ||
193 | 141 | }, | ||
194 | 142 | 'webcatalog.exhibit': { | ||
195 | 143 | 'Meta': {'object_name': 'Exhibit'}, | ||
196 | 144 | 'banner_url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), | ||
197 | 145 | 'click_url': ('django.db.models.fields.URLField', [], {'default': "''", 'max_length': '200'}), | ||
198 | 146 | 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||
199 | 147 | 'display': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}), | ||
200 | 148 | 'distroseries': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.DistroSeries']", 'symmetrical': 'False'}), | ||
201 | 149 | 'html': ('django.db.models.fields.TextField', [], {}), | ||
202 | 150 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
203 | 151 | 'package_names': ('django.db.models.fields.CharField', [], {'max_length': '1024'}), | ||
204 | 152 | 'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), | ||
205 | 153 | 'sca_id': ('django.db.models.fields.IntegerField', [], {}), | ||
206 | 154 | 'weight': ('django.db.models.fields.IntegerField', [], {'default': '0'}) | ||
207 | 155 | }, | ||
208 | 156 | 'webcatalog.machine': { | ||
209 | 157 | 'Meta': {'unique_together': "(('owner', 'uuid'),)", 'object_name': 'Machine'}, | ||
210 | 158 | 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '64'}), | ||
211 | 159 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
212 | 160 | 'logo_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56', 'blank': 'True'}), | ||
213 | 161 | 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}), | ||
214 | 162 | 'package_list': ('django.db.models.fields.TextField', [], {}), | ||
215 | 163 | 'packages_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56'}), | ||
216 | 164 | 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'}) | ||
217 | 165 | }, | ||
218 | 166 | 'webcatalog.nonce': { | ||
219 | 167 | 'Meta': {'unique_together': "(('nonce', 'token', 'consumer'),)", 'object_name': 'Nonce'}, | ||
220 | 168 | 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}), | ||
221 | 169 | 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||
222 | 170 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
223 | 171 | 'nonce': ('django.db.models.fields.CharField', [], {'max_length': '255'}), | ||
224 | 172 | 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Token']"}) | ||
225 | 173 | }, | ||
226 | 174 | 'webcatalog.reviewstatsimport': { | ||
227 | 175 | 'Meta': {'object_name': 'ReviewStatsImport'}, | ||
228 | 176 | 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']", 'unique': 'True'}), | ||
229 | 177 | 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), | ||
230 | 178 | 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'}) | ||
231 | 179 | }, | ||
232 | 180 | 'webcatalog.token': { | ||
233 | 181 | 'Meta': {'object_name': 'Token'}, | ||
234 | 182 | 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}), | ||
235 | 183 | 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), | ||
236 | 184 | 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), | ||
237 | 185 | 'token': ('django.db.models.fields.CharField', [], {'default': "'pjfQGToPAEUjSRBWGoACORELAevZrhxtbALJSHxqfaXZAEtIym'", 'max_length': '50', 'primary_key': 'True'}), | ||
238 | 186 | 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'RbVwxdrIRcQnwdlszhxgeoFHoaLpOMUjZebJkAggzeKrAHGcHZ'", 'max_length': '50'}), | ||
239 | 187 | 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}) | ||
240 | 188 | } | ||
241 | 189 | } | ||
242 | 190 | |||
243 | 191 | complete_apps = ['webcatalog'] | ||
244 | 0 | 192 | ||
245 | === modified file 'src/webcatalog/models/__init__.py' | |||
246 | --- src/webcatalog/models/__init__.py 2012-03-19 08:16:51 +0000 | |||
247 | +++ src/webcatalog/models/__init__.py 2012-09-06 14:09:20 +0000 | |||
248 | @@ -26,6 +26,7 @@ | |||
249 | 26 | 'DistroSeries', | 26 | 'DistroSeries', |
250 | 27 | 'Application', | 27 | 'Application', |
251 | 28 | 'ApplicationMedia', | 28 | 'ApplicationMedia', |
252 | 29 | 'ApplicationWidget', | ||
253 | 29 | 'Department', | 30 | 'Department', |
254 | 30 | 'Exhibit', | 31 | 'Exhibit', |
255 | 31 | 'ReviewStatsImport', | 32 | 'ReviewStatsImport', |
256 | @@ -36,6 +37,7 @@ | |||
257 | 36 | from .applications import ( | 37 | from .applications import ( |
258 | 37 | Application, | 38 | Application, |
259 | 38 | ApplicationMedia, | 39 | ApplicationMedia, |
260 | 40 | ApplicationWidget, | ||
261 | 39 | Department, | 41 | Department, |
262 | 40 | DistroSeries, | 42 | DistroSeries, |
263 | 41 | Exhibit, | 43 | Exhibit, |
264 | 42 | 44 | ||
265 | === modified file 'src/webcatalog/models/applications.py' | |||
266 | --- src/webcatalog/models/applications.py 2012-08-21 13:57:11 +0000 | |||
267 | +++ src/webcatalog/models/applications.py 2012-09-06 14:09:20 +0000 | |||
268 | @@ -41,6 +41,7 @@ | |||
269 | 41 | __all__ = [ | 41 | __all__ = [ |
270 | 42 | 'Application', | 42 | 'Application', |
271 | 43 | 'ApplicationMedia', | 43 | 'ApplicationMedia', |
272 | 44 | 'ApplicationWidget', | ||
273 | 44 | 'Department', | 45 | 'Department', |
274 | 45 | 'DistroSeries', | 46 | 'DistroSeries', |
275 | 46 | 'Exhibit', | 47 | 'Exhibit', |
276 | @@ -314,6 +315,19 @@ | |||
277 | 314 | return reverse('wc-package-list') + '?' + pkgs | 315 | return reverse('wc-package-list') + '?' + pkgs |
278 | 315 | 316 | ||
279 | 316 | 317 | ||
280 | 318 | class ApplicationWidget(models.Model): | ||
281 | 319 | name = models.CharField(max_length=32, | ||
282 | 320 | help_text="Just a reference for the admin view.") | ||
283 | 321 | applications = models.ManyToManyField(Application) | ||
284 | 322 | template_snippet = models.TextField() | ||
285 | 323 | |||
286 | 324 | def __unicode__(self): | ||
287 | 325 | return self.name | ||
288 | 326 | |||
289 | 327 | class Meta: | ||
290 | 328 | app_label = 'webcatalog' | ||
291 | 329 | |||
292 | 330 | |||
293 | 317 | # GroupPermissions can't go in fixtures because Permissions are stored as | 331 | # GroupPermissions can't go in fixtures because Permissions are stored as |
294 | 318 | # model metadata, so our Permission's primary key can change without warning | 332 | # model metadata, so our Permission's primary key can change without warning |
295 | 319 | def post_syncdb_handler(sender, **kwargs): | 333 | def post_syncdb_handler(sender, **kwargs): |
296 | 320 | 334 | ||
297 | === modified file 'src/webcatalog/schema.py' | |||
298 | --- src/webcatalog/schema.py 2012-08-23 15:12:54 +0000 | |||
299 | +++ src/webcatalog/schema.py 2012-09-06 14:09:20 +0000 | |||
300 | @@ -66,8 +66,6 @@ | |||
301 | 66 | number_top_rated_apps = schema.IntOption(default=8) | 66 | number_top_rated_apps = schema.IntOption(default=8) |
302 | 67 | screenshots_base_url = schema.StringOption( | 67 | screenshots_base_url = schema.StringOption( |
303 | 68 | default='http://screenshots.ubuntu.com/') | 68 | default='http://screenshots.ubuntu.com/') |
304 | 69 | hib_apps = schema.ListOption( | ||
305 | 70 | item=schema.StringOption(), default=[]) | ||
306 | 71 | ubuntu_series_for_versions = schema.DictOption( | 69 | ubuntu_series_for_versions = schema.DictOption( |
307 | 72 | item=schema.StringOption()) | 70 | item=schema.StringOption()) |
308 | 73 | 71 | ||
309 | 74 | 72 | ||
310 | === modified file 'src/webcatalog/templates/webcatalog/application_detail.html' | |||
311 | --- src/webcatalog/templates/webcatalog/application_detail.html 2012-06-28 13:48:45 +0000 | |||
312 | +++ src/webcatalog/templates/webcatalog/application_detail.html 2012-09-06 14:09:20 +0000 | |||
313 | @@ -109,12 +109,9 @@ | |||
314 | 109 | <p>{{ application.description|htmlize_package_description }}</p> | 109 | <p>{{ application.description|htmlize_package_description }}</p> |
315 | 110 | <div class="install-button">{% install_options application %}</div> | 110 | <div class="install-button">{% install_options application %}</div> |
316 | 111 | </div> | 111 | </div> |
323 | 112 | {% if display_hib_widget %} | 112 | {% for widget in application.applicationwidget_set.all %} |
324 | 113 | <div id="hib-widget" style="width: 652px; padding:24px 16px 8px; clear:both"> | 113 | {% render_app_widget widget.template_snippet %} |
325 | 114 | <p>{{ application.name }} is currently also available as part of the <a href="http://www.humblebundle.com/?utm_source=ubuntu">Humble Bundle</a></p> | 114 | {% endfor %} |
320 | 115 | <iframe style="display: block; margin:0 auto; border:none;" src="http://www.humblebundle.com/_widget/html" width="410" height="150"></iframe> | ||
321 | 116 | </div> | ||
322 | 117 | {% endif %} | ||
326 | 118 | <div class="license"> | 115 | <div class="license"> |
327 | 119 | <table> | 116 | <table> |
328 | 120 | <tr> | 117 | <tr> |
329 | 121 | 118 | ||
330 | === modified file 'src/webcatalog/templatetags/webcatalog.py' | |||
331 | --- src/webcatalog/templatetags/webcatalog.py 2012-07-02 21:25:30 +0000 | |||
332 | +++ src/webcatalog/templatetags/webcatalog.py 2012-09-06 14:09:20 +0000 | |||
333 | @@ -32,6 +32,7 @@ | |||
334 | 32 | from django.utils.html import escape | 32 | from django.utils.html import escape |
335 | 33 | from django.utils.safestring import mark_safe | 33 | from django.utils.safestring import mark_safe |
336 | 34 | from django.utils.translation import ugettext as _ | 34 | from django.utils.translation import ugettext as _ |
337 | 35 | from django.template import Template | ||
338 | 35 | 36 | ||
339 | 36 | from webcatalog.models import Application | 37 | from webcatalog.models import Application |
340 | 37 | from webcatalog.utilities import UserAgentString | 38 | from webcatalog.utilities import UserAgentString |
341 | @@ -196,3 +197,9 @@ | |||
342 | 196 | 197 | ||
343 | 197 | return dict(stars=stars, total=num_ratings, | 198 | return dict(stars=stars, total=num_ratings, |
344 | 198 | STATIC_URL=settings.STATIC_URL, size=size) | 199 | STATIC_URL=settings.STATIC_URL, size=size) |
345 | 200 | |||
346 | 201 | |||
347 | 202 | @register.simple_tag(takes_context=True) | ||
348 | 203 | def render_app_widget(context, template_snippet): | ||
349 | 204 | template = Template(template_snippet) | ||
350 | 205 | return template.render(context) | ||
351 | 199 | 206 | ||
352 | === modified file 'src/webcatalog/tests/factory.py' | |||
353 | --- src/webcatalog/tests/factory.py 2012-08-23 10:07:28 +0000 | |||
354 | +++ src/webcatalog/tests/factory.py 2012-09-06 14:09:20 +0000 | |||
355 | @@ -40,6 +40,7 @@ | |||
356 | 40 | from webcatalog.models import ( | 40 | from webcatalog.models import ( |
357 | 41 | Application, | 41 | Application, |
358 | 42 | ApplicationMedia, | 42 | ApplicationMedia, |
359 | 43 | ApplicationWidget, | ||
360 | 43 | Consumer, | 44 | Consumer, |
361 | 44 | Department, | 45 | Department, |
362 | 45 | DistroSeries, | 46 | DistroSeries, |
363 | @@ -250,6 +251,16 @@ | |||
364 | 250 | expire_date=expire_date | 251 | expire_date=expire_date |
365 | 251 | ) | 252 | ) |
366 | 252 | 253 | ||
367 | 254 | def make_app_widget(self, applications=None, template_snippet=None): | ||
368 | 255 | if applications is None: | ||
369 | 256 | applications = [self.make_application()] | ||
370 | 257 | if template_snippet is None: | ||
371 | 258 | template_snippet = u"Promotion for {{ application.name }}" | ||
372 | 259 | |||
373 | 260 | widget = ApplicationWidget.objects.create( | ||
374 | 261 | template_snippet=template_snippet) | ||
375 | 262 | widget.applications = applications | ||
376 | 263 | |||
377 | 253 | 264 | ||
378 | 254 | class TestCaseWithFactory(TestCase): | 265 | class TestCaseWithFactory(TestCase): |
379 | 255 | 266 | ||
380 | 256 | 267 | ||
381 | === modified file 'src/webcatalog/tests/test_views.py' | |||
382 | --- src/webcatalog/tests/test_views.py 2012-07-02 21:25:30 +0000 | |||
383 | +++ src/webcatalog/tests/test_views.py 2012-09-06 14:09:20 +0000 | |||
384 | @@ -205,16 +205,43 @@ | |||
385 | 205 | self.assertContains(response, "https://twitter.com/share") | 205 | self.assertContains(response, "https://twitter.com/share") |
386 | 206 | 206 | ||
387 | 207 | def test_hib_widget_is_present_for_hib_apps(self): | 207 | def test_hib_widget_is_present_for_hib_apps(self): |
392 | 208 | with patch_settings(HIB_APPS=['pkgfoo']): | 208 | app = self.factory.make_application(package_name='pkgfoo', |
393 | 209 | response, app = self.get_app_and_response() | 209 | name=u'A great foo') |
394 | 210 | 210 | self.factory.make_app_widget( | |
395 | 211 | self.assertContains(response, '<div id="hib-widget"') | 211 | applications=[app], |
396 | 212 | template_snippet=u"<p>Promotion for {{ application.name }}</p>") | ||
397 | 213 | |||
398 | 214 | response = self.get_package_details_response('pkgfoo') | ||
399 | 215 | |||
400 | 216 | self.assertContains(response, '<p>Promotion for A great foo</p>') | ||
401 | 212 | 217 | ||
402 | 213 | def test_hib_widget_not_present_for_non_hib_apps(self): | 218 | def test_hib_widget_not_present_for_non_hib_apps(self): |
407 | 214 | with patch_settings(HIB_APPS=['something', 'different']): | 219 | self.factory.make_application(package_name='pkgfoo', |
408 | 215 | response, app = self.get_app_and_response() | 220 | name=u'A great foo') |
409 | 216 | 221 | other_app = self.factory.make_application(package_name='pkgbar', | |
410 | 217 | self.assertNotContains(response, '<div id="hib-widget"') | 222 | name=u'Another app') |
411 | 223 | self.factory.make_app_widget( | ||
412 | 224 | applications=[other_app], | ||
413 | 225 | template_snippet=u"Promotion for {{ application.name }}") | ||
414 | 226 | |||
415 | 227 | response = self.get_package_details_response('pkgfoo') | ||
416 | 228 | |||
417 | 229 | self.assertNotContains(response, 'Promotion for') | ||
418 | 230 | |||
419 | 231 | def test_multiple_promotions_for_app(self): | ||
420 | 232 | app = self.factory.make_application(package_name='pkgfoo', | ||
421 | 233 | name=u'A great foo') | ||
422 | 234 | self.factory.make_app_widget( | ||
423 | 235 | applications=[app], | ||
424 | 236 | template_snippet=u"Promotion X for {{ application.name }}") | ||
425 | 237 | self.factory.make_app_widget( | ||
426 | 238 | applications=[app], | ||
427 | 239 | template_snippet=u"Promotion Y for {{ application.name }}") | ||
428 | 240 | |||
429 | 241 | response = self.get_package_details_response('pkgfoo') | ||
430 | 242 | |||
431 | 243 | self.assertContains(response, 'Promotion X for A great foo') | ||
432 | 244 | self.assertContains(response, 'Promotion Y for A great foo') | ||
433 | 218 | 245 | ||
434 | 219 | def test_twitter_link_does_not_contain_none(self): | 246 | def test_twitter_link_does_not_contain_none(self): |
435 | 220 | app = self.factory.make_application() | 247 | app = self.factory.make_application() |
436 | 221 | 248 | ||
437 | === modified file 'src/webcatalog/views.py' | |||
438 | --- src/webcatalog/views.py 2012-07-02 21:25:30 +0000 | |||
439 | +++ src/webcatalog/views.py 2012-09-06 14:09:20 +0000 | |||
440 | @@ -219,8 +219,6 @@ | |||
441 | 219 | 219 | ||
442 | 220 | debtags = None if not app.debtags else json.loads(app.debtags) | 220 | debtags = None if not app.debtags else json.loads(app.debtags) |
443 | 221 | 221 | ||
444 | 222 | hib_apps = getattr(settings, 'HIB_APPS', []) | ||
445 | 223 | |||
446 | 224 | atts = { | 222 | atts = { |
447 | 225 | 'application': app, | 223 | 'application': app, |
448 | 226 | 'available_distroseries': app.available_distroseries(), | 224 | 'available_distroseries': app.available_distroseries(), |
449 | @@ -230,7 +228,6 @@ | |||
450 | 230 | reverse('wc-package-detail', args=[package_name])), | 228 | reverse('wc-package-detail', args=[package_name])), |
451 | 231 | 'email_form': form, | 229 | 'email_form': form, |
452 | 232 | 'debtags': debtags, | 230 | 'debtags': debtags, |
453 | 233 | 'display_hib_widget': app.package_name in hib_apps | ||
454 | 234 | } | 231 | } |
455 | 235 | 232 | ||
456 | 236 | return render_to_response( | 233 | return render_to_response( |