Merge lp:~elachuni/ubuntu-webcatalog/machine-model into lp:ubuntu-webcatalog

Proposed by Anthony Lenton
Status: Merged
Approved by: Anthony Lenton
Approved revision: 56
Merged at revision: 46
Proposed branch: lp:~elachuni/ubuntu-webcatalog/machine-model
Merge into: lp:ubuntu-webcatalog
Diff against target: 1224 lines (+991/-9)
14 files modified
src/webcatalog/admin.py (+7/-0)
src/webcatalog/api/forms.py (+46/-0)
src/webcatalog/api/handlers.py (+81/-0)
src/webcatalog/api/urls.py (+20/-2)
src/webcatalog/migrations/0007_add_machine.py (+136/-0)
src/webcatalog/migrations/0008_add_oauth_tables.py (+176/-0)
src/webcatalog/models/__init__.py (+2/-0)
src/webcatalog/models/applications.py (+17/-3)
src/webcatalog/models/oauthtoken.py (+3/-3)
src/webcatalog/schema.py (+3/-0)
src/webcatalog/tests/__init__.py (+6/-0)
src/webcatalog/tests/factory.py (+52/-0)
src/webcatalog/tests/test_api.py (+254/-0)
src/webcatalog/tests/test_handlers.py (+188/-1)
To merge this branch: bzr merge lp:~elachuni/ubuntu-webcatalog/machine-model
Reviewer Review Type Date Requested Status
Ricardo Kirkner (community) Approve
Review via email: mp+69506@code.launchpad.net

Commit message

Added a Machine model and fleshes out the api to interact with it.

Description of the change

Overview
========
This branch adds a Machine model and fleshes out the api to interact with the model.

Details
=======
Work was based on a stub api provided by didrocks, that's been merged into this branch already.
Code has been tested using the oneconf client, besides the handler tests included.
The model itself is just a boilerplate bag of fields, so no tests added for that. Client tests (using the piston-mini-client code oneconf uses) can be included in a later branch.
There's no auth protection on the handlers yet, so all machine data gets piled onto the first user in the system. This needs to be added for the api to be really usable, but can also be included in a later branch.

To post a comment you must log in.
50. By Anthony Lenton

Two small code style fixes

51. By Anthony Lenton

Merged in latest changes from trunk

52. By Anthony Lenton

Plugged in authentication.

53. By Anthony Lenton

Plugged in api tests.

54. By Anthony Lenton

Completed tests for list-machines.

55. By Anthony Lenton

Completed API tests.

Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

Some comments:

l. 115: still using hardcoded User instead of the one from the request
l. 736: factory methods default prefix don't end in '-' (let's be consistent)
l. 845: you don't want to 'import simplejson' directly, but 'from django.utils import simplejson', as that will try to load python's json module first, and fallback to the simplejson module provided by django if not found
l. 911, why not check for status code here? to make sure it's 204?
l. 1003 why test for the content instead of the status code ('Success' vs 200)?. Why is the content not the same as with the status ('Success' vs 'ok')?
l. 1064: why use None for the request when you have a request you can use (self.request)?

Otherwise looks pretty nice.

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

Hi Ricardo,

Thanks for the review! I'm attaching a diff that fixes all your
 comments.

On 07/29/2011 11:31 AM, Ricardo Kirkner wrote:
> l. 115: still using hardcoded User instead of the one from the request
True! Fixed

> l. 736: factory methods default prefix don't end in '-' (let's be consistent)
Yep, fixed also.

> l. 845: you don't want to 'import simplejson' directly, but 'from django.utils import simplejson', as that will try to load python's json module first, and fallback to the simplejson module provided by django if not found
Fixed.

> l. 911, why not check for status code here? to make sure it's 204?
Yep, it makes sense to check the status also. The test wants to verify
 that the instance was actually deleted from the db, hence the model
 check.

> l. 1003 why test for the content instead of the status code ('Success' vs 200)?. Why is the content not the same as with the status ('Success' vs 'ok')?

Fixed the test so that it checks for both status and content (with
 assertContains).
The 'Success' and 'ok' responses are for different things (command was
 successful, vs. current status of the server). I don't think it makes
 much sense to make these two response messages the same.

> l. 1064: why use None for the request when you have a request you can use (self.request)?
And, also fixed.

I'm attaching a diff with just these fixes, hope it works :)

1=== modified file 'src/webcatalog/api/handlers.py'
2--- src/webcatalog/api/handlers.py 2011-07-28 18:49:26 +0000
3+++ src/webcatalog/api/handlers.py 2011-07-29 15:06:35 +0000
4@@ -49,10 +49,7 @@
5 fields = ('uuid', 'hostname', 'logo_checksum', 'packages_checksum')
6
7 def read(self, request):
8- # Once the api is authenticated this will be just request.user:
9- user = User.objects.all()[0]
10-
11- result = Machine.objects.filter(owner=user)
12+ result = Machine.objects.filter(owner=request.user)
13 return result.defer('package_list')
14
15 class MachineHandler(BaseHandler):
16
17=== modified file 'src/webcatalog/tests/factory.py'
18--- src/webcatalog/tests/factory.py 2011-07-28 20:39:00 +0000
19+++ src/webcatalog/tests/factory.py 2011-07-29 15:08:51 +0000
20@@ -150,14 +150,14 @@
21 user = self.make_user()
22
23 consumer_key = user.useropenid_set.get().claimed_id.split('/')[-1]
24- consumer_secret = self.get_unique_string(prefix='consumer-secret')
25+ consumer_secret = self.get_unique_string(prefix='consumer-secret-')
26 consumer = Consumer(user=user, key=consumer_key,
27 secret=consumer_secret)
28 if save:
29 consumer.save()
30- token_string = self.get_unique_string(prefix='token')
31- token_secret = self.get_unique_string(prefix='token-secret')
32- token_name = self.get_unique_string(prefix='token-name')
33+ token_string = self.get_unique_string(prefix='token-')
34+ token_secret = self.get_unique_string(prefix='token-secret-')
35+ token_name = self.get_unique_string(prefix='token-name-')
36 token = Token(consumer=consumer, token=token_string,
37 token_secret=token_secret, name=token_name)
38 if save:
39
40=== modified file 'src/webcatalog/tests/test_api.py'
41--- src/webcatalog/tests/test_api.py 2011-07-29 13:08:31 +0000
42+++ src/webcatalog/tests/test_api.py 2011-07-29 15:12:08 +0000
43@@ -30,7 +30,7 @@
44 'UpdatePackageListTestCase',
45 ]
46
47-import simplejson
48+from django.utils import simplejson
49
50 from django.test import TestCase
51 from oauth.oauth import (
52@@ -144,6 +144,7 @@
53 response = self.client.delete(url,
54 **self.auth_header_for_user(url, user=machine.owner))
55
56+ self.assertEqual(204, response.status_code)
57 self.assertRaises(Machine.DoesNotExist, Machine.objects.get,
58 uuid=machine.uuid, owner=machine.owner)
59
60@@ -237,7 +238,7 @@
61 content_type='application/json',
62 **self.auth_header_for_user(url, user=machine.owner))
63
64- self.assertEqual('"Success"', response.content)
65+ self.assertContains(response, 'Success')
66 updated = Machine.objects.get(uuid=machine.uuid, owner=machine.owner)
67 self.assertEqual(expected, updated.package_list)
68
69
70=== modified file 'src/webcatalog/tests/test_handlers.py'
71--- src/webcatalog/tests/test_handlers.py 2011-07-28 18:49:26 +0000
72+++ src/webcatalog/tests/test_handlers.py 2011-07-29 15:07:55 +0000
73@@ -59,7 +59,7 @@
74 class ListMachineHandlerTestCase(HandlerTestCase):
75 def test_no_machines_returns_empty_list(self):
76 handler = ListMachinesHandler()
77- machines = handler.read(None)
78+ machines = handler.read(self.request)
79 self.assertEqual([], list(machines))
80
81 def test_multiple_machines_returns_as_expected(self):
82@@ -67,12 +67,28 @@
83 machine2 = self.factory.make_machine(owner=self.user)
84 handler = ListMachinesHandler()
85
86- machines = handler.read(None)
87+ machines = handler.read(self.request)
88
89 self.assertEqual(2, len(machines))
90 expected = set([machine1.uuid, machine2.uuid])
91 self.assertEqual(expected, set(x.uuid for x in machines))
92
93+ def test_machine_for_user_other_than_the_first(self):
94+ """Check that we're not returning only machiens for the first user"""
95+ self.factory.make_user()
96+ self.factory.make_user()
97+ # Not the first user in the system:
98+ user = User.objects.all()[1]
99+ machine = self.factory.make_machine(owner=user)
100+ handler = ListMachinesHandler()
101+ request = HttpRequest()
102+ request.user = user
103+
104+ machines = handler.read(request)
105+
106+ self.assertEqual(1, len(machines))
107+ self.assertEqual(machine.uuid, machines[0].uuid)
108+
109
110 class MachineHandlerTestCase(HandlerTestCase):
111 def test_read_invalid_uuid_returns_404(self):
56. By Anthony Lenton

Minor changes per code review.

Revision history for this message
Ricardo Kirkner (ricardokirkner) wrote :

Lovely!

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'src/webcatalog/admin.py'
2--- src/webcatalog/admin.py 2011-07-02 04:53:26 +0000
3+++ src/webcatalog/admin.py 2011-07-29 15:59:29 +0000
4@@ -26,11 +26,13 @@
5 Application,
6 Department,
7 DistroSeries,
8+ Machine,
9 )
10
11 __metaclass__ = type
12 __all__ = [
13 'ApplicationAdmin',
14+ 'MachineAdmin',
15 ]
16
17
18@@ -40,6 +42,11 @@
19 list_filter = ('distroseries', 'departments')
20 exclude = ('for_purchase', 'archive_id')
21
22+class MachineAdmin(admin.ModelAdmin):
23+ search_fields = ('owner__username', 'hostname', 'uuid')
24+ list_display = ('hostname', 'uuid', 'owner')
25+
26 admin.site.register(Application, ApplicationAdmin)
27 admin.site.register(Department)
28 admin.site.register(DistroSeries)
29+admin.site.register(Machine, MachineAdmin)
30
31=== added file 'src/webcatalog/api/forms.py'
32--- src/webcatalog/api/forms.py 1970-01-01 00:00:00 +0000
33+++ src/webcatalog/api/forms.py 2011-07-29 15:59:29 +0000
34@@ -0,0 +1,46 @@
35+# -*- coding: utf-8 -*-
36+# This file is part of the Ubuntu Web Catalog
37+# Copyright (C) 2011 Canonical Ltd.
38+#
39+# This program is free software: you can redistribute it and/or modify
40+# it under the terms of the GNU Affero General Public License as
41+# published by the Free Software Foundation, either version 3 of the
42+# License, or (at your option) any later version.
43+#
44+# This program is distributed in the hope that it will be useful,
45+# but WITHOUT ANY WARRANTY; without even the implied warranty of
46+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
47+# GNU Affero General Public License for more details.
48+#
49+# You should have received a copy of the GNU Affero General Public License
50+# along with this program. If not, see <http://www.gnu.org/licenses/>.
51+
52+"""Forms for using within the Ubuntu Web Catalog API."""
53+
54+from __future__ import absolute_import
55+
56+__metaclass__ = type
57+__all__ = [
58+ 'MachineCreateUpdateForm',
59+ 'MachineUpdatePackagesForm',
60+]
61+
62+from django import forms
63+
64+from webcatalog.models import Machine
65+
66+class MachineCreateUpdateForm(forms.ModelForm):
67+ class Meta:
68+ model = Machine
69+ fields = (
70+ 'hostname',
71+ 'logo_checksum',
72+ )
73+
74+class MachineUpdatePackagesForm(forms.ModelForm):
75+ class Meta:
76+ model = Machine
77+ fields = (
78+ 'packages_checksum',
79+ 'package_list',
80+ )
81
82=== modified file 'src/webcatalog/api/handlers.py'
83--- src/webcatalog/api/handlers.py 2011-06-29 11:40:16 +0000
84+++ src/webcatalog/api/handlers.py 2011-07-29 15:59:29 +0000
85@@ -24,11 +24,92 @@
86 'ServerStatusHandler',
87 ]
88
89+import json
90+
91+from django.contrib.auth.models import User
92+from django.http import (
93+ HttpResponse,
94+ HttpResponseBadRequest,
95+ HttpResponseNotFound,
96+ )
97 from piston.handler import BaseHandler
98
99+from webcatalog.models import Machine
100+from .forms import MachineCreateUpdateForm, MachineUpdatePackagesForm
101+
102 class ServerStatusHandler(BaseHandler):
103 allowed_methods = ('GET',)
104
105 def read(self, request):
106 return "ok"
107
108+class ListMachinesHandler(BaseHandler):
109+ allowed_methods = ('GET',)
110+ model = Machine
111+ fields = ('uuid', 'hostname', 'logo_checksum', 'packages_checksum')
112+
113+ def read(self, request):
114+ result = Machine.objects.filter(owner=request.user)
115+ return result.defer('package_list')
116+
117+class MachineHandler(BaseHandler):
118+ allowed_methods = ('GET', 'POST', 'DELETE')
119+ model = Machine
120+ fields = ('uuid', 'hostname', 'logo_checksum', 'packages_checksum')
121+
122+ def read(self, request, uuid):
123+ try:
124+ return Machine.objects.get(owner=request.user, uuid=uuid)
125+ except Machine.DoesNotExist:
126+ return HttpResponseNotFound('Invalid machine UUID')
127+
128+ def create(self, request, uuid):
129+ if not hasattr(request, 'data'):
130+ return HttpResponseBadRequest("Unable to deserialize request")
131+ form = MachineCreateUpdateForm(request.data)
132+ if form.is_valid():
133+ # Make this call work both for updating and creating machines
134+ instance, created = Machine.objects.get_or_create(
135+ owner=request.user, uuid=uuid)
136+ form = MachineCreateUpdateForm(request.data, instance=instance)
137+ return form.save()
138+ else:
139+ errors = dict((k, map(unicode, v))
140+ for (k, v) in form.errors.items())
141+ result = {'status': 'error', 'errors': errors}
142+ return result
143+
144+ def delete(self, request, uuid):
145+ instances = Machine.objects.filter(owner=request.user, uuid=uuid)
146+ if instances.count() > 0:
147+ instances.delete()
148+ return HttpResponse(status=204)
149+ else:
150+ return HttpResponseNotFound('Invalid machine UUID')
151+
152+
153+class PackagesHandler(BaseHandler):
154+ allowed_methods = ('GET', 'POST',)
155+ def read(self, request, uuid):
156+ try:
157+ instance = Machine.objects.get(owner=request.user, uuid=uuid)
158+ return instance.package_list
159+ except Machine.DoesNotExist:
160+ return HttpResponseNotFound('Invalid machine UUID')
161+
162+ def create(self, request, uuid):
163+ try:
164+ instance = Machine.objects.get(owner=request.user, uuid=uuid)
165+ except Machine.DoesNotExist:
166+ return HttpResponseNotFound('Invalid machine UUID')
167+ if not hasattr(request, 'data'):
168+ return HttpResponseBadRequest("Unable to deserialize request")
169+ form = MachineUpdatePackagesForm(request.data, instance=instance)
170+ if form.is_valid():
171+ form.save()
172+ return 'Success'
173+ else:
174+ errors = dict((k, map(unicode, v))
175+ for (k, v) in form.errors.items())
176+ result = {'status': 'error', 'errors': errors}
177+ return result
178
179=== modified file 'src/webcatalog/api/urls.py'
180--- src/webcatalog/api/urls.py 2011-06-29 11:40:16 +0000
181+++ src/webcatalog/api/urls.py 2011-07-29 15:59:29 +0000
182@@ -18,13 +18,25 @@
183
184 from piston.resource import Resource
185 from webcatalog.api.handlers import (
186+ ListMachinesHandler,
187+ MachineHandler,
188+ PackagesHandler,
189 ServerStatusHandler,
190 )
191 from webcatalog.auth import SSOOAuthAuthentication
192
193 auth = SSOOAuthAuthentication(realm="Ubuntu Web Catalog")
194
195+class CSRFExemptResource(Resource):
196+ """A Custom Resource that is csrf exempt"""
197+ def __init__(self, handler, authentication=None):
198+ super(CSRFExemptResource, self).__init__(handler, authentication)
199+ self.csrf_exempt = True
200+
201 server_status_resource = Resource(handler=ServerStatusHandler)
202+list_machines_resource = Resource(handler=ListMachinesHandler, authentication=auth)
203+machine_resource = CSRFExemptResource(handler=MachineHandler, authentication=auth)
204+packages_resource = CSRFExemptResource(handler=PackagesHandler, authentication=auth)
205
206 urlpatterns = patterns('',
207 # get status of the service (usually just "ok", might be "read-only")
208@@ -32,6 +44,12 @@
209 # send a moderation request
210 # GET /1.0/server-status/
211 url(r'^1.0/server-status/$', server_status_resource,
212- name='server-status'),
213-
214+ name='wb-server-status'),
215+ # GET /1.0/list-machines/
216+ url(r'^1.0/list-machines/$', list_machines_resource,
217+ name='wb-list-machines'),
218+ url(r'^1.0/machine/(?P<uuid>[-\w]+)/$',
219+ machine_resource, name='wb-machine'),
220+ url(r'^1.0/packages/(?P<uuid>[-\w]+)/$',
221+ packages_resource, name='wb-packages'),
222 )
223
224=== added file 'src/webcatalog/migrations/0007_add_machine.py'
225--- src/webcatalog/migrations/0007_add_machine.py 1970-01-01 00:00:00 +0000
226+++ src/webcatalog/migrations/0007_add_machine.py 2011-07-29 15:59:29 +0000
227@@ -0,0 +1,136 @@
228+# encoding: utf-8
229+import datetime
230+from south.db import db
231+from south.v2 import SchemaMigration
232+from django.db import models
233+
234+class Migration(SchemaMigration):
235+
236+ def forwards(self, orm):
237+
238+ # Adding model 'Machine'
239+ db.create_table('webcatalog_machine', (
240+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
241+ ('owner', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'])),
242+ ('uuid', self.gf('django.db.models.fields.CharField')(max_length=32, db_index=True)),
243+ ('hostname', self.gf('django.db.models.fields.CharField')(max_length=64)),
244+ ('packages_checksum', self.gf('django.db.models.fields.CharField')(max_length=56)),
245+ ('package_list', self.gf('django.db.models.fields.TextField')()),
246+ ('logo_checksum', self.gf('django.db.models.fields.CharField')(max_length=56, blank=True)),
247+ ))
248+ db.send_create_signal('webcatalog', ['Machine'])
249+
250+ # Adding unique constraint on 'Machine', fields ['owner', 'uuid']
251+ db.create_unique('webcatalog_machine', ['owner_id', 'uuid'])
252+
253+ # Adding unique constraint on 'ReviewStatsImport', fields ['distroseries']
254+ db.create_unique('webcatalog_reviewstatsimport', ['distroseries_id'])
255+
256+
257+ def backwards(self, orm):
258+
259+ # Removing unique constraint on 'ReviewStatsImport', fields ['distroseries']
260+ db.delete_unique('webcatalog_reviewstatsimport', ['distroseries_id'])
261+
262+ # Removing unique constraint on 'Machine', fields ['owner', 'uuid']
263+ db.delete_unique('webcatalog_machine', ['owner_id', 'uuid'])
264+
265+ # Deleting model 'Machine'
266+ db.delete_table('webcatalog_machine')
267+
268+
269+ models = {
270+ 'auth.group': {
271+ 'Meta': {'object_name': 'Group'},
272+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
273+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
274+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
275+ },
276+ 'auth.permission': {
277+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
278+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
279+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
280+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
281+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
282+ },
283+ 'auth.user': {
284+ 'Meta': {'object_name': 'User'},
285+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
286+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
287+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
288+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
289+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
290+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
291+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
292+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
293+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
294+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
295+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
296+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
297+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
298+ },
299+ 'contenttypes.contenttype': {
300+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
301+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
302+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
303+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
304+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
305+ },
306+ 'webcatalog.application': {
307+ 'Meta': {'unique_together': "(('distroseries', 'archive_id'),)", 'object_name': 'Application'},
308+ 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
309+ 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
310+ 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}),
311+ 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
312+ 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
313+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
314+ 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}),
315+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
316+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}),
317+ 'for_purchase': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
318+ 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
319+ 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
320+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
321+ 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
322+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}),
323+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
324+ 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
325+ 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
326+ 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2', 'blank': 'True'}),
327+ 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}),
328+ 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
329+ 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
330+ 'screenshot_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
331+ 'section': ('django.db.models.fields.CharField', [], {'max_length': '32'})
332+ },
333+ 'webcatalog.department': {
334+ 'Meta': {'object_name': 'Department'},
335+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
336+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
337+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'})
338+ },
339+ 'webcatalog.distroseries': {
340+ 'Meta': {'object_name': 'DistroSeries'},
341+ 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
342+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
343+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'})
344+ },
345+ 'webcatalog.machine': {
346+ 'Meta': {'unique_together': "(('owner', 'uuid'),)", 'object_name': 'Machine'},
347+ 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
348+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
349+ 'logo_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56', 'blank': 'True'}),
350+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
351+ 'package_list': ('django.db.models.fields.TextField', [], {}),
352+ 'packages_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56'}),
353+ 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
354+ },
355+ 'webcatalog.reviewstatsimport': {
356+ 'Meta': {'object_name': 'ReviewStatsImport'},
357+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']", 'unique': 'True'}),
358+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
359+ 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
360+ }
361+ }
362+
363+ complete_apps = ['webcatalog']
364
365=== added file 'src/webcatalog/migrations/0008_add_oauth_tables.py'
366--- src/webcatalog/migrations/0008_add_oauth_tables.py 1970-01-01 00:00:00 +0000
367+++ src/webcatalog/migrations/0008_add_oauth_tables.py 2011-07-29 15:59:29 +0000
368@@ -0,0 +1,176 @@
369+# encoding: utf-8
370+import datetime
371+from south.db import db
372+from south.v2 import SchemaMigration
373+from django.db import models
374+
375+class Migration(SchemaMigration):
376+
377+ def forwards(self, orm):
378+
379+ # Adding model 'Consumer'
380+ db.create_table('webcatalog_consumer', (
381+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
382+ ('user', self.gf('django.db.models.fields.related.OneToOneField')(related_name='oauth_consumer', unique=True, to=orm['auth.User'])),
383+ ('key', self.gf('django.db.models.fields.CharField')(max_length=64)),
384+ ('secret', self.gf('django.db.models.fields.CharField')(default='PxDdsAQWcEFXUXmyQfsceNTJmgxcsQ', max_length=255, blank=True)),
385+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
386+ ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
387+ ))
388+ db.send_create_signal('webcatalog', ['Consumer'])
389+
390+ # Adding model 'Token'
391+ db.create_table('webcatalog_token', (
392+ ('consumer', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['webcatalog.Consumer'])),
393+ ('token', self.gf('django.db.models.fields.CharField')(default='rLuJPRLPHKDpxPqeaiytxwZctoWUdjRfbUvinaCSKrsTyZWGsV', max_length=50, primary_key=True)),
394+ ('token_secret', self.gf('django.db.models.fields.CharField')(default='UoHiMuBdlsZxAZYHrDCnJnEnBJBKrTmZXGsqlMGXFENLtXeAvq', max_length=50)),
395+ ('name', self.gf('django.db.models.fields.CharField')(max_length=255, blank=True)),
396+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
397+ ('updated_at', self.gf('django.db.models.fields.DateTimeField')(auto_now=True, blank=True)),
398+ ))
399+ db.send_create_signal('webcatalog', ['Token'])
400+
401+ # Adding model 'Nonce'
402+ db.create_table('webcatalog_nonce', (
403+ ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
404+ ('token', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['webcatalog.Token'])),
405+ ('consumer', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['webcatalog.Consumer'])),
406+ ('nonce', self.gf('django.db.models.fields.CharField')(unique=True, max_length=255)),
407+ ('created_at', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
408+ ))
409+ db.send_create_signal('webcatalog', ['Nonce'])
410+
411+
412+ def backwards(self, orm):
413+
414+ # Deleting model 'Consumer'
415+ db.delete_table('webcatalog_consumer')
416+
417+ # Deleting model 'Token'
418+ db.delete_table('webcatalog_token')
419+
420+ # Deleting model 'Nonce'
421+ db.delete_table('webcatalog_nonce')
422+
423+
424+ models = {
425+ 'auth.group': {
426+ 'Meta': {'object_name': 'Group'},
427+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
428+ 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
429+ 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
430+ },
431+ 'auth.permission': {
432+ 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
433+ 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
434+ 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
435+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
436+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
437+ },
438+ 'auth.user': {
439+ 'Meta': {'object_name': 'User'},
440+ 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
441+ 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
442+ 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
443+ 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
444+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
445+ 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
446+ 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
447+ 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
448+ 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
449+ 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
450+ 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
451+ 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
452+ 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
453+ },
454+ 'contenttypes.contenttype': {
455+ 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
456+ 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
457+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
458+ 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
459+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
460+ },
461+ 'webcatalog.application': {
462+ 'Meta': {'unique_together': "(('distroseries', 'archive_id'),)", 'object_name': 'Application'},
463+ 'app_type': ('django.db.models.fields.CharField', [], {'max_length': '32', 'blank': 'True'}),
464+ 'architectures': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
465+ 'archive_id': ('django.db.models.fields.CharField', [], {'db_index': 'True', 'max_length': '64', 'null': 'True', 'blank': 'True'}),
466+ 'categories': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
467+ 'channel': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
468+ 'comment': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
469+ 'departments': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['webcatalog.Department']", 'symmetrical': 'False', 'blank': 'True'}),
470+ 'description': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
471+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']"}),
472+ 'for_purchase': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
473+ 'icon': ('django.db.models.fields.files.ImageField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
474+ 'icon_name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
475+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
476+ 'keywords': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
477+ 'mimetype': ('django.db.models.fields.CharField', [], {'max_length': '2048', 'blank': 'True'}),
478+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
479+ 'package_name': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
480+ 'popcon': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
481+ 'price': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '7', 'decimal_places': '2', 'blank': 'True'}),
482+ 'ratings_average': ('django.db.models.fields.DecimalField', [], {'null': 'True', 'max_digits': '3', 'decimal_places': '2', 'blank': 'True'}),
483+ 'ratings_histogram': ('django.db.models.fields.CharField', [], {'max_length': '128', 'blank': 'True'}),
484+ 'ratings_total': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
485+ 'screenshot_url': ('django.db.models.fields.URLField', [], {'max_length': '200', 'blank': 'True'}),
486+ 'section': ('django.db.models.fields.CharField', [], {'max_length': '32'})
487+ },
488+ 'webcatalog.consumer': {
489+ 'Meta': {'object_name': 'Consumer'},
490+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
491+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
492+ 'key': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
493+ 'secret': ('django.db.models.fields.CharField', [], {'default': "'hFuSuGGcaWAcAgjRlcWbIuHVTutivv'", 'max_length': '255', 'blank': 'True'}),
494+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'}),
495+ 'user': ('django.db.models.fields.related.OneToOneField', [], {'related_name': "'oauth_consumer'", 'unique': 'True', 'to': "orm['auth.User']"})
496+ },
497+ 'webcatalog.department': {
498+ 'Meta': {'object_name': 'Department'},
499+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
500+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
501+ 'parent': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Department']", 'null': 'True', 'blank': 'True'})
502+ },
503+ 'webcatalog.distroseries': {
504+ 'Meta': {'object_name': 'DistroSeries'},
505+ 'code_name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '20', 'db_index': 'True'}),
506+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
507+ 'version': ('django.db.models.fields.CharField', [], {'max_length': '10', 'blank': 'True'})
508+ },
509+ 'webcatalog.machine': {
510+ 'Meta': {'unique_together': "(('owner', 'uuid'),)", 'object_name': 'Machine'},
511+ 'hostname': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
512+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
513+ 'logo_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56', 'blank': 'True'}),
514+ 'owner': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']"}),
515+ 'package_list': ('django.db.models.fields.TextField', [], {}),
516+ 'packages_checksum': ('django.db.models.fields.CharField', [], {'max_length': '56'}),
517+ 'uuid': ('django.db.models.fields.CharField', [], {'max_length': '32', 'db_index': 'True'})
518+ },
519+ 'webcatalog.nonce': {
520+ 'Meta': {'object_name': 'Nonce'},
521+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
522+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
523+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
524+ 'nonce': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'}),
525+ 'token': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Token']"})
526+ },
527+ 'webcatalog.reviewstatsimport': {
528+ 'Meta': {'object_name': 'ReviewStatsImport'},
529+ 'distroseries': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.DistroSeries']", 'unique': 'True'}),
530+ 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
531+ 'last_import': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.utcnow'})
532+ },
533+ 'webcatalog.token': {
534+ 'Meta': {'object_name': 'Token'},
535+ 'consumer': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['webcatalog.Consumer']"}),
536+ 'created_at': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
537+ 'name': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}),
538+ 'token': ('django.db.models.fields.CharField', [], {'default': "'BzXRMTLNLbCqttuBrpPirPQnsUJNctCyykYYhMruecOojcBlGf'", 'max_length': '50', 'primary_key': 'True'}),
539+ 'token_secret': ('django.db.models.fields.CharField', [], {'default': "'aYiLvMWQoXqKdlQXJSPFgRLXnQxUHpfXHdpdSLKCcfmMZRWMNw'", 'max_length': '50'}),
540+ 'updated_at': ('django.db.models.fields.DateTimeField', [], {'auto_now': 'True', 'blank': 'True'})
541+ }
542+ }
543+
544+ complete_apps = ['webcatalog']
545
546=== modified file 'src/webcatalog/models/__init__.py'
547--- src/webcatalog/models/__init__.py 2011-07-19 12:42:03 +0000
548+++ src/webcatalog/models/__init__.py 2011-07-29 15:59:29 +0000
549@@ -27,6 +27,7 @@
550 'Application',
551 'Department',
552 'ReviewStatsImport',
553+ 'Machine',
554 ]
555
556 from .oauthtoken import Token, Consumer, Nonce, DataStore
557@@ -34,5 +35,6 @@
558 Application,
559 Department,
560 DistroSeries,
561+ Machine,
562 ReviewStatsImport,
563 )
564
565=== modified file 'src/webcatalog/models/applications.py'
566--- src/webcatalog/models/applications.py 2011-07-20 15:27:06 +0000
567+++ src/webcatalog/models/applications.py 2011-07-29 15:59:29 +0000
568@@ -26,6 +26,7 @@
569 import re
570 from datetime import datetime
571
572+from django.contrib.auth.models import User
573 from django.core.urlresolvers import reverse
574 from django.db import models
575
576@@ -34,7 +35,9 @@
577 __metaclass__ = type
578 __all__ = [
579 'Application',
580+ 'Department',
581 'DistroSeries',
582+ 'Machine',
583 'ReviewStatsImport',
584 ]
585
586@@ -183,6 +186,17 @@
587 class ReviewStatsImport(models.Model):
588 distroseries = models.ForeignKey(DistroSeries, unique=True)
589 last_import = models.DateTimeField(default=datetime.utcnow)
590-
591- class Meta:
592- app_label = 'webcatalog'
593+ class Meta:
594+ app_label = 'webcatalog'
595+
596+
597+class Machine(models.Model):
598+ owner = models.ForeignKey(User, db_index=True)
599+ uuid = models.CharField(max_length=32, db_index=True)
600+ hostname = models.CharField(max_length=64)
601+ packages_checksum = models.CharField(max_length=56)
602+ package_list = models.TextField()
603+ logo_checksum = models.CharField(max_length=56, blank=True)
604+ class Meta:
605+ app_label = 'webcatalog'
606+ unique_together = ('owner', 'uuid')
607
608=== modified file 'src/webcatalog/models/oauthtoken.py'
609--- src/webcatalog/models/oauthtoken.py 2011-06-27 16:31:36 +0000
610+++ src/webcatalog/models/oauthtoken.py 2011-07-29 15:59:29 +0000
611@@ -84,7 +84,7 @@
612 return self.token
613
614 class Meta:
615- app_label = 'reviewsapp'
616+ app_label = 'webcatalog'
617
618
619 class Consumer(models.Model):
620@@ -130,7 +130,7 @@
621 return OAuthConsumer(self.key, self.secret)
622
623 class Meta:
624- app_label = 'reviewsapp'
625+ app_label = 'webcatalog'
626
627
628 class Nonce(models.Model):
629@@ -149,7 +149,7 @@
630 return consumer.nonce_set.create(token=token, nonce=nonce)
631
632 class Meta:
633- app_label = 'reviewsapp'
634+ app_label = 'webcatalog'
635
636 class DataStore(OAuthDataStore):
637
638
639=== modified file 'src/webcatalog/schema.py'
640--- src/webcatalog/schema.py 2011-07-19 15:44:09 +0000
641+++ src/webcatalog/schema.py 2011-07-29 15:59:29 +0000
642@@ -49,6 +49,9 @@
643 webcatalog.disk_apt_cache_location = StringConfigOption()
644 webcatalog.default_distro = StringConfigOption()
645 webcatalog.page_batch_size = IntConfigOption(default=20)
646+ webcatalog.preload_api_service_roots = BoolConfigOption()
647+ webcatalog.oauth_data_store = StringConfigOption(
648+ default='webcatalog.models.oauthtoken.DataStore')
649
650 google = ConfigSection()
651 google.google_analytics_id = StringConfigOption()
652
653=== modified file 'src/webcatalog/tests/__init__.py'
654--- src/webcatalog/tests/__init__.py 2011-06-30 17:18:27 +0000
655+++ src/webcatalog/tests/__init__.py 2011-07-29 15:59:29 +0000
656@@ -16,10 +16,16 @@
657 # along with this program. If not, see <http://www.gnu.org/licenses/>.
658
659 """Import various view, model and other tests for django's default runner."""
660+from .test_api import *
661 from .test_commands import *
662 from .test_department_filters import *
663 from .test_forms import *
664+from .test_handlers import *
665 from .test_models import *
666 from .test_templatetags import *
667 from .test_utilities import *
668 from .test_views import *
669+
670+# disable logging when running tests
671+import logging
672+logging.disable(logging.CRITICAL)
673
674=== modified file 'src/webcatalog/tests/factory.py'
675--- src/webcatalog/tests/factory.py 2011-07-21 13:18:39 +0000
676+++ src/webcatalog/tests/factory.py 2011-07-29 15:59:29 +0000
677@@ -26,12 +26,17 @@
678
679 from django.contrib.auth.models import User
680 from django.test import TestCase
681+from django_openid_auth.models import UserOpenID
682
683 from webcatalog.models import (
684 Application,
685+ Consumer,
686 Department,
687 DistroSeries,
688+ Machine,
689+ Token,
690 )
691+from webcatalog.utilities import full_claimed_id
692
693 __metaclass__ = type
694 __all__ = [
695@@ -74,6 +79,16 @@
696 user.is_superuser = True
697 user.save()
698
699+ # Create an openid record too.
700+ if open_id is None:
701+ open_id = full_claimed_id(self.get_unique_string(prefix='ident-'))
702+ elif open_id is False:
703+ return user
704+ useropenid = UserOpenID.objects.create(
705+ user=user, claimed_id=open_id, display_id=open_id)
706+
707+ return user
708+
709 def make_application(self, package_name=None, name=None,
710 comment=None, description=None, icon_name='', icon=None,
711 distroseries=None, arch='i686', ratings_average=None,
712@@ -112,6 +127,43 @@
713 return os.path.join(
714 os.path.dirname(__file__), 'test_data', file_name)
715
716+ def make_machine(self, owner=None, uuid=None, hostname=None,
717+ package_list=None):
718+ if owner is None:
719+ owner = self.make_user()
720+ if hostname is None:
721+ hostname = self.get_unique_string(prefix='hostname-')
722+ if uuid is None:
723+ uuid = self.get_unique_string(prefix='uuid-')
724+ if package_list is None:
725+ package_list = self.get_unique_string(prefix='package-list-')
726+ packages_checksum = self.get_unique_string(prefix='package-checksum-')
727+ logo_checksum = self.get_unique_string(prefix='logo-checksum-')
728+
729+ return Machine.objects.create(owner=owner, hostname=hostname,
730+ uuid=uuid, packages_checksum=packages_checksum,
731+ package_list=package_list, logo_checksum=logo_checksum)
732+
733+ def make_oauth_token_and_consumer(self, user=None, save=True):
734+ """Create a new set of OAuth token and consumer creds."""
735+ if user is None:
736+ user = self.make_user()
737+
738+ consumer_key = user.useropenid_set.get().claimed_id.split('/')[-1]
739+ consumer_secret = self.get_unique_string(prefix='consumer-secret-')
740+ consumer = Consumer(user=user, key=consumer_key,
741+ secret=consumer_secret)
742+ if save:
743+ consumer.save()
744+ token_string = self.get_unique_string(prefix='token-')
745+ token_secret = self.get_unique_string(prefix='token-secret-')
746+ token_name = self.get_unique_string(prefix='token-name-')
747+ token = Token(consumer=consumer, token=token_string,
748+ token_secret=token_secret, name=token_name)
749+ if save:
750+ token.save()
751+ return token, consumer
752+
753
754 class TestCaseWithFactory(TestCase):
755
756
757=== added file 'src/webcatalog/tests/test_api.py'
758--- src/webcatalog/tests/test_api.py 1970-01-01 00:00:00 +0000
759+++ src/webcatalog/tests/test_api.py 2011-07-29 15:59:29 +0000
760@@ -0,0 +1,254 @@
761+# -*- coding: utf-8 -*-
762+# This file is part of the Ubuntu Web Catalog
763+# Copyright (C) 2011 Canonical Ltd.
764+#
765+# This program is free software: you can redistribute it and/or modify
766+# it under the terms of the GNU Affero General Public License as
767+# published by the Free Software Foundation, either version 3 of the
768+# License, or (at your option) any later version.
769+#
770+# This program is distributed in the hope that it will be useful,
771+# but WITHOUT ANY WARRANTY; without even the implied warranty of
772+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
773+# GNU Affero General Public License for more details.
774+#
775+# You should have received a copy of the GNU Affero General Public License
776+# along with this program. If not, see <http://www.gnu.org/licenses/>.
777+
778+"""WebCatalog API tests."""
779+
780+from __future__ import absolute_import
781+
782+__metaclass__ = type
783+__all__ = [
784+ 'DeleteMachineTestCase',
785+ 'GetMachineTestCase',
786+ 'ListPackagesTestCase',
787+ 'ListMachinesTestCase',
788+ 'ServerStatusTestCase',
789+ 'UpdateMachineTestCase',
790+ 'UpdatePackageListTestCase',
791+ ]
792+
793+from django.utils import simplejson
794+
795+from django.test import TestCase
796+from oauth.oauth import (
797+ OAuthRequest,
798+ OAuthConsumer,
799+ OAuthToken,
800+ OAuthSignatureMethod_PLAINTEXT,
801+ )
802+
803+from .factory import TestCaseWithFactory
804+from webcatalog.models import Machine
805+
806+class ServerStatusTestCase(TestCase):
807+ def test_server_status(self):
808+ response = self.client.get('/cat/api/1.0/server-status/')
809+ self.assertEqual(response.content, '"ok"')
810+
811+class AuthenticatedAPITestCase(TestCaseWithFactory):
812+ def auth_header_for_user(self, url, user=None, realm='OAuth'):
813+ token, consumer = self.factory.make_oauth_token_and_consumer(user=user)
814+ oaconsumer = OAuthConsumer(token.consumer.key, token.consumer.secret)
815+ oatoken = OAuthToken(token.token, token.token_secret)
816+ oarequest = OAuthRequest.from_consumer_and_token(
817+ oaconsumer, oatoken, http_url=url)
818+ oarequest.sign_request(OAuthSignatureMethod_PLAINTEXT(),
819+ oaconsumer, oatoken)
820+ header = oarequest.to_header(realm)
821+ return {'HTTP_AUTHORIZATION': header['Authorization']}
822+
823+
824+class ListMachinesTestCase(AuthenticatedAPITestCase):
825+ url = '/cat/api/1.0/list-machines/'
826+ def test_no_auth_returns_401(self):
827+ response = self.client.get(self.url)
828+ self.assertEqual(401, response.status_code)
829+
830+ def test_read_no_machines(self):
831+ user = self.factory.make_user()
832+ response = self.client.get(self.url,
833+ **self.auth_header_for_user(self.url, user=user))
834+ self.assertEqual('[]', response.content)
835+
836+ def test_read_multiple_machines(self):
837+ user = self.factory.make_user()
838+ machine1 = self.factory.make_machine(owner=user)
839+ machine2 = self.factory.make_machine(owner=user)
840+ response = self.client.get(self.url,
841+ **self.auth_header_for_user(self.url, user=user))
842+ data = simplejson.loads(response.content)
843+ self.assertEqual(2, len(data))
844+ expected = set([machine1.uuid, machine2.uuid])
845+ self.assertEqual(expected, set([x['uuid'] for x in data]))
846+
847+ def test_only_returns_machines_for_the_authenticated_user(self):
848+ mymachine = self.factory.make_machine()
849+ othermachine = self.factory.make_machine()
850+ response = self.client.get(self.url,
851+ **self.auth_header_for_user(self.url, user=mymachine.owner))
852+ data = simplejson.loads(response.content)
853+
854+ self.assertEqual(1, len(data))
855+ self.assertEqual(mymachine.uuid, data[0]['uuid'])
856+
857+
858+class UpdateMachineTestCase(AuthenticatedAPITestCase):
859+ url = '/cat/api/1.0/machine/%s/'
860+
861+ def test_no_auth_returns_401(self):
862+ machine = self.factory.make_machine()
863+ response = self.client.get(self.url % machine.uuid)
864+ self.assertEqual(401, response.status_code)
865+
866+ def test_update_success(self):
867+ machine = self.factory.make_machine()
868+ data = simplejson.dumps({'hostname': machine.hostname + '-updated'})
869+ url = self.url % machine.uuid
870+
871+ response = self.client.post(url, data=data,
872+ content_type='application/json',
873+ **self.auth_header_for_user(url, user=machine.owner))
874+
875+ data = simplejson.loads(response.content)
876+ self.assertEqual(machine.hostname + '-updated', data['hostname'])
877+
878+ def test_creates_new_machine_if_not_owner(self):
879+ machine = self.factory.make_machine()
880+ otheruser = self.factory.make_user()
881+ data = simplejson.dumps({'hostname': machine.hostname + '-updated'})
882+ url = self.url % machine.uuid
883+
884+ response = self.client.post(url, data=data,
885+ content_type='application/json',
886+ **self.auth_header_for_user(url, user=otheruser))
887+
888+ data = simplejson.loads(response.content)
889+ self.assertEqual('', data['packages_checksum'])
890+
891+
892+class DeleteMachineTestCase(AuthenticatedAPITestCase):
893+ url = '/cat/api/1.0/machine/%s/'
894+
895+ def test_no_auth_returns_401(self):
896+ machine = self.factory.make_machine()
897+ response = self.client.delete(self.url % machine.uuid)
898+ self.assertEqual(401, response.status_code)
899+
900+ def test_delete_success(self):
901+ machine = self.factory.make_machine()
902+ url = self.url % machine.uuid
903+
904+ response = self.client.delete(url,
905+ **self.auth_header_for_user(url, user=machine.owner))
906+
907+ self.assertEqual(204, response.status_code)
908+ self.assertRaises(Machine.DoesNotExist, Machine.objects.get,
909+ uuid=machine.uuid, owner=machine.owner)
910+
911+ def test_delete_other_users_machine_fails(self):
912+ machine = self.factory.make_machine()
913+ otheruser = self.factory.make_user()
914+ url = self.url % machine.uuid
915+
916+ response = self.client.delete(url,
917+ **self.auth_header_for_user(url, user=otheruser))
918+
919+ self.assertEqual(404, response.status_code)
920+
921+
922+class GetMachineTestCase(AuthenticatedAPITestCase):
923+ url = '/cat/api/1.0/machine/%s/'
924+
925+ def test_no_auth_returns_401(self):
926+ machine = self.factory.make_machine()
927+ response = self.client.get(self.url % machine.uuid)
928+ self.assertEqual(401, response.status_code)
929+
930+ def test_get_success(self):
931+ machine = self.factory.make_machine()
932+ url = self.url % machine.uuid
933+
934+ response = self.client.get(url,
935+ **self.auth_header_for_user(url, user=machine.owner))
936+
937+ data = simplejson.loads(response.content)
938+ self.assertEqual(machine.hostname, data['hostname'])
939+ self.assertEqual(machine.logo_checksum, data['logo_checksum'])
940+
941+ def test_get_other_users_machine_fails(self):
942+ machine = self.factory.make_machine()
943+ otheruser = self.factory.make_user()
944+ url = self.url % machine.uuid
945+
946+ response = self.client.get(url,
947+ **self.auth_header_for_user(url, user=otheruser))
948+
949+ self.assertEqual(404, response.status_code)
950+
951+
952+class ListPackagesTestCase(AuthenticatedAPITestCase):
953+ url = '/cat/api/1.0/packages/%s/'
954+
955+ def test_no_auth_returns_401(self):
956+ machine = self.factory.make_machine()
957+ response = self.client.get(self.url % machine.uuid)
958+ self.assertEqual(401, response.status_code)
959+
960+ def test_get_success(self):
961+ expected = 'some-random-package-list'
962+ machine = self.factory.make_machine(package_list=expected)
963+ url = self.url % machine.uuid
964+
965+ response = self.client.get(url,
966+ **self.auth_header_for_user(url, user=machine.owner))
967+
968+ data = simplejson.loads(response.content)
969+ self.assertEqual(expected, data)
970+
971+ def test_get_other_users_package_list_fails(self):
972+ machine = self.factory.make_machine()
973+ otheruser = self.factory.make_user()
974+ url = self.url % machine.uuid
975+
976+ response = self.client.get(url,
977+ **self.auth_header_for_user(url, user=otheruser))
978+
979+ self.assertEqual(404, response.status_code)
980+
981+
982+class UpdatePackageListTestCase(AuthenticatedAPITestCase):
983+ url = '/cat/api/1.0/packages/%s/'
984+
985+ def test_no_auth_returns_401(self):
986+ machine = self.factory.make_machine()
987+ response = self.client.post(self.url % machine.uuid)
988+ self.assertEqual(401, response.status_code)
989+
990+ def test_post_success(self):
991+ expected = 'some-random-package-list'
992+ machine = self.factory.make_machine(package_list=expected)
993+ url = self.url % machine.uuid
994+ data = simplejson.dumps({'package_list': expected,
995+ 'packages_checksum': 'foo'})
996+
997+ response = self.client.post(url, data=data,
998+ content_type='application/json',
999+ **self.auth_header_for_user(url, user=machine.owner))
1000+
1001+ self.assertContains(response, 'Success')
1002+ updated = Machine.objects.get(uuid=machine.uuid, owner=machine.owner)
1003+ self.assertEqual(expected, updated.package_list)
1004+
1005+ def test_get_other_users_package_list_fails(self):
1006+ machine = self.factory.make_machine()
1007+ otheruser = self.factory.make_user()
1008+ url = self.url % machine.uuid
1009+
1010+ response = self.client.post(url, data='"foo"',
1011+ content_type='application/json',
1012+ **self.auth_header_for_user(url, user=otheruser))
1013+
1014+ self.assertEqual(404, response.status_code)
1015
1016=== modified file 'src/webcatalog/tests/test_handlers.py'
1017--- src/webcatalog/tests/test_handlers.py 2011-06-29 11:40:16 +0000
1018+++ src/webcatalog/tests/test_handlers.py 2011-07-29 15:59:29 +0000
1019@@ -21,17 +21,204 @@
1020
1021 __metaclass__ = type
1022 __all__ = [
1023+ 'ListMachineHandlerTestCase',
1024+ 'MachineHandlerTestCase',
1025+ 'PackagesHandlerTestCase',
1026 'ServerStatusHandlerTestCase',
1027 ]
1028
1029
1030+from django.http import HttpRequest
1031 from django.test import TestCase
1032+from django.contrib.auth.models import User
1033
1034-from reviewsapp.api.handlers import (
1035+from webcatalog.models import Machine
1036+from webcatalog.api.handlers import (
1037+ ListMachinesHandler,
1038+ MachineHandler,
1039+ PackagesHandler,
1040 ServerStatusHandler,
1041 )
1042+from webcatalog.tests.factory import TestCaseWithFactory
1043+
1044
1045 class ServerStatusHandlerTestCase(TestCase):
1046 def test_read(self):
1047 ss_handler = ServerStatusHandler()
1048 self.assertEqual('ok', ss_handler.read(None))
1049+
1050+
1051+class HandlerTestCase(TestCaseWithFactory):
1052+ def setUp(self):
1053+ super(HandlerTestCase, self).setUp()
1054+ self.user = self.factory.make_user()
1055+ self.request = HttpRequest()
1056+ self.request.user = self.user
1057+
1058+
1059+class ListMachineHandlerTestCase(HandlerTestCase):
1060+ def test_no_machines_returns_empty_list(self):
1061+ handler = ListMachinesHandler()
1062+ machines = handler.read(self.request)
1063+ self.assertEqual([], list(machines))
1064+
1065+ def test_multiple_machines_returns_as_expected(self):
1066+ machine1 = self.factory.make_machine(owner=self.user)
1067+ machine2 = self.factory.make_machine(owner=self.user)
1068+ handler = ListMachinesHandler()
1069+
1070+ machines = handler.read(self.request)
1071+
1072+ self.assertEqual(2, len(machines))
1073+ expected = set([machine1.uuid, machine2.uuid])
1074+ self.assertEqual(expected, set(x.uuid for x in machines))
1075+
1076+ def test_machine_for_user_other_than_the_first(self):
1077+ """Check that we're not returning only machiens for the first user"""
1078+ self.factory.make_user()
1079+ self.factory.make_user()
1080+ # Not the first user in the system:
1081+ user = User.objects.all()[1]
1082+ machine = self.factory.make_machine(owner=user)
1083+ handler = ListMachinesHandler()
1084+ request = HttpRequest()
1085+ request.user = user
1086+
1087+ machines = handler.read(request)
1088+
1089+ self.assertEqual(1, len(machines))
1090+ self.assertEqual(machine.uuid, machines[0].uuid)
1091+
1092+
1093+class MachineHandlerTestCase(HandlerTestCase):
1094+ def test_read_invalid_uuid_returns_404(self):
1095+ handler = MachineHandler()
1096+ response = handler.read(self.request, self.factory.get_unique_string())
1097+ self.assertEqual(404, response.status_code)
1098+
1099+ def test_read(self):
1100+ handler = MachineHandler()
1101+ machine = self.factory.make_machine(self.user)
1102+ returned = handler.read(self.request, machine.uuid)
1103+ self.assertEqual(machine.uuid, returned.uuid)
1104+
1105+ def test_create_no_data(self):
1106+ """Test the case where no data was deserialized"""
1107+ request = HttpRequest()
1108+ handler = MachineHandler()
1109+ response = handler.create(request, uuid='foo')
1110+ self.assertContains(response, "Unable to deserialize request",
1111+ status_code=400)
1112+
1113+ def test_create_missing_hostname(self):
1114+ request = HttpRequest()
1115+ request.data = {'logo_checksum': 'bar'}
1116+ handler = MachineHandler()
1117+ response = handler.create(request, 'uuid')
1118+ expected = {'status': 'error', 'errors':
1119+ {'hostname': [u'This field is required.']}}
1120+ self.assertEqual(expected, response)
1121+
1122+ def test_create_blank_logo_checksum(self):
1123+ request = HttpRequest()
1124+ data = {'hostname': 'foo', 'logo_checksum': ''}
1125+ request.data = data
1126+ request.user = self.user
1127+ handler = MachineHandler()
1128+ requested_uuid = 'uuid'
1129+
1130+ machine = handler.create(request, requested_uuid)
1131+
1132+ self.assertEqual(requested_uuid, machine.uuid)
1133+ self.assertEqual('', machine.logo_checksum)
1134+
1135+ def test_create_updates_existing(self):
1136+ machine = self.factory.make_machine(owner=self.user)
1137+ handler = MachineHandler()
1138+ request = HttpRequest()
1139+ request.user = self.user
1140+ changed_hostname = machine.hostname + '-changed'
1141+ request.data = {'hostname': changed_hostname}
1142+
1143+ handler.create(request, machine.uuid)
1144+
1145+ updated = Machine.objects.get(uuid=machine.uuid)
1146+ self.assertEqual(changed_hostname, updated.hostname)
1147+ self.assertEqual('', updated.logo_checksum)
1148+
1149+ def test_delete_invalid_uuid_returns_404(self):
1150+ handler = MachineHandler()
1151+ response = handler.delete(self.request,
1152+ self.factory.get_unique_string())
1153+ self.assertEqual(404, response.status_code)
1154+
1155+ def test_delete(self):
1156+ machine = self.factory.make_machine(owner=self.user)
1157+ handler = MachineHandler()
1158+
1159+ response = handler.delete(self.request, machine.uuid)
1160+
1161+ self.assertEqual(204, response.status_code)
1162+ self.assertEqual('', response.content)
1163+ self.assertRaises(Machine.DoesNotExist, Machine.objects.get,
1164+ uuid=machine.uuid)
1165+
1166+
1167+class PackagesHandlerTestCase(HandlerTestCase):
1168+ def test_read_invalid_uuid_returns_404(self):
1169+ handler = PackagesHandler()
1170+ response = handler.read(self.request, self.factory.get_unique_string())
1171+ self.assertEqual(404, response.status_code)
1172+
1173+ def test_read(self):
1174+ expected = 'some-package-list'
1175+ machine = self.factory.make_machine(owner=self.user,
1176+ package_list=expected)
1177+ handler = PackagesHandler()
1178+
1179+ response = handler.read(self.request, machine.uuid)
1180+ self.assertEqual(response, expected)
1181+
1182+ def test_create_invalid_uuid_returns_404(self):
1183+ handler = PackagesHandler()
1184+ response = handler.create(self.request,
1185+ self.factory.get_unique_string())
1186+ self.assertEqual(404, response.status_code)
1187+
1188+ def test_create_missing_data(self):
1189+ machine = self.factory.make_machine(owner=self.user)
1190+ request = HttpRequest()
1191+ request.user = self.user
1192+ data = {'packages_checksum': 'bar', 'package_list': 'some-data'}
1193+ handler = PackagesHandler()
1194+ for key in data:
1195+ request.data = dict((k, data[k]) for k in data if k != key)
1196+ response = handler.create(request, machine.uuid)
1197+ self.assertEqual('error', response['status'])
1198+ self.assertEqual([key], response['errors'].keys())
1199+
1200+ def test_create_success(self):
1201+ machine = self.factory.make_machine(owner=self.user)
1202+ request = HttpRequest()
1203+ request.user = self.user
1204+ data = {'packages_checksum': 'bar', 'package_list': 'some-data'}
1205+ request.data = data
1206+ handler = PackagesHandler()
1207+
1208+ response = handler.create(request, machine.uuid)
1209+
1210+ updated = Machine.objects.get(uuid=machine.uuid)
1211+ self.assertEqual('Success', response)
1212+ self.assertEqual(machine.uuid, updated.uuid)
1213+ self.assertEqual(data['packages_checksum'], updated.packages_checksum)
1214+ self.assertEqual(data['package_list'], updated.package_list)
1215+
1216+ def test_create_no_data(self):
1217+ """Test the case where no data was deserialized"""
1218+ machine = self.factory.make_machine(owner=self.user)
1219+ request = HttpRequest()
1220+ request.user = self.user
1221+ handler = PackagesHandler()
1222+ response = handler.create(request, uuid=machine.uuid)
1223+ self.assertContains(response, "Unable to deserialize request",
1224+ status_code=400)

Subscribers

People subscribed via source and target branches