mirror of
https://github.com/jazzband/django-admin2.git
synced 2026-04-30 19:44:46 +00:00
Merge remote-tracking branch 'origin/master' into inline-formsets
Conflicts: djadmin2/templates/admin2/bootstrap/model_edit_form.html
This commit is contained in:
commit
c625a80eaa
29 changed files with 547 additions and 110 deletions
|
|
@ -1,4 +1,4 @@
|
|||
include README.rst
|
||||
include LICENSE
|
||||
include CONTRIBUTORS.txt
|
||||
include AUTHORS.rst
|
||||
include MANIFEST.in
|
||||
30
README.rst
30
README.rst
|
|
@ -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
|
||||
==============
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
__version__ = '0.2.0'
|
||||
|
||||
__author__ = 'Daniel Greenfeld'
|
||||
__author__ = 'Daniel Greenfeld & Contributors'
|
||||
|
||||
VERSION = __version__ # synonym
|
||||
|
||||
|
|
|
|||
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.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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,2 +0,0 @@
|
|||
class NoAdminSpecified(Exception):
|
||||
pass
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
23
djadmin2/static/themes/bootstrap/css/bootstrap-custom.css
vendored
Normal file
23
djadmin2/static/themes/bootstrap/css/bootstrap-custom.css
vendored
Normal 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;
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,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
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.
|
||||
|
|
@ -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::
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
30
docs/themes.rst
Normal 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.
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
10
example/blog/templates/blog/home.html
Normal file
10
example/blog/templates/blog/home.html
Normal 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 %}
|
||||
3
example/blog/tests/__init__.py
Normal file
3
example/blog/tests/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
from test_views import *
|
||||
from test_apiviews import *
|
||||
from test_builtin_api_resources 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'))
|
||||
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)
|
||||
29
example/blog/tests/test_builtin_api_resources.py
Normal file
29
example/blog/tests/test_builtin_api_resources.py
Normal 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)
|
||||
|
|
@ -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',
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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")),
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue