mirror of
https://github.com/jazzband/django-admin2.git
synced 2026-03-16 22:20:24 +00:00
Merge pull request #90 from gregmuellegger/rest-api-integration
Rest api integration
This commit is contained in:
commit
0c1238dff6
12 changed files with 256 additions and 17 deletions
85
djadmin2/apiviews.py
Normal file
85
djadmin2/apiviews.py
Normal 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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
16
docs/api.rst
Normal 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.
|
||||
|
|
@ -61,6 +61,7 @@ Content
|
|||
:maxdepth: 2
|
||||
|
||||
contributing
|
||||
api
|
||||
design
|
||||
meta
|
||||
|
||||
|
|
@ -71,4 +72,3 @@ Indices and tables
|
|||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
2
example/blog/tests/__init__.py
Normal file
2
example/blog/tests/__init__.py
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
from test_views import *
|
||||
from test_apiviews import *
|
||||
79
example/blog/tests/test_apiviews.py
Normal file
79
example/blog/tests/test_apiviews.py
Normal 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)
|
||||
|
|
@ -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):
|
||||
|
|
@ -121,6 +121,7 @@ INSTALLED_APPS = (
|
|||
# Uncomment the next line to enable admin documentation:
|
||||
# 'django.contrib.admindocs',
|
||||
'django_coverage',
|
||||
'rest_framework',
|
||||
'djadmin2',
|
||||
'blog',
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue