Skip to content
This repository has been archived by the owner on Jan 18, 2020. It is now read-only.

Commit

Permalink
Add optional support for object set resources and endpoints
Browse files Browse the repository at this point in the history
This uses a settings-based approach to defining which object set models
to define resources and endpoints for. In addition to the functionality
provided by django-objectset directly, resources support creating sets
based on a DataContext literal, an existing instance or the user's
current session.

Requires django-objectset 0.2.3+
  • Loading branch information
bruth committed Jan 17, 2014
1 parent 6b7989c commit 0c1be38
Show file tree
Hide file tree
Showing 13 changed files with 255 additions and 27 deletions.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ git+git://github.com/cbmi/avocado.git@2.2
django-preserialize>=1.0.4,<1.1.0
restlib2>=0.3.9,<0.4
python-memcached>=1.48
django-objectset>=0.2.3
21 changes: 20 additions & 1 deletion serrano/conf/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,28 @@ def setup(self):
return self.test_setup() is not False


class Objectset(Dependency):
"""django-objectset provides a set-like abstract model for Django and
makes it trivial to creates sets of objects using common set operations.
Install by doing `pip install django-objectset`. Define models that
subclass `objectset.models.ObjectSet`.
"""

name = 'objectset'

def test_install(self):
try:
import objectset # noqa
except ImportError:
return False


# Keep track of the officially supported apps and libraries used for various
# features.
OPTIONAL_DEPS = {}
OPTIONAL_DEPS = {
'objectset': Objectset(),
}


def dep_supported(lib):
Expand Down
12 changes: 10 additions & 2 deletions serrano/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from django.core.urlresolvers import reverse
from django.contrib.auth import authenticate, login
import serrano
from serrano.conf import dep_supported
from serrano.tokens import token_generator
from .base import BaseResource

Expand All @@ -20,7 +21,7 @@ def __call__(self, request, *args, **kwargs):
def get(self, request):
uri = request.build_absolute_uri

return {
data = {
'title': 'Serrano Hypermedia API',
'version': API_VERSION,
'_links': {
Expand Down Expand Up @@ -53,10 +54,17 @@ def get(self, request):
},
'exporter': {
'href': uri(reverse('serrano:data:exporter')),
}
},
}
}

if dep_supported('objectset'):
data['_links']['sets'] = {
'href': uri(reverse('serrano:sets:root')),
}

return data

def post(self, request):
username = request.data.get('username')
password = request.data.get('password')
Expand Down
128 changes: 128 additions & 0 deletions serrano/resources/sets.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from django import forms
from django.db.models import get_model
from django.conf.urls import url, patterns
from django.core.urlresolvers import reverse
from django.core.exceptions import ImproperlyConfigured
from objectset import resources
from objectset.models import ObjectSet
from objectset.forms import objectset_form_factory
from serrano.conf import settings
from .base import BaseResource


URL_REVERSE_NAME = 'serrano:sets:{0}'


def configure_object_set(config):
model_label = config['model']
app_name, model_name = model_label.split('.', 1)
model_name = model_name.lower()

model = get_model(app_name, model_name)

if not issubclass(model, ObjectSet):
raise ImproperlyConfigured('Only models that subclass ObjectSet '
'are supported, not {0}'
.format(model_label))

default_name = unicode(model._meta.verbose_name_plural)
name = config.get('name', default_name.lower().replace(' ', ''))
label = config.get('label', default_name.title())

options = {
'label': label,
}

url_names = {
'sets': model_name,
'set': model_name,
'objects': '{0}-objects'.format(model_name),
}

url_reverse_names = {
'sets': URL_REVERSE_NAME.format(model_name),
'set': URL_REVERSE_NAME.format(model_name),
'objects': URL_REVERSE_NAME.format('{0}-objects'.format(model_name)),
}

class ObjectSetForm(objectset_form_factory(model)):
context = forms.Field(required=False)

def __init__(self, *args, **kwargs):
super(ObjectSetForm, self).__init__(*args, **kwargs)

def clean_context(self):
self._context_applied = False
# Extract is from the request data. See
# ``serrano.resources.base.get_request_context`` for parsing
# details
context = self.resource.get_context(self.request)
return context

def save(self, commit=True):
instance = super(ObjectSetForm, self).save(commit=False)

# Prevents reapplying the context to the pending objects
if not self._context_applied:
self._context_applied = True
context = self.cleaned_data.get('context')
if context:
instance._pending |= context.apply()

if commit:
instance.save()
self.save_m2m()

return instance

bases = (resources.BaseSetResource, BaseResource)

BaseSetResource = type('BaseSetResource', bases, {
'model': model,
'form_class': ObjectSetForm,
'url_names': url_names,
'url_reverse_names': url_reverse_names,
'user_support': config.get('user_support'),
'session_support': config.get('session_support'),
})

options['url_reverse_names'] = url_reverse_names
options['url_patterns'] = resources.get_url_patterns(model, {
'base': BaseSetResource
}, prefix=name)

return options


urlpatterns = patterns('')
object_set_options = []

for config in settings.OBJECT_SETS:
options = configure_object_set(config)
object_set_options.append(options)
urlpatterns += options['url_patterns']


class SetsRootResource(BaseResource):
object_set_options = tuple(object_set_options)

def get(self, request):
uri = request.build_absolute_uri
data = []

for options in self.object_set_options:
reverses = options['url_reverse_names']

data.append({
'label': options['label'],
'_links': {
'self': uri(reverse(reverses['sets'])),
}
})

return data


sets_root_resource = SetsRootResource()

urlpatterns += patterns('', url(r'^$', sets_root_resource, name='root'))
62 changes: 40 additions & 22 deletions serrano/urls.py
Original file line number Diff line number Diff line change
@@ -1,35 +1,53 @@
from django.conf.urls import patterns, url, include
from serrano.conf import dep_supported


urlpatterns = patterns(
# Patterns for the data namespace
data_patterns = patterns(
'',

url(r'^export/', include('serrano.resources.exporter')),

url(r'^preview/', include('serrano.resources.preview')),
)

# Patterns for the serrano namespace
serrano_patterns = patterns(
'',
url(r'', include(patterns('',
url(r'^$',
include('serrano.resources')),

url(r'^categories/',
include('serrano.resources.category')),
url(r'^$',
include('serrano.resources')),

url(r'^fields/',
include('serrano.resources.field')),
url(r'^categories/',
include('serrano.resources.category')),

url(r'^concepts/',
include('serrano.resources.concept')),
url(r'^fields/',
include('serrano.resources.field')),

url(r'^contexts/',
include('serrano.resources.context', namespace='contexts')),
url(r'^concepts/',
include('serrano.resources.concept')),

url(r'^queries/',
include('serrano.resources.query', namespace='queries')),
url(r'^contexts/',
include('serrano.resources.context', namespace='contexts')),

url(r'^views/',
include('serrano.resources.view', namespace='views')),
url(r'^queries/',
include('serrano.resources.query', namespace='queries')),

url(r'^data/', include(patterns(
'',
url(r'^export/', include('serrano.resources.exporter')),
url(r'^preview/', include('serrano.resources.preview')),
), namespace='data')),
url(r'^views/',
include('serrano.resources.view', namespace='views')),

), namespace='serrano')),
url(r'^data/', include(data_patterns, namespace='data')),
)

if dep_supported('objectset'):
# Patterns for the 'sets' namespace
serrano_patterns += patterns(
'',
url(r'^sets/', include('serrano.resources.sets', namespace='sets'))
)

# Exported patterns
urlpatterns = patterns(
'',
url(r'^', include(serrano_patterns, namespace='serrano'))
)
3 changes: 2 additions & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
'avocado[permissions,search,extras]>=2.2,<2.3'
'coverage',
'whoosh',
'python-memcached>=1.48'
'python-memcached>=1.48',
'django-objectset>=0.2.3',
],

'test_suite': 'test_suite',
Expand Down
1 change: 1 addition & 0 deletions tests/cases/resources/tests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def test_get(self):
'self': {'href': 'http://testserver/api/'},
'concepts': {'href': 'http://testserver/api/concepts/'},
'preview': {'href': 'http://testserver/api/data/preview/'},
'sets': {'href': 'http://testserver/api/sets/'},
},
})

Expand Down
2 changes: 1 addition & 1 deletion tests/cases/resources/tests/field.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ def test_get_privileged(self):

response = self.client.get('/api/fields/',
HTTP_ACCEPT='application/json')
self.assertEqual(len(json.loads(response.content)), 11)
self.assertEqual(len(json.loads(response.content)), 12)

response = self.client.get('/api/fields/1/',
HTTP_ACCEPT='application/json')
Expand Down
Empty file added tests/cases/sets/__init__.py
Empty file.
Empty file added tests/cases/sets/models.py
Empty file.
45 changes: 45 additions & 0 deletions tests/cases/sets/tests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import json
from django.test import TestCase
from django.core import management
from ...models import Team, Employee


class SetResourcesTest(TestCase):
fixtures = ['test_data.json']

def setUp(self):
management.call_command('avocado', 'init', 'tests', quiet=True)

def test_root(self):
response = self.client.get('/api/sets/',
HTTP_ACCEPT='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(json.loads(response.content)), 1)

def test_type(self):
response = self.client.get('/api/sets/teams/',
HTTP_ACCEPT='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(json.loads(response.content)), 0)

def test_type_instance(self):
Team(Employee.objects.all(), save=True)
response = self.client.get('/api/sets/teams/',
HTTP_ACCEPT='application/json')
self.assertEqual(response.status_code, 200)
self.assertEqual(len(json.loads(response.content)), 1)

def test_context(self):
response = self.client.post('/api/sets/teams/', json.dumps({
'context': {
'field': 'tests.title.salary',
'operator': 'gt',
'value': 15000,
}
}), content_type='application/json',
HTTP_ACCEPT='application/json')

self.assertEqual(Employee.objects.filter(title__salary__gt=15000)
.count(), 2)
self.assertEqual(response.status_code, 200)
self.assertEqual(json.loads(response.content)['count'], 2)
6 changes: 6 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import logging
from django.db import models
from objectset.models import ObjectSet


class MockHandler(logging.Handler):
def __init__(self, *args, **kwargs):
Expand Down Expand Up @@ -49,3 +51,7 @@ class Project(models.Model):
employees = models.ManyToManyField(Employee)
manager = models.OneToOneField(Employee, related_name='managed_projects')
due_date = models.DateField(null=True)


class Team(ObjectSet):
employees = models.ManyToManyField(Employee)
1 change: 1 addition & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
'tests.cases.base',
'tests.cases.resources',
'tests.cases.forms',
'tests.cases.sets',
)

EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
Expand Down

0 comments on commit 0c1be38

Please sign in to comment.