Merge lp:~michael.nelson/ubuntu-webcatalog/1015515-only-one-app into lp:ubuntu-webcatalog

Proposed by Michael Nelson
Status: Merged
Approved by: Anthony Lenton
Approved revision: 175
Merged at revision: 152
Proposed branch: lp:~michael.nelson/ubuntu-webcatalog/1015515-only-one-app
Merge into: lp:ubuntu-webcatalog
Prerequisite: lp:~michael.nelson/ubuntu-webcatalog/1015515-dont-create-new-app
Diff against target: 621 lines (+467/-44)
9 files modified
src/webcatalog/migrations/0025_only_one_app_per_series_pkg_name.py (+186/-0)
src/webcatalog/migrations/0026_unique_pkg_name_distroseries.py (+171/-0)
src/webcatalog/models/applications.py (+1/-1)
src/webcatalog/tests/__init__.py (+1/-0)
src/webcatalog/tests/factory.py (+13/-3)
src/webcatalog/tests/helpers.py (+37/-0)
src/webcatalog/tests/test_migrations.py (+57/-0)
src/webcatalog/tests/test_views.py (+0/-33)
src/webcatalog/views.py (+1/-7)
To merge this branch: bzr merge lp:~michael.nelson/ubuntu-webcatalog/1015515-only-one-app
Reviewer Review Type Date Requested Status
Anthony Lenton (community) Approve
Review via email: mp+112553@code.launchpad.net

Commit message

package_name and distroseries are unique together on Application.

Description of the change

Overview
========

Continues on from the prereq for bug 1015515, adding a migration to ensure that duplicates are removed, before updating the unique constraint for Application.

`fab test:pg=true`

Note:
 1) I'm getting "psycopg2.OperationalError: FATAL: role "michael" does not exist" when trying to run tests here with psql (which looks like it's trying to use ident, but I've set local privs in pg_hba to be trust), so
 2) I'm not sure whether I should comment out the migration test as per usual. If it does pass locally with psql, I'm keen to see if it fails on the vps or passes. If it passes, yay.

To post a comment you must log in.
174. By Michael Nelson

Merged 1015515-dont-create-new-app into 1015515-only-one-app.

175. By Michael Nelson

REFACTOR: removed outdated comment and switched get_list_or_404 to get_object now that we have db restriction.

Revision history for this message
Anthony Lenton (elachuni) wrote :

Looks fine!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== added file 'src/webcatalog/migrations/0025_only_one_app_per_series_pkg_name.py'
2--- src/webcatalog/migrations/0025_only_one_app_per_series_pkg_name.py 1970-01-01 00:00:00 +0000
3+++ src/webcatalog/migrations/0025_only_one_app_per_series_pkg_name.py 2012-06-28 14:07:20 +0000
4@@ -0,0 +1,186 @@
5+# encoding: utf-8
6+from south.v2 import DataMigration
7+from django.db.models import Count
8+
9+class Migration(DataMigration):
10+
11+ def forwards(self, orm):
12+ """Delete duplicate apps per distroseries.
13+
14+ Prefer first an sca_imported app, then the most recent.
15+ """
16+ Application = orm['webcatalog.Application']
17+ dups = Application.objects.values(
18+ 'package_name', 'distroseries__code_name').order_by().annotate(
19+ num_dups=Count('distroseries')).filter(num_dups__gt=1)
20+ for dup in dups:
21+ sca_version = Application.objects.filter(
22+ package_name=dup['package_name'],
23+ distroseries__code_name=dup['distroseries__code_name'],
24+ imported_from_sca=True)
25+ if sca_version:
26+ sca_version = sca_version[0]
27+ dups_to_delete = Application.objects.filter(
28+ package_name=dup['package_name'],
29+ distroseries__code_name=dup['distroseries__code_name'])
30+ dups_to_delete = dups_to_delete.exclude(id=sca_version.id)
31+ dups_to_delete.delete()
32+ else:
33+ dup_apps = Application.objects.filter(
34+ package_name=dup['package_name'],
35+ distroseries__code_name=dup['distroseries__code_name']
36+ ).order_by('-id')
37+ most_recent = dup_apps[0]
38+ dup_apps.exclude(id=most_recent.id).delete()
39+
40+ def backwards(self, orm):
41+ pass
42+
43+
44+ models = {
45+ 'auth.group': {
46+ 'Meta': {'object_name': 'Group'},
47+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
48+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
49+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
50+ },
51+ 'auth.permission': {
52+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
53+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
54+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
55+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
56+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
57+ },
58+ 'auth.user': {
59+ 'Meta': {'object_name': 'User'},
60+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
61+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
62+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
63+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
64+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
65+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
66+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
67+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
68+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
69+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
70+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
71+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
72+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
73+ },
74+ 'contenttypes.contenttype': {
75+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
76+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
77+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
78+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
79+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
80+ },
81+ 'webcatalog.application': {
82+ 'Meta': {'ordering': "('-wilson_score', 'name')", 'unique_together': "(('distroseries', 'archive_id'),)", 'object_name': 'Application'},
83+ 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
84+ 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
85+ 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}),
86+ 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
87+ 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
88+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
89+ 'debtags': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
90+ 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}),
91+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
92+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}),
93+ 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
94+ 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
95+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
96+ 'imported_from_sca': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
97+ 'is_latest': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
98+ 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
99+ 'license': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
100+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}),
101+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
102+ 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
103+ 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
104+ 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2', 'blank': 'True'}),
105+ 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}),
106+ 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
107+ 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
108+ 'section': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
109+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
110+ 'wilson_score': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
111+ },
112+ 'webcatalog.applicationmedia': {
113+ 'Meta': {'ordering': "('url',)", 'unique_together': "(('application', 'url'),)", 'object_name': 'ApplicationMedia'},
114+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Application']"}),
115+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
116+ 'media_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
117+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
118+ },
119+ 'webcatalog.consumer': {
120+ 'Meta': {'object_name': 'Consumer'},
121+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
122+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
123+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
124+ 'secret': ('django.db.models.fields.CharField', [], {'default': "'HQTqmfphaOwnrnRfWIzLsPUUqqthuh'", 'max_length': '255', 'blank': 'True'}),
125+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
126+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"})
127+ },
128+ 'webcatalog.department': {
129+ 'Meta': {'object_name': 'Department'},
130+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
131+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
132+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'}),
133+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'})
134+ },
135+ 'webcatalog.distroseries': {
136+ 'Meta': {'object_name': 'DistroSeries'},
137+ 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
138+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
139+ 'prerelease': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
140+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'})
141+ },
142+ 'webcatalog.exhibit': {
143+ 'Meta': {'object_name': 'Exhibit'},
144+ 'banner_url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
145+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
146+ 'display': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
147+ 'distroseries': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.DistroSeries']", 'symmetrical': 'False'}),
148+ 'html': ('django.db.models.fields.TextField', [], {}),
149+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
150+ 'package_names': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
151+ 'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
152+ 'sca_id': ('django.db.models.fields.IntegerField', [], {}),
153+ 'weight': ('django.db.models.fields.IntegerField', [], {'default': '0'})
154+ },
155+ 'webcatalog.machine': {
156+ 'Meta': {'unique_together': "(('owner', 'uuid'),)", 'object_name': 'Machine'},
157+ 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
158+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
159+ 'logo_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56', 'blank': 'True'}),
160+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
161+ 'package_list': ('django.db.models.fields.TextField', [], {}),
162+ 'packages_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56'}),
163+ 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
164+ },
165+ 'webcatalog.nonce': {
166+ 'Meta': {'object_name': 'Nonce'},
167+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
168+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
169+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
170+ 'nonce': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
171+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Token']"})
172+ },
173+ 'webcatalog.reviewstatsimport': {
174+ 'Meta': {'object_name': 'ReviewStatsImport'},
175+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']", 'unique': 'True'}),
176+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
177+ 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
178+ },
179+ 'webcatalog.token': {
180+ 'Meta': {'object_name': 'Token'},
181+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
182+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
183+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
184+ 'token': ('django.db.models.fields.CharField', [], {'default': "'sZsoIBDSOGiMtIjLpqHhOsnLNFoJrpvXhCUaZHktQQKzdYMMPU'", 'max_length': '50', 'primary_key': 'True'}),
185+ 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'bpTvhxNJwAHXkVNvtBgcEhuhXeTvySnFZtIQxjgENGFnrrHjMM'", 'max_length': '50'}),
186+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
187+ }
188+ }
189+
190+ complete_apps = ['webcatalog']
191
192=== added file 'src/webcatalog/migrations/0026_unique_pkg_name_distroseries.py'
193--- src/webcatalog/migrations/0026_unique_pkg_name_distroseries.py 1970-01-01 00:00:00 +0000
194+++ src/webcatalog/migrations/0026_unique_pkg_name_distroseries.py 2012-06-28 14:07:20 +0000
195@@ -0,0 +1,171 @@
196+# encoding: utf-8
197+from south.db import db
198+from south.v2 import SchemaMigration
199+
200+
201+class Migration(SchemaMigration):
202+
203+ def forwards(self, orm):
204+
205+ # Removing unique constraint on 'Application', fields ['archive_id', 'distroseries']
206+ db.delete_unique('webcatalog_application', ['archive_id', 'distroseries_id'])
207+
208+ # Adding unique constraint on 'Application', fields ['distroseries', 'package_name']
209+ db.create_unique('webcatalog_application', ['distroseries_id', 'package_name'])
210+
211+
212+ def backwards(self, orm):
213+
214+ # Removing unique constraint on 'Application', fields ['distroseries', 'package_name']
215+ db.delete_unique('webcatalog_application', ['distroseries_id', 'package_name'])
216+
217+ # Adding unique constraint on 'Application', fields ['archive_id', 'distroseries']
218+ db.create_unique('webcatalog_application', ['archive_id', 'distroseries_id'])
219+
220+ models = {
221+ 'auth.group': {
222+ 'Meta': {'object_name': 'Group'},
223+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
224+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
225+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
226+ },
227+ 'auth.permission': {
228+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
229+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
230+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
231+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
232+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
233+ },
234+ 'auth.user': {
235+ 'Meta': {'object_name': 'User'},
236+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
237+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
238+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
239+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
240+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
241+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
242+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
243+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
244+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
245+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
246+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
247+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
248+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
249+ },
250+ 'contenttypes.contenttype': {
251+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
252+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
253+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
254+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
255+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
256+ },
257+ 'webcatalog.application': {
258+ 'Meta': {'ordering': "('-wilson_score', 'name')", 'unique_together': "(('distroseries', 'package_name'),)", 'object_name': 'Application'},
259+ 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
260+ 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
261+ 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}),
262+ 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
263+ 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
264+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
265+ 'debtags': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
266+ 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}),
267+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
268+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}),
269+ 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
270+ 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
271+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
272+ 'imported_from_sca': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
273+ 'is_latest': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
274+ 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
275+ 'license': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
276+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}),
277+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
278+ 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
279+ 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
280+ 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2', 'blank': 'True'}),
281+ 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}),
282+ 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
283+ 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
284+ 'section': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
285+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
286+ 'wilson_score': ('django.db.models.fields.FloatField', [], {'db_index': 'True', 'null': 'True', 'blank': 'True'})
287+ },
288+ 'webcatalog.applicationmedia': {
289+ 'Meta': {'ordering': "('url',)", 'unique_together': "(('application', 'url'),)", 'object_name': 'ApplicationMedia'},
290+ 'application': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Application']"}),
291+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
292+ 'media_type': ('django.db.models.fields.CharField', [], {'max_length': '16'}),
293+ 'url': ('django.db.models.fields.URLField', [], {'max_length': '200'})
294+ },
295+ 'webcatalog.consumer': {
296+ 'Meta': {'object_name': 'Consumer'},
297+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
298+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
299+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
300+ 'secret': ('django.db.models.fields.CharField', [], {'default': "'CkaJaMaVaYjiDEVByNnJWMmblaoFop'", 'max_length': '255', 'blank': 'True'}),
301+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
302+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"})
303+ },
304+ 'webcatalog.department': {
305+ 'Meta': {'object_name': 'Department'},
306+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
307+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
308+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'}),
309+ 'slug': ('django.db.models.fields.SlugField', [], {'unique': 'True', 'max_length': '50', 'db_index': 'True'})
310+ },
311+ 'webcatalog.distroseries': {
312+ 'Meta': {'object_name': 'DistroSeries'},
313+ 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
314+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
315+ 'prerelease': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
316+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'})
317+ },
318+ 'webcatalog.exhibit': {
319+ 'Meta': {'object_name': 'Exhibit'},
320+ 'banner_url': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
321+ 'date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
322+ 'display': ('django.db.models.fields.NullBooleanField', [], {'null': 'True', 'blank': 'True'}),
323+ 'distroseries': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.DistroSeries']", 'symmetrical': 'False'}),
324+ 'html': ('django.db.models.fields.TextField', [], {}),
325+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
326+ 'package_names': ('django.db.models.fields.CharField', [], {'max_length': '1024'}),
327+ 'published': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
328+ 'sca_id': ('django.db.models.fields.IntegerField', [], {}),
329+ 'weight': ('django.db.models.fields.IntegerField', [], {'default': '0'})
330+ },
331+ 'webcatalog.machine': {
332+ 'Meta': {'unique_together': "(('owner', 'uuid'),)", 'object_name': 'Machine'},
333+ 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
334+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
335+ 'logo_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56', 'blank': 'True'}),
336+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
337+ 'package_list': ('django.db.models.fields.TextField', [], {}),
338+ 'packages_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56'}),
339+ 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
340+ },
341+ 'webcatalog.nonce': {
342+ 'Meta': {'object_name': 'Nonce'},
343+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
344+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
345+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
346+ 'nonce': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
347+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Token']"})
348+ },
349+ 'webcatalog.reviewstatsimport': {
350+ 'Meta': {'object_name': 'ReviewStatsImport'},
351+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']", 'unique': 'True'}),
352+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
353+ 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
354+ },
355+ 'webcatalog.token': {
356+ 'Meta': {'object_name': 'Token'},
357+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
358+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
359+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
360+ 'token': ('django.db.models.fields.CharField', [], {'default': "'XTzTOPjxjOzhqRWOpdmAPcbaAZqIKoiJKPnfuAVUpuYhCwgxhm'", 'max_length': '50', 'primary_key': 'True'}),
361+ 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'uZmUdXqaSKXXaCeeLbiwQjLzDPixijEGysvlXSiSpVatcifOxX'", 'max_length': '50'}),
362+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
363+ }
364+ }
365+
366+ complete_apps = ['webcatalog']
367
368=== modified file 'src/webcatalog/models/applications.py'
369--- src/webcatalog/models/applications.py 2012-06-28 14:07:20 +0000
370+++ src/webcatalog/models/applications.py 2012-06-28 14:07:20 +0000
371@@ -191,7 +191,7 @@
372
373 class Meta:
374 app_label = 'webcatalog'
375- unique_together = ('distroseries', 'archive_id')
376+ unique_together = ('distroseries', 'package_name')
377 ordering = ('-wilson_score', 'name')
378
379
380
381=== modified file 'src/webcatalog/tests/__init__.py'
382--- src/webcatalog/tests/__init__.py 2012-05-30 21:57:52 +0000
383+++ src/webcatalog/tests/__init__.py 2012-06-28 14:07:20 +0000
384@@ -24,6 +24,7 @@
385 from .test_handlers import *
386 from .test_models import *
387 from .test_managers import *
388+from .test_migrations import *
389 from .test_pep8 import *
390 from .test_preflight import *
391 from .test_templatetags import *
392
393=== modified file 'src/webcatalog/tests/factory.py'
394--- src/webcatalog/tests/factory.py 2012-06-28 14:07:20 +0000
395+++ src/webcatalog/tests/factory.py 2012-06-28 14:07:20 +0000
396@@ -29,7 +29,10 @@
397 from itertools import count
398 from django.contrib.auth.models import User
399 from django.contrib.sessions.models import Session
400-from django.test import TestCase
401+from django.test import (
402+ TestCase,
403+ TransactionTestCase,
404+)
405 from django_openid_auth.models import UserOpenID
406
407 from webcatalog.models import (
408@@ -104,7 +107,7 @@
409 ratings_histogram='', screenshot_url='',
410 archive_id=None, version='', is_latest=False,
411 wilson_score=0.0, debtags=[], departments=None,
412- license='', price=None):
413+ license='', price=None, imported_from_sca=False):
414 if name is None:
415 name = self.get_unique_string(prefix='Readable Name')
416 if package_name is None:
417@@ -124,7 +127,7 @@
418 ratings_histogram=ratings_histogram,
419 archive_id=archive_id, version=version, is_latest=is_latest,
420 wilson_score=wilson_score, debtags=debtags,
421- license=license, price=price)
422+ license=license, price=price, imported_from_sca=imported_from_sca)
423
424 if departments is not None:
425 for d in departments:
426@@ -237,3 +240,10 @@
427 def setUp(self):
428 super(TestCaseWithFactory, self).setUp()
429 self.factory = WebCatalogObjectFactory()
430+
431+
432+class TxTestCaseWithFactory(TransactionTestCase):
433+
434+ def setUp(self):
435+ super(TxTestCaseWithFactory, self).setUp()
436+ self.factory = WebCatalogObjectFactory()
437
438=== modified file 'src/webcatalog/tests/helpers.py'
439--- src/webcatalog/tests/helpers.py 2012-06-06 18:00:26 +0000
440+++ src/webcatalog/tests/helpers.py 2012-06-28 14:07:20 +0000
441@@ -30,6 +30,10 @@
442
443 from contextlib import contextmanager
444 from django.conf import settings
445+from django.core.management import call_command
446+from south.migration import Migrations
447+
448+from webcatalog.tests.factory import TxTestCaseWithFactory
449
450
451 # Original snippet from http://djangosnippets.org/snippets/2156/
452@@ -60,3 +64,36 @@
453 finally:
454 switch_settings(**old_settings)
455 # end snippet
456+
457+
458+class MigrationTestCase(TxTestCaseWithFactory):
459+ """A Test case for testing migrations."""
460+
461+ # These must be defined by subclasses.
462+ start_migration = None
463+ dest_migration = None
464+ django_application = None
465+
466+ def setUp(self):
467+ super(MigrationTestCase, self).setUp()
468+ migrations = Migrations(self.django_application)
469+ self.start_orm = migrations[self.start_migration].orm()
470+ self.dest_orm = migrations[self.dest_migration].orm()
471+
472+ # Ensure the migration history is up-to-date with a fake migration.
473+ # The other option would be to use the south setting for these tests
474+ # so that the migrations are used to setup the test db.
475+ call_command('migrate', self.django_application, fake=True,
476+ verbosity=0)
477+ # Then migrate back to the start migration.
478+ call_command('migrate', self.django_application, self.start_migration,
479+ verbosity=0)
480+
481+ def tearDown(self):
482+ # Leave the db in the final state so that the test runner doesn't
483+ # error when truncating the database.
484+ call_command('migrate', self.django_application, verbosity=0)
485+
486+ def migrate_to_dest(self):
487+ call_command('migrate', self.django_application, self.dest_migration,
488+ verbosity=0)
489
490=== added file 'src/webcatalog/tests/test_migrations.py'
491--- src/webcatalog/tests/test_migrations.py 1970-01-01 00:00:00 +0000
492+++ src/webcatalog/tests/test_migrations.py 2012-06-28 14:07:20 +0000
493@@ -0,0 +1,57 @@
494+# -*- coding: utf-8 -*-
495+# This file is part of the Apps Directory
496+# Copyright (C) 2011 Canonical Ltd.
497+#
498+# This program is free software: you can redistribute it and/or modify
499+# it under the terms of the GNU Affero General Public License as
500+# published by the Free Software Foundation, either version 3 of the
501+# License, or (at your option) any later version.
502+#
503+# This program is distributed in the hope that it will be useful,
504+# but WITHOUT ANY WARRANTY; without even the implied warranty of
505+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
506+# GNU Affero General Public License for more details.
507+#
508+# You should have received a copy of the GNU Affero General Public License
509+# along with this program. If not, see <http://www.gnu.org/licenses/>.
510+
511+"""Temporary test cases for developing and reviewing migrations."""
512+
513+from webcatalog.tests.helpers import MigrationTestCase
514+from webcatalog.models import Application
515+
516+
517+class OnlyOneAppPerSeriesPkgNameTestCase(MigrationTestCase):
518+ start_migration = '0024_remove_application_id_and_rename_for_purchase'
519+ dest_migration = '0025_only_one_app_per_series_pkg_name'
520+ django_application = 'webcatalog'
521+
522+ def test_latest_app_remains_when_neither_sca_imported(self):
523+ oneiric = self.factory.make_distroseries(code_name='oneiric')
524+ precise = self.factory.make_distroseries(code_name='precise')
525+ self.factory.make_application(
526+ package_name='foo', distroseries=oneiric)
527+ foo_oneiric_2 = self.factory.make_application(
528+ package_name='foo', distroseries=oneiric)
529+ foo_precise = self.factory.make_application(
530+ package_name='foo', distroseries=precise)
531+
532+ self.migrate_to_dest()
533+
534+ all_foo_apps = Application.objects.filter(package_name='foo')
535+ self.assertEqual([foo_oneiric_2, foo_precise], list(all_foo_apps))
536+
537+ def test_sca_imported_app_wins(self):
538+ oneiric = self.factory.make_distroseries(code_name='oneiric')
539+ precise = self.factory.make_distroseries(code_name='precise')
540+ foo_oneiric_1 = self.factory.make_application(
541+ package_name='foo', distroseries=oneiric, imported_from_sca=True)
542+ self.factory.make_application(
543+ package_name='foo', distroseries=oneiric)
544+ foo_precise = self.factory.make_application(
545+ package_name='foo', distroseries=precise)
546+
547+ self.migrate_to_dest()
548+
549+ all_foo_apps = Application.objects.filter(package_name='foo')
550+ self.assertEqual([foo_oneiric_1, foo_precise], list(all_foo_apps))
551
552=== modified file 'src/webcatalog/tests/test_views.py'
553--- src/webcatalog/tests/test_views.py 2012-06-28 14:07:20 +0000
554+++ src/webcatalog/tests/test_views.py 2012-06-28 14:07:20 +0000
555@@ -419,29 +419,6 @@
556 response,
557 '<iframe src="http://example.com/video_iframe.html"')
558
559- def test_double_entry_in_the_db_is_displayed_sainely(self):
560- """Make sure that bug:1006442 is fixed"""
561- app = self.factory.make_application()
562- self.factory.make_application(
563- package_name=app.package_name,
564- distroseries=app.distroseries
565- )
566- response = self.client.get(self.get_app_details_url(app))
567-
568- self.assertEquals(response.status_code, 200)
569-
570- def test_make_sure_doubled_entry_displays_only_one_distroseries(self):
571- app = self.factory.make_application()
572- self.factory.make_application(
573- package_name=app.package_name,
574- distroseries=app.distroseries
575- )
576- response = self.client.get(
577- reverse('wc-package-detail', args=[app.package_name]))
578-
579- ds = "Ubuntu {0}".format(app.distroseries.version)
580- self.assertContains(response, ds, 1)
581-
582
583 class ApplicationDetailNoSeriesTestCase(TestCaseWithFactory):
584 def test_renders_latest(self):
585@@ -1162,16 +1139,6 @@
586
587 self.assertEqual(200, response.status_code)
588
589- def test_handles_multiple_apps_for_same_distroseries(self):
590- ds = self.factory.make_distroseries()
591- self.factory.make_application(package_name='skype', distroseries=ds)
592- self.factory.make_application(package_name='skype', distroseries=ds)
593-
594- response = self.client.get(reverse('wc-package-reviews',
595- args=[ds.code_name, 'skype']))
596-
597- self.assertEqual(200, response.status_code)
598-
599 def test_handles_api_error(self):
600 app = self.factory.make_application()
601 self.mock_get_reviews.side_effect = APIError('500', 'error')
602
603=== modified file 'src/webcatalog/views.py'
604--- src/webcatalog/views.py 2012-06-21 09:21:42 +0000
605+++ src/webcatalog/views.py 2012-06-28 14:07:20 +0000
606@@ -231,14 +231,8 @@
607
608
609 def application_reviews(request, package_name, distro, ajax=False, page=1):
610- # XXX 2012-05-25 bug=1002262 michaeln Multiple apps for distroseries.
611- # It seems that we have a few applications which are present in
612- # multiple archives for the same distroseries (skype,
613- # unity-lens-utilities, probably via a myapps PPA as well as extras?).
614- # Don't break when this happens.
615- apps = get_list_or_404(Application, package_name=package_name,
616+ app = get_object_or_404(Application, package_name=package_name,
617 distroseries__code_name=distro)
618- app = apps[0]
619
620 # XXX michaeln 2011-09-15 bug=851662 Better review language options.
621 reviews = WebServices().get_reviews_for_package(

Subscribers

People subscribed via source and target branches