A major RESTful API rewrite

Rewritten API to handle creating pins for both urls
and previously-uploaded images. Added some tests for it.
This commit is contained in:
Krzysztof Klimonda
2013-02-25 15:10:15 -08:00
parent f5ef3a0c2b
commit d462bfb83b
7 changed files with 193 additions and 56 deletions

View File

@@ -1,73 +1,86 @@
from django.conf import settings
from django_images.models import Thumbnail
from tastypie.resources import ModelResource
from tastypie import fields from tastypie import fields
from tastypie.authorization import DjangoAuthorization from tastypie.authorization import DjangoAuthorization
from tastypie.resources import ModelResource
from django_images.models import Thumbnail
from pinry.core.models import User from pinry.core.models import User
from pinry.pins.models import Image, Pin
from pinry.pins.models import Pin
class UserResource(ModelResource): class UserResource(ModelResource):
gravatar = fields.CharField() gravatar = fields.CharField(readonly=True)
def dehydrate_gravatar(self, bundle): def dehydrate_gravatar(self, bundle):
return bundle.obj.gravatar return bundle.obj.gravatar
class Meta: class Meta:
list_allowed_methods = ['get']
queryset = User.objects.all() queryset = User.objects.all()
resource_name = 'user' resource_name = 'user'
excludes = ['password', 'is_superuser', 'first_name', fields = ['username']
'last_name', 'is_active', 'is_staff', 'last_login', 'date_joined']
include_resource_uri = False include_resource_uri = False
def filter_generator_for(size):
def wrapped_func(bundle, **kwargs):
return Thumbnail.objects.get_or_create_at_size(bundle.obj.pk, size, **kwargs)
return wrapped_func
class ThumbnailResource(ModelResource):
class Meta:
list_allowed_methods = ['get']
fields = ['image', 'width', 'height']
queryset = Thumbnail.objects.all()
resource_name = 'thumbnail'
include_resource_uri = False
class ImageResource(ModelResource):
standard = fields.ToOneField(ThumbnailResource, full=True,
attribute=lambda bundle: filter_generator_for('standard')(bundle))
thumbnail = fields.ToOneField(ThumbnailResource, full=True,
attribute=lambda bundle: filter_generator_for('thumbnail')(bundle))
class Meta:
fields = ['image', 'width', 'height']
include_resource_uri = False
resource_name = 'image'
queryset = Image.objects.all()
authorization = DjangoAuthorization()
class PinResource(ModelResource): class PinResource(ModelResource):
images = fields.DictField() submitter = fields.ToOneField(UserResource, 'submitter', full=True)
image = fields.ToOneField(ImageResource, 'image', full=True)
tags = fields.ListField() tags = fields.ListField()
submitter = fields.ForeignKey(UserResource, 'submitter', full=True)
def dehydrate_images(self, bundle): def hydrate_image(self, bundle):
original = bundle.obj.image url = bundle.data.get('url', None)
images = {'original': { if url:
'url': original.get_absolute_url(), 'width': original.width, 'height': original.height} image = Image.objects.create_for_url(url)
} bundle.data['image'] = '/api/v1/image/{}/'.format(image.pk)
for image in ['standard', 'thumbnail']: return bundle
obj = Thumbnail.objects.get_or_create_at_size(original.pk, image)
images[image] = {
'url': obj.get_absolute_url(), 'width': obj.width, 'height': obj.height
}
return images
class Meta: def dehydrate_tags(self, bundle):
queryset = Pin.objects.all() return map(str, bundle.obj.tags.all())
resource_name = 'pin'
include_resource_uri = False
filtering = {
'published': ['gt'],
'submitter': ['exact']
}
fields = ['submitter', 'tags', 'published', 'description', 'url']
authorization = DjangoAuthorization()
def build_filters(self, filters=None): def build_filters(self, filters=None):
if filters is None: if filters is None:
filters = {} filters = {}
orm_filters = super(PinResource, self).build_filters(filters) orm_filters = super(PinResource, self).build_filters(filters)
if 'tag' in filters: if 'tag' in filters:
orm_filters['tags__name__in'] = filters['tag'].split(',') orm_filters['tags__name__in'] = filters['tag'].split(',')
return orm_filters return orm_filters
def dehydrate_tags(self, bundle):
return map(str, bundle.obj.tags.all())
def save_m2m(self, bundle): def save_m2m(self, bundle):
tags = bundle.data.get('tags', []) tags = bundle.data.get('tags', [])
bundle.obj.tags.set(*tags) bundle.obj.tags.set(*tags)
return super(PinResource, self).save_m2m(bundle) return super(PinResource, self).save_m2m(bundle)
class Meta:
fields = ['id', 'url', 'description']
queryset = Pin.objects.all()
resource_name = 'pin'
include_resource_uri = False
authorization = DjangoAuthorization()

View File

@@ -1,16 +1,118 @@
from django.test import TestCase
from django.test.client import Client
# pylint: disable-msg=R0904 # pylint: disable-msg=R0904
# pylint: disable-msg=E1103 # pylint: disable-msg=E1103
from django.test.client import Client
from django_images.models import Thumbnail
from taggit.models import Tag
from tastypie.test import ResourceTestCase
from ..pins.models import User, Pin, Image
def filter_generator_for(size):
def wrapped_func(obj):
return Thumbnail.objects.get_or_create_at_size(obj.pk, size)
return wrapped_func
class ImageResourceTest(ResourceTestCase):
fixtures = ['test_resources.json']
pass
class RecentPinsTest(TestCase):
def setUp(self): def setUp(self):
super(ImageResourceTest, self).setUp()
self.client = Client() self.client = Client()
self.url = '/api/pin/?format=json'
def test_status_code(self): def test_list_detail(self):
response = self.client.get(self.url) image = Image.objects.get(pk=1)
self.assertEqual(response.status_code, 200) thumbnail = filter_generator_for('thumbnail')(image)
standard = filter_generator_for('standard')(image)
response = self.api_client.get('/api/v1/image/', format='json')
self.assertDictEqual(self.deserialize(response)['objects'][0], {
u'image': image.image.url,
u'height': image.height,
u'width': image.width,
u'standard': {
u'image': unicode(standard.image.url),
u'width': standard.width,
u'height': standard.height,
},
u'thumbnail': {
u'image': unicode(thumbnail.image.url),
u'width': thumbnail.width,
u'height': thumbnail.height,
}
})
class PinResourceTest(ResourceTestCase):
fixtures = ['test_resources.json']
def setUp(self):
super(PinResourceTest, self).setUp()
self.pin_1 = Pin.objects.get(pk=1)
self.image_url = 'http://technicallyphilly.com/wp-content/uploads/2013/02/url1.jpeg'
self.user = User.objects.get(pk=1)
self.api_client.client.login(username=self.user.username, password='password')
def test_post_create_url(self):
post_data = {
'submitter': '/api/v1/user/1/',
'url': self.image_url,
'description': 'That\'s an Apple!'
}
response = self.api_client.post('/api/v1/pin/', data=post_data)
self.assertHttpCreated(response)
self.assertEqual(Pin.objects.count(), 3)
self.assertEqual(Image.objects.count(), 3)
def test_post_create_obj(self):
user = User.objects.get(pk=1)
image = Image.objects.get(pk=1)
post_data = {
'submitter': '/api/v1/user/{}/'.format(user.pk),
'image': '/api/v1/image/{}/'.format(image.pk),
'description': 'That\'s something else (probably a CC logo)!',
'tags': ['random', 'tags'],
}
response = self.api_client.post('/api/v1/pin/', data=post_data)
self.assertHttpCreated(response)
# A number of Image objects should stay the same as we are using an existing image
self.assertEqual(Image.objects.count(), 2)
self.assertEquals(Tag.objects.count(), 4)
def test_get_list_json(self):
user = User.objects.get(pk=1)
image = Image.objects.get(pk=1)
standard = filter_generator_for('standard')(image)
thumbnail = filter_generator_for('thumbnail')(image)
response = self.api_client.get('/api/v1/pin/', format='json')
self.assertValidJSONResponse(response)
self.assertDictEqual(self.deserialize(response)['objects'][0], {
u'id': self.pin_1.id,
u'submitter': {
u'username': user.username,
u'gravatar': user.gravatar
},
u'image': {
u'image': unicode(image.image.url),
u'width': image.width,
u'height': image.height,
u'standard': {
u'image': unicode(standard.image.url),
u'width': standard.width,
u'height': standard.height,
},
u'thumbnail': {
u'image': unicode(thumbnail.image.url),
u'width': thumbnail.width,
u'height': thumbnail.height,
}
},
u'url': self.pin_1.url,
u'description': self.pin_1.description,
u'tags': [u'creative-commons'],
})

View File

@@ -2,11 +2,12 @@ from django.conf.urls import patterns, include, url
from tastypie.api import Api from tastypie.api import Api
from .api import PinResource from .api import ImageResource, ThumbnailResource, PinResource, UserResource
from .api import UserResource
v1_api = Api(api_name='v1') v1_api = Api(api_name='v1')
v1_api.register(ImageResource())
v1_api.register(ThumbnailResource())
v1_api.register(PinResource()) v1_api.register(PinResource())
v1_api.register(UserResource()) v1_api.register(UserResource())

View File

@@ -1,3 +1,4 @@
from django.contrib.auth.models import Permission
from django.template.response import TemplateResponse from django.template.response import TemplateResponse
from django.http import HttpResponseRedirect from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
@@ -25,7 +26,9 @@ def register(request):
if request.method == 'POST': if request.method == 'POST':
form = UserCreationForm(request.POST) form = UserCreationForm(request.POST)
if form.is_valid(): if form.is_valid():
form.save() permissions = Permission.objects.filter(codename__in=['add_pin', 'add_image'])
user = form.save()
user.user_permissions = permissions
messages.success(request, 'Thank you for registering, you can now ' messages.success(request, 'Thank you for registering, you can now '
'login.') 'login.')
return HttpResponseRedirect(reverse('core:login')) return HttpResponseRedirect(reverse('core:login'))

File diff suppressed because one or more lines are too long

View File

@@ -1,11 +1,31 @@
from cStringIO import StringIO
import urllib2
from django.core.files.uploadedfile import InMemoryUploadedFile
from django.db import models from django.db import models
from django_images.models import Image from django_images.models import Image as BaseImage
from taggit.managers import TaggableManager from taggit.managers import TaggableManager
from ..core.models import User from ..core.models import User
class ImageManager(models.Manager):
# FIXME: Move this into an asynchronous task
def create_for_url(self, url):
file_name = url.split("/")[-1]
buf = StringIO()
buf.write(urllib2.urlopen(url).read())
obj = InMemoryUploadedFile(buf, 'image', file_name, None, buf.tell(), None)
return Image.objects.create(image=obj)
class Image(BaseImage):
objects = ImageManager()
class Meta:
proxy = True
class Pin(models.Model): class Pin(models.Model):
submitter = models.ForeignKey(User) submitter = models.ForeignKey(User)
url = models.TextField(blank=True, null=True) url = models.TextField(blank=True, null=True)
@@ -16,6 +36,3 @@ class Pin(models.Model):
def __unicode__(self): def __unicode__(self):
return self.url return self.url
class Meta:
ordering = ['-id']

View File

@@ -5,6 +5,6 @@ from django.conf import settings
urlpatterns = patterns('', urlpatterns = patterns('',
url(r'^pins/', include('pinry.pins.urls', namespace='pins')), url(r'^pins/', include('pinry.pins.urls', namespace='pins')),
url(r'', include('pinry.api.urls', namespace='api')), url(r'', include('pinry.api.urls')),
url(r'', include('pinry.core.urls', namespace='core')), url(r'', include('pinry.core.urls', namespace='core')),
) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) ) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)