Merge pull request #90 from gregmuellegger/rest-api-integration

Rest api integration
This commit is contained in:
Daniel Greenfeld 2013-05-19 05:48:51 -07:00
commit 0c1238dff6
12 changed files with 256 additions and 17 deletions

85
djadmin2/apiviews.py Normal file
View file

@ -0,0 +1,85 @@
from django.utils.encoding import force_str
from rest_framework import fields, generics, serializers
from rest_framework.response import Response
from rest_framework.reverse import reverse
from rest_framework.views import APIView
from .views import Admin2Mixin
API_VERSION = '0.1'
class Admin2APISerializer(serializers.HyperlinkedModelSerializer):
_default_view_name = 'admin2:%(app_label)s_%(model_name)s_api-detail'
pk = fields.Field(source='pk')
__str__ = fields.Field(source='__unicode__')
class Admin2APIMixin(Admin2Mixin):
def get_serializer_class(self):
if self.serializer_class is None:
model_class = self.get_model()
class ModelAPISerilizer(Admin2APISerializer):
# we need to reset this here, since we don't know anything
# about the name of the admin instance when declaring the
# Admin2APISerializer base class
_default_view_name = ':'.join((
self.modeladmin.admin.name,
'%(app_label)s_%(model_name)s_api-detail'))
class Meta:
model = model_class
return ModelAPISerilizer
return super(Admin2APIMixin, self).get_serializer_class()
class IndexAPIView(Admin2APIMixin, APIView):
apps = None
registry = None
def get_model_data(self, model):
modeladmin = self.registry[model]
opts = {
'current_app': modeladmin.admin.name,
'app_label': model._meta.app_label,
'model_name': model._meta.object_name.lower(),
}
model_url = reverse(
'%(current_app)s:%(app_label)s_%(model_name)s_api-list' % opts,
request=self.request,
format=self.kwargs.get('format'))
return {
'url': model_url,
'verbose_name': force_str(model._meta.verbose_name),
'verbose_name_plural': force_str(model._meta.verbose_name_plural),
}
def get_app_data(self, app_label, models):
model_data = []
for model in models:
model_data.append(self.get_model_data(model))
return {
'app_label': app_label,
'models': model_data,
}
def get(self, request):
app_data = []
for app_label, registry in self.apps.items():
models = registry.keys()
app_data.append(self.get_app_data(app_label, models))
index_data = {
'version': API_VERSION,
'apps': app_data,
}
return Response(index_data)
class ListCreateAPIView(Admin2APIMixin, generics.ListCreateAPIView):
pass
class RetrieveUpdateDestroyAPIView(Admin2APIMixin, generics.RetrieveUpdateDestroyAPIView):
pass

View file

@ -4,6 +4,7 @@ from django.core.exceptions import ImproperlyConfigured
from django.utils.importlib import import_module
from . import apiviews
from . import models
from . import views
@ -17,6 +18,7 @@ class Admin2(object):
It also provides an index view that serves as an entry point to the admin site.
"""
index_view = views.IndexView
api_index_view = apiviews.IndexAPIView
def __init__(self, name='admin2'):
self.registry = {}
@ -41,7 +43,7 @@ class Admin2(object):
raise ImproperlyConfigured('%s is already registered in django-admin2' % model)
if not modeladmin:
modeladmin = models.ModelAdmin2
self.registry[model] = modeladmin(model, **kwargs)
self.registry[model] = modeladmin(model, admin=self, **kwargs)
# Add the model to the apps registry
app_label = model._meta.app_label
@ -77,6 +79,7 @@ class Admin2(object):
Autodiscovers all admin2.py modules for apps in INSTALLED_APPS by
trying to import them.
"""
apps = []
for app_name in [x for x in settings.INSTALLED_APPS]:
try:
import_module("%s.admin2" % app_name)
@ -91,20 +94,32 @@ class Admin2(object):
'apps': self.apps,
}
def get_api_index_kwargs(self):
return {
'registry': self.registry,
'apps': self.apps,
}
def get_urls(self):
urlpatterns = patterns('',
url(r'^$', self.index_view.as_view(**self.get_index_kwargs()), name='dashboard'),
url(r'^api/v0/$',
self.api_index_view.as_view(**self.get_api_index_kwargs()), name='api-index'),
)
for model, modeladmin in self.registry.iteritems():
app_label = model._meta.app_label
model_name = model._meta.object_name.lower()
urlpatterns += patterns('',
url('^{}/{}/'.format(app_label, model_name),
url('^{}/{}/'.format(
model._meta.app_label,
model._meta.object_name.lower()),
include(modeladmin.urls)),
url('^api/v0/{}/{}/'.format(
model._meta.app_label,
model._meta.object_name.lower()),
include(modeladmin.api_urls)),
)
return urlpatterns
@property
def urls(self):
# We set the application and instance namespace here
return self.get_urls(), self.name, self.name

View file

@ -5,12 +5,12 @@ synonymous with the django.contrib.admin.sites model.
"""
from django.core.urlresolvers import reverse
from django.conf.urls import patterns, url
from django.contrib.auth import models as auth_app
from django.db.models import get_models, signals
from djadmin2 import apiviews
from djadmin2 import views
try:
@ -48,10 +48,10 @@ class BaseAdmin2(object):
readonly_fields = ()
ordering = None
def __init__(self, model):
def __init__(self, model, admin):
super(BaseAdmin2, self).__init__()
self.model = model
self.admin = admin
def _user_has_permission(self, user, permission_type, obj=None):
""" Generic method for checking whether the user has permission of specified type for the model.
@ -108,8 +108,13 @@ class ModelAdmin2(BaseAdmin2):
detail_view = views.ModelDetailView
delete_view = views.ModelDeleteView
def __init__(self, model, **kwargs):
# API Views
api_list_view = apiviews.ListCreateAPIView
api_detail_view = apiviews.RetrieveUpdateDestroyAPIView
def __init__(self, model, admin, **kwargs):
self.model = model
self.admin = admin
self.app_label = model._meta.app_label
self.model_name = model._meta.object_name.lower()
@ -152,9 +157,25 @@ class ModelAdmin2(BaseAdmin2):
def get_delete_kwargs(self):
return self.get_default_view_kwargs()
def get_api_index_kwargs(self):
return self.get_default_view_kwargs()
def get_api_detail_kwargs(self):
return self.get_default_view_kwargs()
def get_index_url(self):
return reverse('admin2:{}'.format(self.get_prefixed_view_name('index')))
def get_api_list_kwargs(self):
kwargs = self.get_default_view_kwargs()
kwargs.update({
'paginate_by': self.list_per_page,
})
return kwargs
def get_api_detail_kwargs(self):
return self.get_default_view_kwargs()
def get_urls(self):
return patterns('',
url(
@ -184,11 +205,29 @@ class ModelAdmin2(BaseAdmin2):
),
)
def get_api_urls(self):
return patterns('',
url(
regex=r'^$',
view=self.api_list_view.as_view(**self.get_api_list_kwargs()),
name=self.get_prefixed_view_name('api-list'),
),
url(
regex=r'^(?P<pk>[0-9]+)/$',
view=self.api_detail_view.as_view(**self.get_api_detail_kwargs()),
name=self.get_prefixed_view_name('api-detail'),
),
)
@property
def urls(self):
# We set the application and instance namespace here
return self.get_urls(), None, None
@property
def api_urls(self):
return self.get_api_urls(), None, None
def create_extra_permissions(app, created_models, verbosity, **kwargs):
"""
Creates 'view' permissions for all models.

View file

@ -16,6 +16,7 @@
<a class="brand" href="{% url 'admin2:dashboard' %}">Django-Admin2</a>
<ul class="nav pull-right">
<li><a href="{% url 'admin2:api-index' %}">API</a></li>
<li><a href="TODO">Log out</a></li>
</ul>
</div>
@ -31,4 +32,4 @@
{% block extrajs %}{% endblock %}
</body>
</html>
</html>

View file

@ -1,7 +1,6 @@
import unittest
from django.db import models
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from ..models import ModelAdmin2
from ..core import Admin2
@ -11,7 +10,7 @@ class Thing(models.Model):
pass
class Admin2Test(unittest.TestCase):
class Admin2Test(TestCase):
def setUp(self):
self.admin2 = Admin2()
@ -33,4 +32,4 @@ class Admin2Test(unittest.TestCase):
def test_get_urls(self):
self.admin2.register(Thing)
self.assertEquals(2, len(self.admin2.get_urls()))
self.assertEquals(4, len(self.admin2.get_urls()))

16
docs/api.rst Normal file
View file

@ -0,0 +1,16 @@
API
===
**django-admin2** comes with a builtin REST-API for accessing all the
resources you can get from the frontend via JSON.
The API can be found at the URL you choose for the admin2 and then append
``api/v0/``.
If the API has changed in a backwards-incompatible way we will increase the
API version to the next number. So you can be sure that you're frontend code
should keep working even between updates to more recent django-admin2
versions.
However currently we are still in heavy development, so we are using ``v0``
for the API, which means is subject to change and being broken at any time.

View file

@ -61,6 +61,7 @@ Content
:maxdepth: 2
contributing
api
design
meta
@ -71,4 +72,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

View file

@ -2,7 +2,7 @@
# Import your custom models
from .models import Post, Comment
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.contrib.auth.models import User
from django.contrib.auth.models import Group, Permission, User
import djadmin2
@ -18,3 +18,5 @@ class UserAdmin2(ModelAdmin2):
djadmin2.default.register(Post)
djadmin2.default.register(Comment)
djadmin2.default.register(User, UserAdmin2)
djadmin2.default.register(Permission)
djadmin2.default.register(Group)

View file

@ -0,0 +1,2 @@
from test_views import *
from test_apiviews import *

View file

@ -0,0 +1,79 @@
from django.test import TestCase
from django.test.client import RequestFactory
from django.core.urlresolvers import reverse
from django.utils import simplejson as json
from djadmin2 import apiviews
from djadmin2 import default
from djadmin2.models import ModelAdmin2
from ..models import Post
class ViewTest(TestCase):
def setUp(self):
self.factory = RequestFactory()
def get_model_admin(self, model):
return ModelAdmin2(model, default)
class IndexAPIViewTest(ViewTest):
def test_response_ok(self):
request = self.factory.get(reverse('admin2:api-index'))
view = apiviews.IndexAPIView.as_view(**default.get_api_index_kwargs())
response = view(request)
self.assertEqual(response.status_code, 200)
class ListCreateAPIViewTest(ViewTest):
def test_response_ok(self):
request = self.factory.get(reverse('admin2:blog_post_api-list'))
modeladmin = self.get_model_admin(Post)
view = apiviews.ListCreateAPIView.as_view(
**modeladmin.get_api_list_kwargs())
response = view(request)
self.assertEqual(response.status_code, 200)
def test_list_includes_unicode_field(self):
Post.objects.create(title='Foo', body='Bar')
request = self.factory.get(reverse('admin2:blog_post_api-list'))
modeladmin = self.get_model_admin(Post)
view = apiviews.ListCreateAPIView.as_view(
**modeladmin.get_api_list_kwargs())
response = view(request)
response.render()
self.assertIn('"__str__": "Foo"', response.content)
def test_pagination(self):
request = self.factory.get(reverse('admin2:blog_post_api-list'))
modeladmin = self.get_model_admin(Post)
view = apiviews.ListCreateAPIView.as_view(
**modeladmin.get_api_list_kwargs())
response = view(request)
response.render()
data = json.loads(response.content)
self.assertEqual(data['count'], 0)
# next and previous fields exist, but are null because we have no
# content
self.assertTrue('next' in data)
self.assertEqual(data['next'], None)
self.assertTrue('previous' in data)
self.assertEqual(data['previous'], None)
class RetrieveUpdateDestroyAPIViewTest(ViewTest):
def test_response_ok(self):
post = Post.objects.create(title='Foo', body='Bar')
request = self.factory.get(
reverse('admin2:blog_post_api-detail',
kwargs={'pk': post.pk}))
modeladmin = self.get_model_admin(Post)
view = apiviews.RetrieveUpdateDestroyAPIView.as_view(
**modeladmin.get_api_detail_kwargs())
response = view(request, pk=post.pk)
self.assertEqual(response.status_code, 200)

View file

@ -2,7 +2,7 @@ from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.test import TestCase, Client
from .models import Post
from ..models import Post
class BaseIntegrationTest(TestCase):

View file

@ -121,6 +121,7 @@ INSTALLED_APPS = (
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'django_coverage',
'rest_framework',
'djadmin2',
'blog',
)