Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Ludvig Wadenstein 2013-05-19 10:35:43 +02:00
commit 151bbce7ae
17 changed files with 256 additions and 102 deletions

View file

@ -1,6 +1,5 @@
language: python
python:
- "2.6"
- "2.7"
before_install:
- export PIP_USE_MIRRORS=true

View file

@ -15,6 +15,14 @@ Contributing
Yes please! Please read our formal contributing document at: https://github.com/pydanny/django-admin2/blob/master/docs/contributing.rst
Requirements
=============
* Django 1.5+
* Python 2.7+ (Python 3.3+ support is pending)
* django-braces
* django-rest-framework
* Sphinx (for documentation)
Basic Pattern
==============
@ -23,20 +31,25 @@ Our goal is to make this API work:
.. code-block:: python
# myapp/admin2.py
# Import your custom models
from .models import Post, Comment
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
from django.contrib.auth.models import User
# Import the Admin2 base class
from admin2.models import Admin2
import djadmin2
from djadmin2.models import ModelAdmin2
# Import your custom models
from blog.models import Post
# Instantiate the Admin2 class
# Then attach the admin2 object to your model
Post.admin2 = Admin2()
class UserAdmin2(ModelAdmin2):
create_form_class = UserCreationForm
update_form_class = UserChangeForm
# Register each model with the admin
djadmin2.default.register(Post)
djadmin2.default.register(Comment)
djadmin2.default.register(User, UserAdmin2)
.. note:: You will notice a difference between how and django.contrib.admin and django-admin2 do configuration. The former associates the configuration class with the model object via a registration utility, and the latter does so by adding the configuration class as an attribute of the model object.
Themes
========

View file

@ -1,4 +1,4 @@
__version__ = '0.1.1'
__version__ = '0.2.0'
__author__ = 'Daniel Greenfeld'
@ -7,7 +7,7 @@ VERSION = __version__ # synonym
# Default datetime input and output formats
ISO_8601 = 'iso-8601'
from . import core
from . import core
default = core.Admin2()

View file

@ -9,14 +9,29 @@ from . import views
class Admin2(object):
"""
The base Admin2 object.
It keeps a registry of all registered Models and collects the urls of their
related ModelAdmin2 instances.
It also provides an index view that serves as an entry point to the admin site.
"""
index_view = views.IndexView
def __init__(self, name='admin2', app_name='admin2'):
def __init__(self, name='admin2'):
self.registry = {}
self.name = name
self.app_name = app_name
def register(self, model, modeladmin=None, **kwargs):
"""
Registers the given model with the given admin class.
If no modeladmin 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.
"""
if model in self.registry:
raise ImproperlyConfigured
if not modeladmin:
@ -24,13 +39,21 @@ class Admin2(object):
self.registry[model] = modeladmin(model, **kwargs)
def deregister(self, model):
"""
Deregisters the given model.
If the model is not already registered, this will raise ImproperlyConfigured.
"""
try:
del self.registry[model]
except KeyError:
raise ImproperlyConfigured
def autodiscover(self):
apps = []
"""
Autodiscovers all admin2.py modules for apps in INSTALLED_APPS by
trying to import them.
"""
for app_name in [x for x in settings.INSTALLED_APPS]:
try:
import_module("%s.admin2" % app_name)
@ -46,11 +69,11 @@ class Admin2(object):
def get_urls(self):
urlpatterns = patterns('',
url(r'^$', self.index_view.as_view(**self.get_index_kwargs()), name='index'),
url(r'^$', self.index_view.as_view(**self.get_index_kwargs()), name='dashboard'),
)
for model, modeladmin in self.registry.iteritems():
app_label = model._meta.app_label
model_name = model._meta.object_name.lower()
model_name = model._meta.object_name.lower()
urlpatterns += patterns('',
url('^{}/{}/'.format(app_label, model_name),
@ -60,4 +83,4 @@ class Admin2(object):
@property
def urls(self):
return self.get_urls(), self.app_name, self.name
return self.get_urls(), self.name, self.name

View file

@ -4,7 +4,10 @@ For wont of a better name, this module is called 'models'. It's role is
synonymous with the django.contrib.admin.sites model.
"""
from django.conf.urls import patterns, include, url
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
@ -16,7 +19,6 @@ except ImportError:
from django import forms
class BaseAdmin2(object):
search_fields = []
@ -46,13 +48,11 @@ class BaseAdmin2(object):
readonly_fields = ()
ordering = None
def __init__(self, model):
super(BaseAdmin2, self).__init__()
self.model = model
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.
Type can be one of view, add, change, delete.
@ -110,6 +110,8 @@ class ModelAdmin2(BaseAdmin2):
def __init__(self, model, **kwargs):
self.model = model
self.app_label = model._meta.app_label
self.model_name = model._meta.object_name.lower()
if self.verbose_name is None:
self.verbose_name = self.model._meta.verbose_name
@ -118,10 +120,15 @@ class ModelAdmin2(BaseAdmin2):
def get_default_view_kwargs(self):
return {
'app_label': self.app_label,
'model': self.model,
'model_name': self.model_name,
'modeladmin': self,
}
def get_prefixed_view_name(self, view_name):
return '{}_{}_{}'.format(self.app_label, self.model_name, view_name)
def get_index_kwargs(self):
return self.get_default_view_kwargs()
@ -145,32 +152,35 @@ class ModelAdmin2(BaseAdmin2):
def get_delete_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_urls(self):
return patterns('',
url(
regex=r'^$',
view=self.index_view.as_view(**self.get_index_kwargs()),
name='index'
name=self.get_prefixed_view_name('index')
),
url(
regex=r'^create/$',
view=self.create_view.as_view(**self.get_create_kwargs()),
name='create'
name=self.get_prefixed_view_name('create')
),
url(
regex=r'^(?P<pk>[0-9]+)/$',
view=self.detail_view.as_view(**self.get_detail_kwargs()),
name='detail'
name=self.get_prefixed_view_name('detail')
),
url(
regex=r'^(?P<pk>[0-9]+)/update/$',
view=self.update_view.as_view(**self.get_update_kwargs()),
name='update'
name=self.get_prefixed_view_name('update')
),
url(
regex=r'^(?P<pk>[0-9]+)/delete/$',
view=self.delete_view.as_view(**self.get_delete_kwargs()),
name='delete'
name=self.get_prefixed_view_name('delete')
),
)
@ -179,9 +189,7 @@ class ModelAdmin2(BaseAdmin2):
# We set the application and instance namespace here
return self.get_urls(), None, None
def create_permissions(app, created_models, verbosity, **kwargs):
def create_extra_permissions(app, created_models, verbosity, **kwargs):
"""
Creates 'view' permissions for all models.
django.contrib.auth only creates add, change and delete permissions. Since we also support read-only views, we need
@ -190,9 +198,6 @@ def create_permissions(app, created_models, verbosity, **kwargs):
"""
from django.contrib.contenttypes.models import ContentType
def _get_permission_codename(action, opts):
return u'%s_%s' % (action, opts.object_name.lower())
app_models = get_models(app)
# This will hold the permissions we're looking for as
@ -205,10 +210,10 @@ def create_permissions(app, created_models, verbosity, **kwargs):
ctypes.add(ctype)
opts = klass._meta
perm = (_get_permission_codename('view', opts), u'Can view %s' % opts.verbose_name_raw)
perm = ('view_%s' % opts.object_name.lower(), u'Can view %s' % opts.verbose_name_raw)
searched_perms.append((ctype, perm))
# Find all the Permissions that have a context_type for a model we're
# Find all the Permissions that have a content_type for a model we're
# looking for. We don't need to check for codenames since we already have
# a list of the ones we're going to create.
all_perms = set(auth_app.Permission.objects.filter(
@ -217,16 +222,16 @@ def create_permissions(app, created_models, verbosity, **kwargs):
"content_type", "codename"
))
objs = [
perms = [
auth_app.Permission(codename=codename, name=name, content_type=ctype)
for ctype, (codename, name) in searched_perms
if (ctype.pk, codename) not in all_perms
]
auth_app.Permission.objects.bulk_create(objs)
auth_app.Permission.objects.bulk_create(perms)
if verbosity >= 2:
for obj in objs:
print "Adding permission '%s'" % obj
for perm in perms:
print "Adding permission '%s'" % perm
signals.post_syncdb.connect(create_permissions,
dispatch_uid = "django-admin2.djadmin2.models.create_permissions")
signals.post_syncdb.connect(create_extra_permissions,
dispatch_uid = "django-admin2.djadmin2.models.create_extra_permissions")

View file

@ -1,19 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<title>django-admin2</title>
<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">
</head>
<body>
<div class="container-fluid">
{% block content %}{% endblock %}
</div>
<head>
<meta charset="utf-8">
<script src="http://code.jquery.com/jquery.js"></script>
<script src="{{ STATIC_URL }}themes/bootstrap/js/bootstrap.min.js"></script>
{% block extrajs %}{% endblock %}
<title>django-admin2</title>
<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">
</head>
<body>
<div class="navbar navbar-inverse navbar-static-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="brand" href="{% url 'admin2:dashboard' %}">Django-Admin2</a>
</body>
<ul class="nav pull-right">
<li><a href="TODO">Log out</a></li>
</ul>
</div>
</div>
</div>
<div class="container-fluid">
{% block content %}{% endblock %}
</div>
<script src="http://code.jquery.com/jquery.js"></script>
<script src="{{ STATIC_URL }}themes/bootstrap/js/bootstrap.min.js"></script>
{% block extrajs %}{% endblock %}
</body>
</html>

View file

@ -4,7 +4,7 @@
<h1>Index</h1>
<table>
{% for modeladmin in registry.values %}
<tr><td><a href="">{{ modeladmin.verbose_name_plural }}</a></td></tr>
<tr><td><a href="{{ modeladmin.get_index_url }}">{{ modeladmin.verbose_name_plural }}</a></td></tr>
{% endfor %}
</table>
{% endblock content %}

View file

@ -1,4 +1,5 @@
{% extends "admin2/bootstrap/base.html" %}
{% load admin2_urls %}
{% block content %}
<div class="row">
@ -6,9 +7,9 @@
<h3>Select {{ model }} to change</h3>
</div>
<div class="span2">
{% if has_add_permission %}
<a class="btn" href="./create/">Add {{ model|title }} <i class=" icon-plus-sign"></i></a>
{% endif %}
{# if has_add_permission #}
<a href="{% url view|admin2_urlname:'create' %}">add</a>
{# endif #}
</div>
</div>
@ -16,6 +17,14 @@
<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
<table class="table table-bordered table-striped">
<thead>
<th><input type="checkbox"></th>
@ -25,17 +34,19 @@
{% for obj in object_list %}
<td><input type="checkbox"></td>
<td>
{{ obj }} <a href="./{{ obj.pk }}/">detail</a>
{% if has_edit_permission %}
<a href="./{{ obj.pk }}/update/">edit</a>
{% endif %}
{% if has_delete_permission %}
<a href="./{{ obj.pk }}/delete/">delete</a>
{% endif %}
{{ 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 #}
</td>
{% endfor %}
</tbody>
</table>
{{ object_list|length }} {{ model }}{{ object_list|pluralize }}
</div>
</div>

View file

@ -0,0 +1,8 @@
from django import template
register = template.Library()
@register.filter
def admin2_urlname(value, arg):
return 'admin2:%s_%s_%s' % (value.app_label, value.model_name, arg)

View file

@ -6,9 +6,11 @@ from django.core.exceptions import ImproperlyConfigured
from ..models import ModelAdmin2
from ..core import Admin2
class Thing(models.Model):
pass
class Admin2Test(unittest.TestCase):
def setUp(self):
self.admin2 = Admin2()
@ -28,7 +30,7 @@ class Admin2Test(unittest.TestCase):
def test_deregister_error(self):
self.assertRaises(ImproperlyConfigured, self.admin2.deregister, Thing)
def test_get_urls(self):
self.admin2.register(Thing)
self.assertEquals(2, len(self.admin2.get_urls()))

View file

@ -1,22 +1,36 @@
import os
from django.core.urlresolvers import reverse
from django.conf import settings
from django.contrib.auth.views import redirect_to_login
from django.core.exceptions import PermissionDenied
from django.forms.models import modelform_factory
from django.views import generic
from django.db import models
from braces.views import LoginRequiredMixin, StaffuserRequiredMixin, AccessMixin
from braces.views import AccessMixin
ADMIN2_THEME_DIRECTORY = getattr(settings, "ADMIN2_THEME_DIRECTORY", "admin2/bootstrap")
class Admin2Mixin(object):
modeladmin = None
model_name = None
app_label = None
def get_template_names(self):
return [os.path.join(ADMIN2_THEME_DIRECTORY, self.default_template_name)]
def get_model(self):
return self.model
def get_queryset(self):
return self.get_model()._default_manager.all()
def get_form_class(self):
if self.form_class is not None:
return self.form_class
return modelform_factory(self.get_model())
class AdminModel2Mixin(Admin2Mixin, AccessMixin):
modeladmin = None
@ -70,7 +84,9 @@ class IndexView(Admin2Mixin, generic.TemplateView):
})
return data
class ModelListView(AdminModel2Mixin, generic.ListView):
class ModelListView(Admin2Mixin, generic.ListView):
default_template_name = "model_list.html"
permission_type = 'view'
@ -80,6 +96,10 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
context['model_pluralized'] = self.get_model()._meta.verbose_name_plural
return context
def get_success_url(self):
view_name = 'admin2:{}_{}_detail'.format(self.app_label, self.model_name)
return reverse(view_name, kwargs={'pk': self.object.pk})
class ModelDetailView(AdminModel2Mixin, generic.DetailView):
default_template_name = "model_detail.html"
@ -99,8 +119,12 @@ class ModelAddFormView(AdminModel2Mixin, generic.CreateView):
default_template_name = "model_add_form.html"
permission_type = 'add'
def get_success_url(self):
view_name = 'admin2:{}_{}_detail'.format(self.app_label, self.model_name)
return reverse(view_name, kwargs={'pk': self.object.pk})
class ModelDeleteView(AdminModel2Mixin, generic.DeleteView):
success_url = "../../"
default_template_name = "model_delete.html"
default_template_name = "model_confirm_delete.html"
permission_type = 'delete'

View file

@ -1,10 +1,30 @@
Welcome to django-admin2's documentation!
=========================================
**django-admin2** aims to replace django's builtin admin that lives in
**django-admin2** aims to replace django's built-in admin that lives in
``django.contrib.admin``. Come and help us, have a look at the
:doc:`contributing` page and see our `GitHub`_ page.
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
=============
* Django 1.5+
* Python 2.7+ (Python 3.3+ support is pending)
* django-braces
* django-rest-framework
* Sphinx (for documentation)
Basic API
==============
@ -44,6 +64,7 @@ Content
design
meta
Indices and tables
==================

View file

@ -17,4 +17,4 @@ class UserAdmin2(ModelAdmin2):
# Register each model with the admin
djadmin2.default.register(Post)
djadmin2.default.register(Comment)
djadmin2.default.register(User, UserAdmin2)
djadmin2.default.register(User, UserAdmin2)

View file

@ -14,4 +14,4 @@ class Comment(models.Model):
body = models.TextField()
def __unicode__(self):
return self.body
return self.body

View file

@ -1,36 +1,70 @@
from django.utils import unittest
from django.test.client import RequestFactory
from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.test import TestCase, Client
from djadmin2 import views
from .models import Post
class ViewTest(unittest.TestCase):
class BaseIntegrationTest(TestCase):
"""
Base TestCase for integration tests.
"""
def setUp(self):
self.factory = RequestFactory()
self.client = Client()
self.user = get_user_model()(username='user', is_staff=True,
is_superuser=True)
self.user.set_password("password")
self.user.save()
self.client.login(username='user', password='password')
class IndexViewTest(ViewTest):
def test_response_ok(self):
request = self.factory.get('/admin/blog/post/')
response = views.IndexView.as_view()(request)
class AdminIndexTest(BaseIntegrationTest):
def test_view_ok(self):
response = self.client.get(reverse("admin2:dashboard"))
self.assertContains(response, reverse("admin2:blog_post_index"))
class PostListTest(BaseIntegrationTest):
def test_view_ok(self):
post = Post.objects.create(title="a_post_title", body="body")
response = self.client.get(reverse("admin2:blog_post_index"))
self.assertContains(response, post.title)
class PostDetailViewTest(BaseIntegrationTest):
def test_view_ok(self):
post = Post.objects.create(title="a_post_title", body="body")
response = self.client.get(reverse("admin2:blog_post_detail",
args=(post.pk, )))
self.assertContains(response, post.title)
class PostCreateViewTest(BaseIntegrationTest):
def test_view_ok(self):
response = self.client.get(reverse("admin2:blog_post_create"))
self.assertEqual(response.status_code, 200)
class ModelListViewTest(ViewTest):
pass
def test_create_post(self):
response = self.client.post(reverse("admin2:blog_post_create"),
{"title": "a_post_title",
"body": "a_post_body"},
follow=True)
self.assertTrue(Post.objects.filter(title="a_post_title").exists())
post = Post.objects.get(title="a_post_title")
self.assertRedirects(response, reverse("admin2:blog_post_detail",
args=(post.pk, )))
class ModelDetailViewTest(ViewTest):
pass
class ModelEditFormViewTest(ViewTest):
pass
class ModelAddFormViewTest(ViewTest):
pass
class ModelDeleteViewTest(ViewTest):
pass
class PostDeleteViewTest(BaseIntegrationTest):
def test_view_ok(self):
post = Post.objects.create(title="a_post_title", body="body")
response = self.client.get(reverse("admin2:blog_post_delete",
args=(post.pk, )))
self.assertContains(response, post.title)
def test_delete_post(self):
post = Post.objects.create(title="a_post_title", body="body")
response = self.client.post(reverse("admin2:blog_post_delete",
args=(post.pk, )))
self.assertRedirects(response, reverse("admin2:blog_post_index"))
self.assertFalse(Post.objects.filter(pk=post.pk).exists())