Merge remote-tracking branch 'origin/master' into inline-formsets

Conflicts:
	djadmin2/templates/admin2/bootstrap/model_edit_form.html
This commit is contained in:
Andrew Ingram 2013-05-19 19:17:51 +01:00
commit c625a80eaa
29 changed files with 547 additions and 110 deletions

View file

@ -1,4 +1,4 @@
include README.rst
include LICENSE
include CONTRIBUTORS.txt
include AUTHORS.rst
include MANIFEST.in

View file

@ -6,14 +6,28 @@ django-admin2
:alt: Build Status
:target: https://travis-ci.org/pydanny/django-admin2
One of the most useful parts of ``django.contrib.admin`` is the ability to configure various views that touch and alter data. django-admin2 is a complete rewrite of that library using modern Class-Based Views and enjoying a design focused on extendibility. By starting over, we can avoid the legacy code and make it easier to write extensions and themes.
**Warning:** This project is currently in an **alpha** state and currently not meant for real projects.
**Note:** This is pre-alpha and currently non-functional. We'll try and have a rough working prototype by the end of May 18th, 2013.
One of the most useful parts of ``django.contrib.admin`` is the ability to configure various views that touch and alter data. django-admin2 is a complete rewrite of that library using modern Class-Based Views and enjoying a design focused on extendibility and adaptability. By starting over, we can avoid the legacy code and make it easier to write extensions and themes.
Contributing
=============
Features (current)
====================
Yes please! Please read our formal contributing document at: https://github.com/pydanny/django-admin2/blob/master/docs/contributing.rst
* Easy-to-extend API that follows similar patterns to ``django.contrib.admin``.
* Built-in RESTFUL API powered by ``django-rest-framework``.
* Default theme built on Twitter Bootstrap that is just starting to act like the current Django admin.
* Easy to implement theme system.
* Basic permission controls.
Features (Planned)
====================
* Much improved documentation including tutorials and reference guides.
* Extending existing views
* Interacting with the basic Admin2 object.
* Improved permission controls
Requirements
=============
@ -22,8 +36,14 @@ Requirements
* Python 2.7+ (Python 3.3+ support is pending)
* django-braces
* django-rest-framework
* django-floppyforms
* Sphinx (for documentation)
Contributing
=============
Yes please! Please read our formal contributing document at: https://github.com/pydanny/django-admin2/blob/master/docs/contributing.rst
Basic Pattern
==============

View file

@ -1,6 +1,6 @@
__version__ = '0.2.0'
__author__ = 'Daniel Greenfeld'
__author__ = 'Daniel Greenfeld & Contributors'
VERSION = __version__ # synonym

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.model_admin.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):
model_admin = self.registry[model]
opts = {
'current_app': model_admin.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,30 +18,32 @@ 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 = {}
self.apps = {}
self.name = name
def register(self, model, modeladmin=None, **kwargs):
def register(self, model, model_admin=None, **kwargs):
"""
Registers the given model with the given admin class.
Registers the given model with the given admin class. Once a model is
registered in self.registry, we also add it to app registries in
self.apps.
If no modeladmin is passed, it will use ModelAdmin2. If keyword
If no model_admin is passed, it will use ModelAdmin2. If keyword
arguments are given they will be passed to the admin class on
instantiation.
If a model is already registered, this will raise ImproperlyConfigured.
Once a model is registered in self.registry, we also add it to app registries
in self.apps.
"""
if model in self.registry:
raise ImproperlyConfigured('%s is already registered in django-admin2' % model)
if not modeladmin:
modeladmin = models.ModelAdmin2
self.registry[model] = modeladmin(model, **kwargs)
if not model_admin:
model_admin = models.ModelAdmin2
self.registry[model] = model_admin(model, admin=self, **kwargs)
# Add the model to the apps registry
app_label = model._meta.app_label
@ -51,22 +54,32 @@ class Admin2(object):
def deregister(self, model):
"""
Deregisters the given model.
Deregisters the given model. Remove the model from the self.app as well
If the model is not already registered, this will raise ImproperlyConfigured.
TODO: Remove the model from the self.app as well
"""
try:
del self.registry[model]
except KeyError:
raise ImproperlyConfigured
raise ImproperlyConfigured('%s was never registered in django-admin2' % model)
# Remove the model from the apps registry
# Get the app label
app_label = model._meta.app_label
# Delete the model from it's app registry
del self.apps[app_label][model]
# if no more models in an app's registry
# then delete the app from the apps.
if self.apps[app_label] is {}:
del self.apps[app_label] # no
def autodiscover(self):
"""
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)
@ -81,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()
for model, model_admin in self.registry.iteritems():
urlpatterns += patterns('',
url('^{}/{}/'.format(app_label, model_name),
include(modeladmin.urls)),
url('^{}/{}/'.format(
model._meta.app_label,
model._meta.object_name.lower()),
include(model_admin.urls)),
url('^api/v0/{}/{}/'.format(
model._meta.app_label,
model._meta.object_name.lower()),
include(model_admin.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

@ -1,2 +0,0 @@
class NoAdminSpecified(Exception):
pass

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:
@ -20,6 +20,9 @@ except ImportError:
class BaseAdmin2(object):
"""
Warning: This class will likely merged with ModelAdmin2
"""
search_fields = []
@ -48,10 +51,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.
@ -85,6 +88,10 @@ class BaseAdmin2(object):
class ModelAdmin2(BaseAdmin2):
"""
Warning: This class is targeted for reduction.
It's bloated and ugly.
"""
list_display = ('__str__',)
list_display_links = ()
list_filter = ()
@ -110,8 +117,16 @@ class ModelAdmin2(BaseAdmin2):
detail_view = views.ModelDetailView
delete_view = views.ModelDeleteView
def __init__(self, model, **kwargs):
# API configuration
api_serializer_class = None
# 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()
@ -125,9 +140,16 @@ class ModelAdmin2(BaseAdmin2):
'app_label': self.app_label,
'model': self.model,
'model_name': self.model_name,
'modeladmin': self,
'model_admin': self,
}
def get_default_api_view_kwargs(self):
kwargs = self.get_default_view_kwargs()
kwargs.update({
'serializer_class': self.api_serializer_class,
})
return kwargs
def get_prefixed_view_name(self, view_name):
return '{}_{}_{}'.format(self.app_label, self.model_name, view_name)
@ -159,6 +181,16 @@ class ModelAdmin2(BaseAdmin2):
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_api_view_kwargs()
kwargs.update({
'paginate_by': self.list_per_page,
})
return kwargs
def get_api_detail_kwargs(self):
return self.get_default_api_view_kwargs()
def get_urls(self):
return patterns('',
url(
@ -188,11 +220,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

@ -0,0 +1,23 @@
/* Fixes a Bootstrap 2.3 bug. This can be removed when upgrading to Bootstrap v3. */
.text-right
{
text-align: right !important;
}
.text-center
{
text-align: center !important;
}
.text-left
{
text-align: left !important;
}
.checkbox-column {
width: 16px;
}
.space-below {
margin-bottom: 10px;
}

View file

@ -7,6 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Bootstrap -->
<link href="{{ STATIC_URL }}themes/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen">
<link href="{{ STATIC_URL }}themes/bootstrap/css/bootstrap-custom.css" rel="stylesheet" media="screen">
</head>
<body>
<div class="navbar navbar-inverse navbar-static-top">
@ -15,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>
@ -30,4 +32,4 @@
{% block extrajs %}{% endblock %}
</body>
</html>
</html>

View file

@ -1,28 +1,53 @@
{% extends "admin2/bootstrap/base.html" %}
{% load admin2_urls %}
{% block content %}
<h3>Site administration</h3>
{% for app, registry in apps.items %}
<table class="table">
<thead>
<tr>
<th>
<a href="TODO {{ app.get_index_url }}">{{ app|title }}</a>
</th>
</tr>
</thead>
<tbody>
{% for model_class, model_admin in registry.items %}
<tr>
<td>
<a href="{{ model_admin.get_index_url }}">
{{ model_admin.verbose_name_plural|title }}
</a>
</td>
</tr>
<div class="row">
<div class="span7">
{% for app, registry in apps.items %}
<table class="table table-bordered table-condensed">
<thead>
<tr>
<th colspan="3">
<a href="TODO {{ app.get_index_url }}">{{ app|title }}</a>
</th>
</tr>
</thead>
<tbody>
{% for model_class, model_admin in registry.items %}
<tr>
<td width="40%">
<a href="{{ model_admin.get_index_url }}">
{{ model_admin.verbose_name_plural|title }}
</a>
</td>
<td class="text-right">
{# if has_add_permission #}
<a href="{% url model_admin|admin2_urlname:'create' %}">
<i class="icon-plus"></i>
Add
</a>
{# endif #}
</td>
<td class="text-right">
<a href="{{ model_admin.get_index_url }}">
<i class="icon-pencil"></i>
Change
</a>
</td>
</tr>
{% endfor %}
</tbody>
</table>
{% endfor %}
</tbody>
</table>
{% endfor %}
</div>
<div class="span5">
<h4>Recent Actions</h4>
<h5>My Actions</h5>
TODO
</div>
</div>
{% endblock content %}

View file

@ -2,15 +2,25 @@
{% block content %}
<form method="post">
{% csrf_token %}
{{ form.as_p }}
<div class="row">
<div class="span10">
<h3>Change {{ model }}</h3>
</div>
</div>
{% for formset in inlines %}
{{ formset }}
{% endfor %}
<input type="submit"/>
</form>
<div class="row">
<div class="span12">
<form method="post">
{% csrf_token %}
{{ form.as_p }}
{% for formset in inlines %}
{{ formset }}
{% endfor %}
<input type="submit"/>
</form>
</div>
</div>
{% endblock content %}

View file

@ -6,47 +6,44 @@
<div class="span10">
<h3>Select {{ model }} to change</h3>
</div>
<div class="span2">
{# if has_add_permission #}
<a href="{% url view|admin2_urlname:'create' %}">add</a>
{# endif #}
</div>
</div>
<hr/>
<div class="row">
<div class="span12">
Action:
<select>
<option>----------</option>
<option>Delete selected {{ model }}{{ object_list|pluralize }}</option>
</select>
<a class="btn btn-mini" href="TODO">Go</a>
TODO of {{ object_list|length }} selected
<div class="space-below">
<div class="btn-group">
<button class="btn dropdown-toggle" data-toggle="dropdown">
Actions
<span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li><a tabindex="-1" href="#">Delete selected {{ model }}{{ object_list|pluralize }}</a></li>
</ul>
</div>
<small class="muted">TODO of {{ object_list|length }} selected</small>
<div class="pull-right">
{# if has_add_permission #}
<a href="{% url view|admin2_urlname:'create' %}" class="btn"><i class="icon-plus"></i> Add {{ model }}</a>
{# endif #}
</div>
</div>
<table class="table table-bordered table-striped">
<thead>
<th><input type="checkbox"></th>
<th class="checkbox-column"><input type="checkbox"></th>
<th>{{ model|title}}</th>
</thead>
<tbody>
{% for obj in object_list %}
<td><input type="checkbox"></td>
<td>
{{ obj }} <a href="{% url view|admin2_urlname:'detail' pk=obj.pk %}">detail</a>
{# if has_edit_permission #}
<a href="{% url view|admin2_urlname:'update' pk=obj.pk %}">edit</a>
{# endif #}
{# if has_delete_permission #}
<a href="{% url view|admin2_urlname:'delete' pk=obj.pk %}">delete</a>
{# endif #}
<a href="{% url view|admin2_urlname:'update' pk=obj.pk %}">{{ obj }}</a>
</td>
{% endfor %}
</tbody>
</table>
{{ object_list|length }} {{ model }}{{ object_list|pluralize }}
<p>{{ object_list|length }} {{ model }}{{ object_list|pluralize }}</p>
</div>
</div>

View file

@ -4,5 +4,9 @@ register = template.Library()
@register.filter
def admin2_urlname(value, arg):
return 'admin2:%s_%s_%s' % (value.app_label, value.model_name, arg)
def admin2_urlname(view, action):
"""
Converts the view and the specified action into a valid namespaced URLConf name.
"""
return 'admin2:%s_%s_%s' % (view.app_label, view.model_name, action)

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()))

View file

@ -16,7 +16,7 @@ ADMIN2_THEME_DIRECTORY = getattr(settings, "ADMIN2_THEME_DIRECTORY", "admin2/boo
class Admin2Mixin(object):
modeladmin = None
model_admin = None
model_name = None
app_label = None
@ -36,13 +36,13 @@ class Admin2Mixin(object):
class AdminModel2Mixin(Admin2Mixin, AccessMixin):
modeladmin = None
model_admin = None
# Permission type to check for when a request is sent to this view.
permission_type = None
def dispatch(self, request, *args, **kwargs):
# Check if user has necessary permissions. If the permission_type isn't specified then check for staff status.
has_permission = self.modeladmin.has_permission(request, self.permission_type) \
has_permission = self.model_admin.has_permission(request, self.permission_type) \
if self.permission_type else request.user.is_staff
# Raise exception or redirect to login if user doesn't have permissions.
if not has_permission:
@ -57,9 +57,9 @@ class AdminModel2Mixin(Admin2Mixin, AccessMixin):
def get_context_data(self, **kwargs):
context = super(AdminModel2Mixin, self).get_context_data(**kwargs)
context.update({
'has_add_permission': self.modeladmin.has_add_permission(self.request),
'has_edit_permission': self.modeladmin.has_edit_permission(self.request),
'has_delete_permission': self.modeladmin.has_delete_permission(self.request),
'has_add_permission': self.model_admin.has_add_permission(self.request),
'has_edit_permission': self.model_admin.has_edit_permission(self.request),
'has_delete_permission': self.model_admin.has_delete_permission(self.request),
})
return context
@ -116,6 +116,11 @@ class ModelEditFormView(AdminModel2Mixin, extra_views.UpdateWithInlinesView):
default_template_name = "model_edit_form.html"
permission_type = 'change'
def get_context_data(self, **kwargs):
context = super(ModelEditFormView, self).get_context_data(**kwargs)
context['model'] = self.get_model()._meta.verbose_name
return context
class ModelAddFormView(AdminModel2Mixin, extra_views.CreateWithInlinesView):
form_class = None

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

@ -173,7 +173,7 @@ First we pull the code into a local branch::
Then we run the tests::
python manage.py test
./runtests.py
We finish with a non-fastforward merge (to preserve the branch history) and push to GitHub::

View file

@ -17,4 +17,11 @@ Workflow
2. Loop through the Apps then models per App
3. Admin2s are created from models: djadmin2.models.register(Poll)
4. Admin2s contain methods/properties necessaey for UI
5. Views
5. Views
UI Goals
---------
1. Replicate the old admin UI as closely as possible in the bootstrap/ theme. This helps us ensure that admin2/ functionality has parity with admin/.
2. Once (1) is complete and we have a stable underlying API, experiment with more interesting UI variations.

View file

@ -7,14 +7,6 @@ Welcome to django-admin2's documentation!
This project is intentionally backwards-incompatible with ``django.contrib.admin``.
Features
==========
* Easy-to-extend API that follows similar patterns to ``django.contrib.admin``.
* Built-in RESTFUL API powered by ``django-rest-framework``
* Default theme built on Twitter Bootstrap
* Easy to implement theme system.
Requirements
=============
@ -61,7 +53,9 @@ Content
:maxdepth: 2
contributing
api
design
themes
meta
@ -71,4 +65,3 @@ Indices and tables
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

30
docs/themes.rst Normal file
View file

@ -0,0 +1,30 @@
======
Themes
======
How To Create a Theme
---------------------
A theme consists of 3 parts. Here's how you set those up:
1. Static files: create a directory in djadmin2/static/themes/ called your-theme-name/. Put your static files in there.
2. Templates: create a directory in djadmin2/templates/admin2/bootstrap/ called your-theme-name/. Copy the template files from the bootstrap theme into there and then modify them as you'd like.
3. Your settings file should point to your theme directory::
ADMIN2_THEME_DIRECTORY = "admin2/bootstrap/"
Look at the "bootstrap" theme as an example. If you run into any problems, please file an issue.
Available Themes
----------------
Currently, only the "bootstrap" theme exists. The goal of this theme is to replicate the original Django admin UI functionality as closely as possible. This helps us ensure that we are not forgetting any functionality that Django users might be dependent on.
If you'd like to experiment with UI design that differs from the original Django admin UI, please create a new theme. It would be great to have at least 1 experimental theme!
Future
------
Keep in mind that this project is an experiment just to get our ideas down. We are looking at other similar projects to see if we can merge or borrow things.

View file

@ -2,12 +2,33 @@
# 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, User
from rest_framework.relations import PrimaryKeyRelatedField
import extra_views
import djadmin2
from djadmin2.models import ModelAdmin2
from djadmin2.apiviews import Admin2APISerializer
class GroupSerializer(Admin2APISerializer):
permissions = PrimaryKeyRelatedField(many=True)
class Meta:
model = Group
class GroupAdmin2(ModelAdmin2):
api_serializer_class = GroupSerializer
class UserSerializer(Admin2APISerializer):
user_permissions = PrimaryKeyRelatedField(many=True)
class Meta:
model = User
exclude = ('passwords',)
class CommentInline(extra_views.InlineFormSet):
@ -22,8 +43,11 @@ class UserAdmin2(ModelAdmin2):
create_form_class = UserCreationForm
update_form_class = UserChangeForm
api_serializer_class = UserSerializer
# Register each model with the admin
djadmin2.default.register(Post, PostAdmin)
djadmin2.default.register(Comment)
djadmin2.default.register(User, UserAdmin2)
djadmin2.default.register(Group, GroupAdmin2)

View file

@ -0,0 +1,10 @@
{% extends "admin2/bootstrap/base.html" %}
{% block content %}
<h1>Example Home</h1>
<ul>
<li><a href="/admin2/">django-admin2</a></li>
<li><a href="/admin/">django.contrib.admin</a> (for reference)</li>
</ul>
{% endblock %}

View file

@ -0,0 +1,3 @@
from test_views import *
from test_apiviews import *
from test_builtin_api_resources 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'))
model_admin = self.get_model_admin(Post)
view = apiviews.ListCreateAPIView.as_view(
**model_admin.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'))
model_admin = self.get_model_admin(Post)
view = apiviews.ListCreateAPIView.as_view(
**model_admin.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'))
model_admin = self.get_model_admin(Post)
view = apiviews.ListCreateAPIView.as_view(
**model_admin.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}))
model_admin = self.get_model_admin(Post)
view = apiviews.RetrieveUpdateDestroyAPIView.as_view(
**model_admin.get_api_detail_kwargs())
response = view(request, pk=post.pk)
self.assertEqual(response.status_code, 200)

View file

@ -0,0 +1,29 @@
from django.contrib.auth.models import Group, User
from django.core.urlresolvers import reverse
from django.test import TestCase
class UserAPITest(TestCase):
def test_list_response_ok(self):
response = self.client.get(reverse('admin2:auth_user_api-list'))
self.assertEqual(response.status_code, 200)
def test_detail_response_ok(self):
user = User.objects.create_user(
username='Foo',
password='bar')
response = self.client.get(
reverse('admin2:auth_user_api-detail', args=(user.pk,)))
self.assertEqual(response.status_code, 200)
class GroupAPITest(TestCase):
def test_list_response_ok(self):
response = self.client.get(reverse('admin2:auth_group_api-list'))
self.assertEqual(response.status_code, 200)
def test_detail_response_ok(self):
group = Group.objects.create(name='group')
response = self.client.get(
reverse('admin2:auth_group_api-detail', args=(group.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',
)

View file

@ -1,5 +1,6 @@
from django.conf.urls import patterns, include, url
from django.contrib import admin
from django.views.generic import TemplateView
admin.autodiscover()
@ -10,4 +11,5 @@ djadmin2.default.autodiscover()
urlpatterns = patterns('',
url(r'^admin2/', include(djadmin2.default.urls)),
url(r'^admin/', include(admin.site.urls)),
url(r'^$', TemplateView.as_view(template_name="blog/home.html")),
)