diff --git a/pinry/api/api.py b/pinry/api/api.py index be92e8d..5fb9f92 100644 --- a/pinry/api/api.py +++ b/pinry/api/api.py @@ -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.authorization import DjangoAuthorization +from tastypie.resources import ModelResource +from django_images.models import Thumbnail from pinry.core.models import User - -from pinry.pins.models import Pin +from pinry.pins.models import Image, Pin class UserResource(ModelResource): - gravatar = fields.CharField() + gravatar = fields.CharField(readonly=True) def dehydrate_gravatar(self, bundle): return bundle.obj.gravatar class Meta: + list_allowed_methods = ['get'] queryset = User.objects.all() resource_name = 'user' - excludes = ['password', 'is_superuser', 'first_name', - 'last_name', 'is_active', 'is_staff', 'last_login', 'date_joined'] + fields = ['username'] 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): - images = fields.DictField() + submitter = fields.ToOneField(UserResource, 'submitter', full=True) + image = fields.ToOneField(ImageResource, 'image', full=True) tags = fields.ListField() - submitter = fields.ForeignKey(UserResource, 'submitter', full=True) - def dehydrate_images(self, bundle): - original = bundle.obj.image - images = {'original': { - 'url': original.get_absolute_url(), 'width': original.width, 'height': original.height} - } - for image in ['standard', 'thumbnail']: - 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 + def hydrate_image(self, bundle): + url = bundle.data.get('url', None) + if url: + image = Image.objects.create_for_url(url) + bundle.data['image'] = '/api/v1/image/{}/'.format(image.pk) + return bundle - class Meta: - queryset = Pin.objects.all() - resource_name = 'pin' - include_resource_uri = False - filtering = { - 'published': ['gt'], - 'submitter': ['exact'] - } - fields = ['submitter', 'tags', 'published', 'description', 'url'] - authorization = DjangoAuthorization() + def dehydrate_tags(self, bundle): + return map(str, bundle.obj.tags.all()) def build_filters(self, filters=None): if filters is None: filters = {} - orm_filters = super(PinResource, self).build_filters(filters) - if 'tag' in filters: orm_filters['tags__name__in'] = filters['tag'].split(',') - return orm_filters - def dehydrate_tags(self, bundle): - return map(str, bundle.obj.tags.all()) - def save_m2m(self, bundle): tags = bundle.data.get('tags', []) bundle.obj.tags.set(*tags) 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() diff --git a/pinry/api/tests.py b/pinry/api/tests.py index 4a7417d..29cd6cc 100644 --- a/pinry/api/tests.py +++ b/pinry/api/tests.py @@ -1,16 +1,118 @@ -from django.test import TestCase -from django.test.client import Client - - # pylint: disable-msg=R0904 # 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): + super(ImageResourceTest, self).setUp() self.client = Client() - self.url = '/api/pin/?format=json' - def test_status_code(self): - response = self.client.get(self.url) - self.assertEqual(response.status_code, 200) + def test_list_detail(self): + image = Image.objects.get(pk=1) + 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'], + }) diff --git a/pinry/api/urls.py b/pinry/api/urls.py index cdfb890..39d4bd9 100644 --- a/pinry/api/urls.py +++ b/pinry/api/urls.py @@ -2,11 +2,12 @@ from django.conf.urls import patterns, include, url from tastypie.api import Api -from .api import PinResource -from .api import UserResource +from .api import ImageResource, ThumbnailResource, PinResource, UserResource v1_api = Api(api_name='v1') +v1_api.register(ImageResource()) +v1_api.register(ThumbnailResource()) v1_api.register(PinResource()) v1_api.register(UserResource()) diff --git a/pinry/core/views.py b/pinry/core/views.py index b1c6e1b..86f7e3c 100644 --- a/pinry/core/views.py +++ b/pinry/core/views.py @@ -1,3 +1,4 @@ +from django.contrib.auth.models import Permission from django.template.response import TemplateResponse from django.http import HttpResponseRedirect from django.core.urlresolvers import reverse @@ -25,7 +26,9 @@ def register(request): if request.method == 'POST': form = UserCreationForm(request.POST) 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 ' 'login.') return HttpResponseRedirect(reverse('core:login')) diff --git a/pinry/pins/fixtures/test_resources.json b/pinry/pins/fixtures/test_resources.json new file mode 100644 index 0000000..b707831 --- /dev/null +++ b/pinry/pins/fixtures/test_resources.json @@ -0,0 +1 @@ +[{"pk": 1, "model": "auth.permission", "fields": {"codename": "add_permission", "name": "Can add permission", "content_type": 1}}, {"pk": 2, "model": "auth.permission", "fields": {"codename": "change_permission", "name": "Can change permission", "content_type": 1}}, {"pk": 3, "model": "auth.permission", "fields": {"codename": "delete_permission", "name": "Can delete permission", "content_type": 1}}, {"pk": 4, "model": "auth.permission", "fields": {"codename": "add_group", "name": "Can add group", "content_type": 2}}, {"pk": 5, "model": "auth.permission", "fields": {"codename": "change_group", "name": "Can change group", "content_type": 2}}, {"pk": 6, "model": "auth.permission", "fields": {"codename": "delete_group", "name": "Can delete group", "content_type": 2}}, {"pk": 7, "model": "auth.permission", "fields": {"codename": "add_user", "name": "Can add user", "content_type": 3}}, {"pk": 8, "model": "auth.permission", "fields": {"codename": "change_user", "name": "Can change user", "content_type": 3}}, {"pk": 9, "model": "auth.permission", "fields": {"codename": "delete_user", "name": "Can delete user", "content_type": 3}}, {"pk": 10, "model": "auth.permission", "fields": {"codename": "add_contenttype", "name": "Can add content type", "content_type": 4}}, {"pk": 11, "model": "auth.permission", "fields": {"codename": "change_contenttype", "name": "Can change content type", "content_type": 4}}, {"pk": 12, "model": "auth.permission", "fields": {"codename": "delete_contenttype", "name": "Can delete content type", "content_type": 4}}, {"pk": 13, "model": "auth.permission", "fields": {"codename": "add_session", "name": "Can add session", "content_type": 5}}, {"pk": 14, "model": "auth.permission", "fields": {"codename": "change_session", "name": "Can change session", "content_type": 5}}, {"pk": 15, "model": "auth.permission", "fields": {"codename": "delete_session", "name": "Can delete session", "content_type": 5}}, {"pk": 16, "model": "auth.permission", "fields": {"codename": "add_migrationhistory", "name": "Can add migration history", "content_type": 6}}, {"pk": 17, "model": "auth.permission", "fields": {"codename": "change_migrationhistory", "name": "Can change migration history", "content_type": 6}}, {"pk": 18, "model": "auth.permission", "fields": {"codename": "delete_migrationhistory", "name": "Can delete migration history", "content_type": 6}}, {"pk": 19, "model": "auth.permission", "fields": {"codename": "add_tag", "name": "Can add Tag", "content_type": 7}}, {"pk": 20, "model": "auth.permission", "fields": {"codename": "change_tag", "name": "Can change Tag", "content_type": 7}}, {"pk": 21, "model": "auth.permission", "fields": {"codename": "delete_tag", "name": "Can delete Tag", "content_type": 7}}, {"pk": 22, "model": "auth.permission", "fields": {"codename": "add_taggeditem", "name": "Can add Tagged Item", "content_type": 8}}, {"pk": 23, "model": "auth.permission", "fields": {"codename": "change_taggeditem", "name": "Can change Tagged Item", "content_type": 8}}, {"pk": 24, "model": "auth.permission", "fields": {"codename": "delete_taggeditem", "name": "Can delete Tagged Item", "content_type": 8}}, {"pk": 25, "model": "auth.permission", "fields": {"codename": "add_image", "name": "Can add image", "content_type": 9}}, {"pk": 26, "model": "auth.permission", "fields": {"codename": "change_image", "name": "Can change image", "content_type": 9}}, {"pk": 27, "model": "auth.permission", "fields": {"codename": "delete_image", "name": "Can delete image", "content_type": 9}}, {"pk": 28, "model": "auth.permission", "fields": {"codename": "add_thumbnail", "name": "Can add thumbnail", "content_type": 10}}, {"pk": 29, "model": "auth.permission", "fields": {"codename": "change_thumbnail", "name": "Can change thumbnail", "content_type": 10}}, {"pk": 30, "model": "auth.permission", "fields": {"codename": "delete_thumbnail", "name": "Can delete thumbnail", "content_type": 10}}, {"pk": 31, "model": "auth.permission", "fields": {"codename": "add_pin", "name": "Can add pin", "content_type": 12}}, {"pk": 32, "model": "auth.permission", "fields": {"codename": "change_pin", "name": "Can change pin", "content_type": 12}}, {"pk": 33, "model": "auth.permission", "fields": {"codename": "delete_pin", "name": "Can delete pin", "content_type": 12}}, {"pk": 1, "model": "auth.user", "fields": {"username": "jdoe", "first_name": "", "last_name": "", "is_active": true, "is_superuser": false, "is_staff": false, "last_login": "2013-02-25T18:07:05.308Z", "groups": [], "user_permissions": [31], "password": "pbkdf2_sha256$10000$fWo1asHpXTeD$zDtszDL5fU2JZQd2goXayWCWl/EZbL8CZ+COg4Xqi04=", "email": "jdoe@example.com", "date_joined": "2013-02-25T18:07:05.308Z"}}, {"pk": 1, "model": "django_images.image", "fields": {"width": 300, "image": "image/original/by-md5/f/2/f2ed405126da6815fdd737eb6bd2a15b/cc_white.png", "height": 300}}, {"pk": 2, "model": "django_images.image", "fields": {"width": 500, "image": "image/original/by-md5/0/6/06cb8b191139d74a5c82f48bfa1ddf22/tumblr_lgabq4NUsR1qb1aw7o1_500.jpg", "height": 500}}, {"pk": 1, "model": "pins.pin", "fields": {"submitter": 1, "url": "https://si0.twimg.com/profile_images/70370524/cc_white.png", "image": 1, "description": "Creative Commons Logo", "published": "2013-02-25T18:08:31.308Z"}}, {"pk": 2, "model": "pins.pin", "fields": {"submitter": 1, "url": "https://si0.twimg.com/profile_images/70370524/cc_white.png", "image": 2, "description": "Creative Commons logo downloaded from DeviantArt", "published": "2013-02-25T18:08:54.612Z"}}, {"pk": 1, "model": "taggit.tag", "fields": {"name": "creative-commons", "slug": "creative-commons"}}, {"pk": 2, "model": "taggit.tag", "fields": {"name": "deviantart", "slug": "deviantart"}}, {"pk": 1, "model": "taggit.taggeditem", "fields": {"tag": 1, "content_type": 12, "object_id": 1}}, {"pk": 2, "model": "taggit.taggeditem", "fields": {"tag": 1, "content_type": 12, "object_id": 2}}, {"pk": 3, "model": "taggit.taggeditem", "fields": {"tag": 2, "content_type": 12, "object_id": 2}}] \ No newline at end of file diff --git a/pinry/pins/models.py b/pinry/pins/models.py index 142716d..7891271 100644 --- a/pinry/pins/models.py +++ b/pinry/pins/models.py @@ -1,11 +1,31 @@ +from cStringIO import StringIO +import urllib2 +from django.core.files.uploadedfile import InMemoryUploadedFile 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 ..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): submitter = models.ForeignKey(User) url = models.TextField(blank=True, null=True) @@ -16,6 +36,3 @@ class Pin(models.Model): def __unicode__(self): return self.url - - class Meta: - ordering = ['-id'] diff --git a/pinry/urls.py b/pinry/urls.py index 9597811..68fea12 100644 --- a/pinry/urls.py +++ b/pinry/urls.py @@ -5,6 +5,6 @@ from django.conf import settings urlpatterns = patterns('', 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')), ) + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)