Merge pull request #431 from arthur-wsw/django_1_8

Django 1.8 and 1.9 compatibility
This commit is contained in:
Kamil Gałuszka 2016-05-25 09:55:49 +02:00
commit d9b3d2c8a6
66 changed files with 887 additions and 709 deletions

6
.eggs/README.txt Normal file
View file

@ -0,0 +1,6 @@
This directory contains eggs that were downloaded by setuptools to build, test, and run plug-ins.
This directory caches those eggs to prevent repeated downloads.
However, it is safe to delete this directory.

5
.gitignore vendored
View file

@ -59,3 +59,8 @@ logfile
# test media upload
media
# PyCharm
.idea/
.cache

View file

@ -1,47 +1,28 @@
sudo: false
language: python
python: "2.7"
python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"
env:
matrix:
- TOX_ENV=py27-dj1.6.x
- TOX_ENV=py27-dj1.7.x
- TOX_ENV=py27-dj1.8.x
- TOX_ENV=py27-dj1.9.x
- TOX_ENV=py33-dj1.6.x
- TOX_ENV=py33-dj1.7.x
- TOX_ENV=py33-dj1.8.x
- TOX_ENV=py33-dj1.9.x
- TOX_ENV=py34-dj1.6.x
- TOX_ENV=py34-dj1.7.x
- TOX_ENV=py34-dj1.8.x
- TOX_ENV=py34-dj1.9.x
- TOX_ENV=pypy-dj1.6.x
- TOX_ENV=pypy-dj1.7.x
- TOX_ENV=pypy-dj1.8.x
- TOX_ENV=pypy-dj1.9.x
- TOX_ENV=pypy3-dj1.6.x
- TOX_ENV=pypy3-dj1.8.x
- TOX_ENV=pypy3-dj1.9.x
- DJANGO=1.8
- DJANGO=1.9
- DJANGO=master
matrix:
exclude:
- python: "3.3"
env: DJANGO=1.9
- python: "3.3"
env: DJANGO=master
allow_failures:
- python: "2.7"
env: DJANGO=master
- python: "3.4"
env: DJANGO=master
- python: "3.5"
env: DJANGO=master
install:
- pip install tox
script:
- tox -e $TOX_ENV
# for now commented. We have to figure which version use for coverage
# and coveralls
#after_success:
# - coverage report
# - pip install --quiet python-coveralls
# - coveralls
matrix:
allow_failures:
- env: TOX_ENV=py27-dj1.8.x
- env: TOX_ENV=py27-dj1.9.x
- env: TOX_ENV=py33-dj1.8.x
- env: TOX_ENV=py33-dj1.9.x
- env: TOX_ENV=py34-dj1.8.x
- env: TOX_ENV=py34-dj1.9.x
- env: TOX_ENV=pypy-dj1.8.x
- env: TOX_ENV=pypy-dj1.9.x
- env: TOX_ENV=pypy3-dj1.8.x
- env: TOX_ENV=pypy3-dj1.9.x
- env: TOX_ENV=pypy3-dj1.9.x
- tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-$DJANGO

View file

@ -52,6 +52,7 @@ Developers
* marangonico
* Kamil Gałuszka (@galuszkak / galuszkak@gmail.com)
* Germano Gabbianelli (@tyrion)
* Arthur (@arthur-wsw / arthur@wallstreetweb.net)
Translators
-----------

View file

@ -1,6 +1,20 @@
History
=========
0.6.2 (?)
* Fix Django 1.8 issues and add 1.9 compatibility
* Update all dependancies (DRF, floppyforms, filters, ...)
* Regenerate example project to make it django 1.9 compatible
* Update tox and travis and add flake8
* Rename AdminModel2Mixin to Admin2ModelMixin
* Add migrations
* Replace IPAddressField with GenericIPAddressField
* Fix password link in user admin
* Fix user logout on password change
* Fix tests
0.6.1 (2014-02-26)
* Fix empty form display

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
__version__ = '0.6.1'
__author__ = 'Daniel Greenfeld & Contributors'
@ -10,17 +11,4 @@ VERSION = __version__ # synonym
# Default datetime input and output formats
ISO_8601 = 'iso-8601'
from . import core
from . import types
default = core.Admin2()
ModelAdmin2 = types.ModelAdmin2
Admin2TabularInline = types.Admin2TabularInline
Admin2StackedInline = types.Admin2StackedInline
# Utility to make migration between versions easier
sites = default
ModelAdmin = ModelAdmin2
AdminInline = Admin2TabularInline
Admin2Inline = Admin2TabularInline
default_app_config = "djadmin2.apps.Djadmin2Config"

View file

@ -2,14 +2,15 @@
from __future__ import division, absolute_import, unicode_literals
from django.contrib import messages
from django.views.generic import TemplateView
from django.db import router
from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy, ungettext, pgettext_lazy
from django.utils.translation import ugettext as _
from django.utils.translation import ugettext_lazy, ungettext, pgettext_lazy
from django.views.generic import TemplateView
from . import permissions, utils
from .viewmixins import AdminModel2Mixin
from .viewmixins import Admin2ModelMixin
def get_description(action):
@ -21,7 +22,7 @@ def get_description(action):
return capfirst(action.__name__.replace('_', ' '))
class BaseListAction(AdminModel2Mixin, TemplateView):
class BaseListAction(Admin2ModelMixin, TemplateView):
permission_classes = (permissions.IsStaffPermission,)
@ -54,7 +55,7 @@ class BaseListAction(AdminModel2Mixin, TemplateView):
super(BaseListAction, self).__init__(*args, **kwargs)
def get_queryset(self):
""" Replaced `get_queryset` from `AdminModel2Mixin`"""
""" Replaced `get_queryset` from `Admin2ModelMixin`"""
return self.queryset
def description(self):
@ -94,7 +95,9 @@ class BaseListAction(AdminModel2Mixin, TemplateView):
return '%s: %s' % (force_text(capfirst(opts.verbose_name)),
force_text(obj))
collector = utils.NestedObjects(using=None)
using = router.db_for_write(self.model)
collector = utils.NestedObjects(using=using)
collector.collect(self.queryset)
context.update({
@ -166,7 +169,6 @@ class DeleteSelectedAction(BaseListAction):
# objects, so render a template asking for their confirmation.
return self.get(request)
def process_queryset(self):
# The user has confirmed that they want to delete the objects.
self.get_queryset().delete()

View file

@ -4,34 +4,34 @@ from __future__ import division, absolute_import, unicode_literals
from django.conf import settings
from django.contrib.auth.models import Group, User
from django.contrib.sites.models import Site
from rest_framework.relations import PrimaryKeyRelatedField
import djadmin2
from djadmin2.forms import UserCreationForm, UserChangeForm
from djadmin2.apiviews import Admin2APISerializer
from djadmin2.forms import UserCreationForm, UserChangeForm
from djadmin2.site import djadmin2_site
from djadmin2.types import ModelAdmin2
class GroupSerializer(Admin2APISerializer):
permissions = PrimaryKeyRelatedField(many=True)
permissions = PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = Group
class GroupAdmin2(djadmin2.ModelAdmin2):
class GroupAdmin2(ModelAdmin2):
api_serializer_class = GroupSerializer
class UserSerializer(Admin2APISerializer):
user_permissions = PrimaryKeyRelatedField(many=True)
user_permissions = PrimaryKeyRelatedField(many=True, read_only=True)
class Meta:
model = User
exclude = ('passwords',)
exclude = ('password',)
class UserAdmin2(djadmin2.ModelAdmin2):
class UserAdmin2(ModelAdmin2):
create_form_class = UserCreationForm
update_form_class = UserChangeForm
search_fields = ('username', 'groups__name', 'first_name', 'last_name',
@ -43,15 +43,15 @@ class UserAdmin2(djadmin2.ModelAdmin2):
# Register each model with the admin
djadmin2.default.register(User, UserAdmin2)
djadmin2.default.register(Group, GroupAdmin2)
djadmin2_site.register(User, UserAdmin2)
djadmin2_site.register(Group, GroupAdmin2)
# Register the sites app if it's been activated in INSTALLED_APPS
if "django.contrib.sites" in settings.INSTALLED_APPS:
class SiteAdmin2(djadmin2.ModelAdmin2):
class SiteAdmin2(ModelAdmin2):
list_display = ('domain', 'name')
search_fields = ('domain', 'name')
djadmin2.default.register(Site, SiteAdmin2)
djadmin2_site.register(Site, SiteAdmin2)

View file

@ -2,7 +2,6 @@
from __future__ import division, absolute_import, unicode_literals
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
@ -17,11 +16,30 @@ 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')
__unicode__ = fields.Field(source='__str__')
pk = fields.ReadOnlyField()
__unicode__ = fields.ReadOnlyField(source='__str__')
def get_extra_kwargs(self):
extra_kwargs = super(Admin2APISerializer, self).get_extra_kwargs()
extra_kwargs.update({
'url': {'view_name': self._get_default_view_name(self.Meta.model)}
})
return extra_kwargs
def _get_default_view_name(self, model):
"""
Return the view name to use if 'view_name' is not specified in 'Meta'
"""
model_meta = model._meta
format_kwargs = {
'app_label': model_meta.app_label,
'model_name': model_meta.object_name.lower()
}
return self._default_view_name % format_kwargs
class Admin2APIMixin(Admin2Mixin):
model = None
raise_exception = True
def get_serializer_class(self):

14
djadmin2/apps.py Normal file
View file

@ -0,0 +1,14 @@
from django.apps import AppConfig
from django.db.models.signals import post_migrate
from django.utils.translation import ugettext_lazy as _
from djadmin2.permissions import create_view_permissions
class Djadmin2Config(AppConfig):
name = 'djadmin2'
verbose_name = _("Django Admin2")
def ready(self):
post_migrate.connect(create_view_permissions,
dispatch_uid="django-admin2.djadmin2.permissions.create_view_permissions")

View file

@ -5,12 +5,11 @@ Issue #99.
"""
from __future__ import division, absolute_import, unicode_literals
from django.conf.urls import patterns, include, url
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from importlib import import_module
from django.conf import settings
from django.conf.urls import include, url
from django.core.exceptions import ImproperlyConfigured
from . import apiviews
from . import types
@ -160,8 +159,7 @@ class Admin2(object):
}
def get_urls(self):
urlpatterns = patterns(
'',
urlpatterns = [
url(regex=r'^$',
view=self.index_view.as_view(**self.get_index_kwargs()),
name='dashboard'
@ -188,11 +186,10 @@ class Admin2(object):
**self.get_api_index_kwargs()),
name='api_index'
),
)
]
for model, model_admin in self.registry.items():
model_options = utils.model_options(model)
urlpatterns += patterns(
'',
urlpatterns += [
url('^{}/{}/'.format(
model_options.app_label,
model_options.object_name.lower()),
@ -201,11 +198,10 @@ class Admin2(object):
model_options.app_label,
model_options.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

@ -2,19 +2,17 @@
from __future__ import division, absolute_import, unicode_literals
import collections
from itertools import chain
from django import forms
from django.forms.util import flatatt
from django.utils.html import format_html
from django.utils.encoding import force_text, force_bytes
from django.utils.safestring import mark_safe
from django.forms import widgets as django_widgets
from django.utils import six
from django.utils.translation import ugettext_lazy
import django_filters
from django import forms
from django.forms import widgets as django_widgets
from django.forms.utils import flatatt
from django.utils import six
from django.utils.encoding import force_text
from django.utils.html import format_html
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy
from .utils import type_str
@ -97,21 +95,21 @@ def build_list_filter(request, model_admin, queryset):
'fields': fields,
},
)
return type(type_str('%sFilterSet' % queryset.model.__name__),(django_filters.FilterSet, ),filterset_dict,)(request.GET, queryset=queryset)
return type(type_str('%sFilterSet' % queryset.model.__name__), (django_filters.FilterSet, ), filterset_dict,)(request.GET, queryset=queryset)
def build_date_filter(request, model_admin, queryset):
def build_date_filter(request, model_admin, queryset, field_name="published_date"):
filterset_dict = {
"year": NumericDateFilter(
name="published_date",
name=field_name,
lookup_type="year",
),
"month": NumericDateFilter(
name="published_date",
name=field_name,
lookup_type="month",
),
"day": NumericDateFilter(
name="published_date",
name=field_name,
lookup_type="day",
)
}

View file

@ -3,16 +3,16 @@ from __future__ import division, absolute_import, unicode_literals
from copy import deepcopy
import django
import django.forms
import django.forms.extras.widgets
import django.forms.models
import floppyforms
from django.contrib.auth import authenticate
from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.forms import UserCreationForm, UserChangeForm
import django
import django.forms
import django.forms.models
import django.forms.extras.widgets
from django.utils.translation import ugettext_lazy
import floppyforms
from django.core.urlresolvers import reverse_lazy
from django.utils.translation import ugettext_lazy as _
_WIDGET_COMMON_ATTRIBUTES = (
@ -166,9 +166,7 @@ _django_to_floppyforms_widget = {
django.forms.extras.widgets.SelectDateWidget:
_create_widget(
floppyforms.widgets.SelectDateWidget,
init_arguments=
('years',)
if django.VERSION >= (1, 7) else ('years', 'required')),
init_arguments=('years',) if django.VERSION >= (1, 7) else ('years', 'required')),
}
_django_field_to_floppyform_widget = {
@ -184,8 +182,8 @@ _django_field_to_floppyform_widget = {
_create_widget(floppyforms.widgets.URLInput),
django.forms.fields.SlugField:
_create_widget(floppyforms.widgets.SlugInput),
django.forms.fields.IPAddressField:
_create_widget(floppyforms.widgets.IPAddressInput),
django.forms.fields.GenericIPAddressField:
_create_widget(floppyforms.widgets.TextInput),
django.forms.fields.SplitDateTimeField:
_create_splitdatetimewidget(floppyforms.widgets.SplitDateTimeWidget),
}
@ -201,11 +199,10 @@ def allow_floppify_widget_for_field(field):
# replaces the default TextInput with a NumberInput, if localization is
# turned off. That applies for Django 1.6 upwards.
# See the relevant source code in django:
# https://github.com/django/django/blob/1.6/django/forms/fields.py#L225
if django.VERSION >= (1, 6):
if isinstance(field, django.forms.IntegerField) and not field.localize:
if field.widget.__class__ is django.forms.NumberInput:
return True
# https://github.com/django/django/blob/1.9.6/django/forms/fields.py#L261-264
if isinstance(field, django.forms.IntegerField) and not field.localize:
if field.widget.__class__ is django.forms.NumberInput:
return True
# We can check if the widget was replaced by comparing the class of the
# specified widget with the default widget that is specified on the field
@ -272,8 +269,10 @@ def modelform_factory(model, form=django.forms.models.ModelForm, fields=None,
# Translators : %(username)s will be replaced by the username_field name
# (default : username, but could be email, or something else)
ERROR_MESSAGE = ugettext_lazy("Please enter the correct %(username)s and password "
"for a staff account. Note that both fields may be case-sensitive.")
ERROR_MESSAGE = _(
"Please enter the correct %(username)s and password "
"for a staff account. Note that both fields may be case-sensitive."
)
class AdminAuthenticationForm(AuthenticationForm):
@ -283,10 +282,13 @@ class AdminAuthenticationForm(AuthenticationForm):
"""
error_messages = {
'required': ugettext_lazy("Please log in again, because your session has expired."),
'required': _("Please log in again, because your session has expired."),
}
this_is_the_login_form = django.forms.BooleanField(widget=floppyforms.HiddenInput,
initial=1, error_messages=error_messages)
this_is_the_login_form = django.forms.BooleanField(
widget=floppyforms.HiddenInput,
initial=1,
error_messages=error_messages
)
def clean(self):
username = self.cleaned_data.get('username')
@ -306,5 +308,17 @@ class AdminAuthenticationForm(AuthenticationForm):
return self.cleaned_data
class Admin2UserChangeForm(UserChangeForm):
def __init__(self, *args, **kwargs):
super(Admin2UserChangeForm, self).__init__(*args, **kwargs)
print(self.fields['password'].help_text)
self.fields['password'].help_text = _("Raw passwords are not stored, so there is no way to see this user's password, but you can change the password using <a href=\"%s\">this form</a>." % self.get_update_password_url())
def get_update_password_url(self):
if self.instance and self.instance.pk:
return reverse_lazy('admin2:password_change', args=[self.instance.pk])
return 'password/'
UserCreationForm = floppify_form(UserCreationForm)
UserChangeForm = floppify_form(UserChangeForm)
UserChangeForm = floppify_form(Admin2UserChangeForm)

View file

@ -0,0 +1,34 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='LogEntry',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('action_time', models.DateTimeField(verbose_name='action time', auto_now=True)),
('object_id', models.TextField(verbose_name='object id', null=True, blank=True)),
('object_repr', models.CharField(max_length=200, verbose_name='object repr')),
('action_flag', models.PositiveSmallIntegerField(verbose_name='action flag')),
('change_message', models.TextField(verbose_name='change message', blank=True)),
('content_type', models.ForeignKey(related_name='log_entries', null=True, blank=True, to='contenttypes.ContentType')),
('user', models.ForeignKey(related_name='log_entries', to=settings.AUTH_USER_MODEL)),
],
options={
'verbose_name': 'log entry',
'ordering': ('-action_time',),
'verbose_name_plural': 'log entries',
},
),
]

View file

View file

@ -5,13 +5,11 @@ from __future__ import division, absolute_import, unicode_literals
from django.conf import settings
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import signals
from django.utils.encoding import force_text
from django.utils.encoding import smart_text
from django.utils.encoding import python_2_unicode_compatible
from django.utils.encoding import smart_text
from django.utils.translation import ugettext, ugettext_lazy as _
from . import permissions
from .utils import quote
@ -99,10 +97,3 @@ class LogEntry(models.Model):
quote(self.object_id)
)
return None
# setup signal handlers here, since ``models.py`` will be imported by django
# for sure if ``djadmin2`` is listed in the ``INSTALLED_APPS``.
signals.post_syncdb.connect(
permissions.create_view_permissions,
dispatch_uid="django-admin2.djadmin2.permissions.create_view_permissions")

View file

@ -20,15 +20,14 @@ from __future__ import division, absolute_import, unicode_literals
import logging
import re
from django.contrib.auth import models as auth_models
from django.contrib.contenttypes import models as contenttypes_models
from django.db.models import get_models
from django.contrib.auth import get_permission_codename
from django.db.utils import DEFAULT_DB_ALIAS
from django.apps import apps
from django.core.exceptions import ValidationError
from django.db import router
from django.utils import six
from . import utils
from django.utils.encoding import python_2_unicode_compatible, force_text
logger = logging.getLogger('djadmin2')
@ -82,13 +81,13 @@ def model_permission(permission):
assert model_class, (
'Cannot apply model permissions on a view that does not '
'have a `.model` or `.queryset` property.')
try:
# django 1.8+
model_name = model_class._meta.model_name
except AttributeError:
model_name = model_class._meta.module_name
permission_name = permission.format(
app_label=model_class._meta.app_label,
model_name=model_name)
@ -363,14 +362,13 @@ class TemplatePermissionChecker(object):
else:
return self._view.has_permission(self._obj)
def __str__(self):
if self._view is None:
return ''
return force_text(bool(self))
def create_view_permissions(app, created_models, verbosity, **kwargs):
def create_view_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs): # noqa
"""
Create 'view' permissions for all models.
@ -378,40 +376,64 @@ def create_view_permissions(app, created_models, verbosity, **kwargs):
Since we want to support read-only views, we need to add our own
permission.
Copied from ``django.contrib.auth.management.create_permissions``.
"""
# Is there any reason for doing this import here?
Copied from ``https://github.com/django/django/blob/1.9.6/django/contrib/auth/management/__init__.py#L60``.
app_models = get_models(app)
"""
if not app_config.models_module:
return
try:
Permission = apps.get_model('auth', 'Permission')
except LookupError:
return
if not router.allow_migrate_model(using, Permission):
return
from django.contrib.contenttypes.models import ContentType
# This will hold the permissions we're looking for as
# (content_type, (codename, name))
searched_perms = list()
# The codenames and ctypes that should exist.
ctypes = set()
for klass in app_models:
ctype = contenttypes_models.ContentType.objects.get_for_model(klass)
ctypes.add(ctype)
for klass in app_config.get_models():
# Force looking up the content types in the current database
# before creating foreign keys to them.
ctype = ContentType.objects.db_manager(using).get_for_model(klass)
opts = utils.model_options(klass)
perm = ('view_%s' % opts.object_name.lower(), u'Can view %s' % opts.verbose_name_raw)
ctypes.add(ctype)
perm = (get_permission_codename('view', klass._meta), 'Can view %s' % (klass._meta.verbose_name_raw))
searched_perms.append((ctype, perm))
# 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_models.Permission.objects.filter(
all_perms = set(Permission.objects.using(using).filter(
content_type__in=ctypes,
).values_list(
"content_type", "codename"
))
perms = [
auth_models.Permission(codename=codename, name=name, content_type=ctype)
for ctype, (codename, name) in searched_perms
if (ctype.pk, codename) not in all_perms
Permission(codename=codename, name=name, content_type=ct)
for ct, (codename, name) in searched_perms
if (ct.pk, codename) not in all_perms
]
auth_models.Permission.objects.bulk_create(perms)
# Validate the permissions before bulk_creation to avoid cryptic
# database error when the verbose_name is longer than 50 characters
permission_name_max_length = Permission._meta.get_field('name').max_length
verbose_name_max_length = permission_name_max_length - 11 # len('Can change ') prefix
for perm in perms:
if len(perm.name) > permission_name_max_length:
raise ValidationError(
"The verbose_name of %s.%s is longer than %s characters" % (
perm.content_type.app_label,
perm.content_type.model,
verbose_name_max_length,
)
)
Permission.objects.using(using).bulk_create(perms)
if verbosity >= 2:
for perm in perms:
logger.info("Adding permission '%s'" % perm)
print("Adding permission '%s'" % perm)

View file

@ -9,9 +9,9 @@ import os.path
from datetime import date, time, datetime
from django.db import models
from django.template.loader import render_to_string
from django.utils import formats, timezone
from django.utils.encoding import force_text
from django.template.loader import render_to_string
from djadmin2 import settings

3
djadmin2/site.py Normal file
View file

@ -0,0 +1,3 @@
from . import core
djadmin2_site = core.Admin2()

View file

@ -0,0 +1,62 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='BigThing',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
],
),
migrations.CreateModel(
name='RendererTestModel',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('decimal', models.DecimalField(max_digits=10, decimal_places=5)),
],
),
migrations.CreateModel(
name='SmallThing',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
],
),
migrations.CreateModel(
name='TagsTestsModel',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('field1', models.CharField(max_length=23)),
('field2', models.CharField(max_length=42, verbose_name='second field')),
],
options={
'verbose_name': 'Tags Test Model',
'verbose_name_plural': 'Tags Test Models',
},
),
migrations.CreateModel(
name='Thing',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
],
),
migrations.CreateModel(
name='UtilsTestModel',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('field1', models.CharField(max_length=23)),
('field2', models.CharField(max_length=42, verbose_name='second field')),
],
options={
'verbose_name': 'Utils Test Model',
'verbose_name_plural': 'Utils Test Models',
},
),
]

View file

50
djadmin2/tests/models.py Normal file
View file

@ -0,0 +1,50 @@
from django.db import models
class Thing(models.Model):
pass
class SmallThing(models.Model):
pass
class BigThing(models.Model):
pass
class TagsTestsModel(models.Model):
field1 = models.CharField(max_length=23)
field2 = models.CharField('second field', max_length=42)
def was_published_recently(self):
return True
was_published_recently.boolean = True
was_published_recently.short_description = 'Published recently?'
class Meta:
verbose_name = "Tags Test Model"
verbose_name_plural = "Tags Test Models"
class RendererTestModel(models.Model):
decimal = models.DecimalField(decimal_places=5, max_digits=10)
class UtilsTestModel(models.Model):
field1 = models.CharField(max_length=23)
field2 = models.CharField('second field', max_length=42)
def simple_method(self):
return 42
def was_published_recently(self):
return True
was_published_recently.boolean = True
was_published_recently.short_description = 'Published recently?'
class Meta:
verbose_name = "Utils Test Model"
verbose_name_plural = "Utils Test Models"

View file

@ -1,12 +1,8 @@
from django.db import models
from django.test import TestCase
from ..core import Admin2
from ..actions import get_description
class Thing(models.Model):
pass
from .models import Thing
class TestAction(object):
@ -26,24 +22,24 @@ class ActionTest(TestCase):
self.admin2.registry[Thing].list_actions.extend([
TestAction,
test_function,
])
])
self.assertEquals(
get_description(
self.admin2.registry[Thing].list_actions[0]
),
),
'Delete selected items'
)
)
self.assertEquals(
get_description(
self.admin2.registry[Thing].list_actions[1]
),
),
'Test Action Class'
)
)
self.assertEquals(
get_description(
self.admin2.registry[Thing].list_actions[2]
),
),
'Test function'
)
)
self.admin2.registry[Thing].list_actions.remove(TestAction)
self.admin2.registry[Thing].list_actions.remove(test_function)

View file

@ -1,25 +1,10 @@
from django.db import models
from django import forms
from django.forms.formsets import formset_factory
from django.test import TestCase
from ..templatetags import admin2_tags
from ..views import IndexView
class TagsTestsModel(models.Model):
field1 = models.CharField(max_length=23)
field2 = models.CharField('second field', max_length=42)
def was_published_recently(self):
return True
was_published_recently.boolean = True
was_published_recently.short_description = 'Published recently?'
class Meta:
verbose_name = "Tags Test Model"
verbose_name_plural = "Tags Test Models"
from .models import TagsTestsModel
class TagsTestForm(forms.Form):
@ -89,7 +74,7 @@ class TagsTests(TestCase):
self.assertEquals(
admin2_tags.formset_visible_fieldlist(formset),
[u'Visible 1', u'Visible 2']
)
)
def test_verbose_name_for(self):
app_verbose_names = {

View file

@ -5,7 +5,7 @@ from django.test.client import RequestFactory
import floppyforms
import djadmin2
from djadmin2.site import djadmin2_site
from ..admin2 import UserAdmin2
@ -27,7 +27,7 @@ class UserAdminTest(TestCase):
request = self.factory.get(reverse('admin2:auth_user_create'))
request.user = self.user
model_admin = UserAdmin2(User, djadmin2.default)
model_admin = UserAdmin2(User, djadmin2_site)
view = model_admin.create_view.view.as_view(
**model_admin.get_create_kwargs())
response = view(request)
@ -48,7 +48,7 @@ class UserAdminTest(TestCase):
request = self.factory.get(
reverse('admin2:auth_user_update', args=(self.user.pk,)))
request.user = self.user
model_admin = UserAdmin2(User, djadmin2.default)
model_admin = UserAdmin2(User, djadmin2_site)
view = model_admin.update_view.view.as_view(
**model_admin.get_update_kwargs())
response = view(request, pk=self.user.pk)

View file

@ -1,17 +1,12 @@
from django.db import models
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
from django.contrib.auth.models import Group, User
from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
import djadmin2
from ..types import ModelAdmin2
from djadmin2.site import djadmin2_site
from .models import SmallThing
from ..core import Admin2
class SmallThing(models.Model):
pass
from ..types import ModelAdmin2
APP_LABEL, APP_VERBOSE_NAME = 'app_one_label', 'App One Verbose Name'
@ -71,4 +66,4 @@ class Admin2Test(TestCase):
def test_default_entries(self):
expected_default_models = (User, Group, Site)
for model in expected_default_models:
self.assertTrue(isinstance(djadmin2.default.registry[model], ModelAdmin2))
self.assertTrue(isinstance(djadmin2_site.registry[model], ModelAdmin2))

View file

@ -5,15 +5,11 @@ import datetime as dt
from decimal import Decimal
from django.test import TestCase
from django.db import models
from django.utils.translation import activate
from django.utils import six
from django.utils.translation import activate
from .. import renderers
class RendererTestModel(models.Model):
decimal = models.DecimalField(decimal_places=5, max_digits=10)
from .models import RendererTestModel
class BooleanRendererTest(TestCase):
@ -109,7 +105,7 @@ class NumberRendererTest(TestCase):
self.assertEqual('42.5', out)
def testEndlessFloat(self):
out = self.renderer(1.0/3, None)
out = self.renderer(1.0 / 3, None)
if six.PY2:
self.assertEqual('0.333333333333', out)
else:

View file

@ -1,10 +1,9 @@
from django.db import models
from django.test import TestCase
from django.views.generic import View
from .. import views
from ..types import ModelAdmin2, immutable_admin_factory
from ..core import Admin2
from .models import BigThing
class ModelAdmin(object):
@ -40,10 +39,6 @@ class ImmutableAdminFactoryTests(TestCase):
self.immutable_admin.d
class BigThing(models.Model):
pass
class ModelAdminTest(TestCase):
def setUp(self):

View file

@ -1,27 +1,9 @@
from django.db import models
from django.test import TestCase
from django.utils import six
from .. import utils
from ..views import IndexView
class UtilsTestModel(models.Model):
field1 = models.CharField(max_length=23)
field2 = models.CharField('second field', max_length=42)
def simple_method(self):
return 42
def was_published_recently(self):
return True
was_published_recently.boolean = True
was_published_recently.short_description = 'Published recently?'
class Meta:
verbose_name = "Utils Test Model"
verbose_name_plural = "Utils Test Models"
from .models import UtilsTestModel
class UtilsTest(TestCase):
@ -40,7 +22,7 @@ class UtilsTest(TestCase):
self.assertEquals(
UtilsTestModel._meta,
utils.model_options(UtilsTestModel)
)
)
UtilsTestModel._meta.verbose_name = "Utils Test Model"
UtilsTestModel._meta.verbose_name_plural = "Utils Test Models"
@ -55,7 +37,7 @@ class UtilsTest(TestCase):
self.assertEquals(
self.instance._meta,
utils.model_options(self.instance)
)
)
self.instance._meta.verbose_name = "Utils Test Model"
self.instance._meta.verbose_name_plural = "Utils Test Models"
@ -166,8 +148,6 @@ class UtilsTest(TestCase):
"str"
)
def test_get_attr(self):
class Klass(object):
attr = "value"

View file

@ -1,5 +1,4 @@
from django.test import TestCase
from django.views.generic import View
from .. import views

View file

@ -1,22 +1,21 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from collections import namedtuple
import logging
import os
import sys
from django.core.urlresolvers import reverse
from django.conf.urls import patterns, url
from django.utils.six import with_metaclass
from collections import namedtuple
import extra_views
from django.conf.urls import url
from django.core.urlresolvers import reverse
from django.utils.six import with_metaclass
from . import actions
from . import apiviews
from . import settings
from . import views
from . import actions
from . import utils
from . import views
from .forms import modelform_factory
@ -202,7 +201,8 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
def get_api_list_kwargs(self):
kwargs = self.get_default_api_view_kwargs()
kwargs.update({
'paginate_by': self.list_per_page,
'queryset': self.model.objects.all(),
# 'paginate_by': self.list_per_page,
})
return kwargs
@ -236,11 +236,10 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
name=self.get_prefixed_view_name(admin_view.name)
)
)
return patterns('', *pattern_list)
return pattern_list
def get_api_urls(self):
return patterns(
'',
return [
url(
regex=r'^$',
view=self.api_list_view.as_view(**self.get_api_list_kwargs()),
@ -252,7 +251,7 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)):
**self.get_api_detail_kwargs()),
name=self.get_prefixed_view_name('api_detail'),
),
)
]
@property
def urls(self):

View file

@ -3,13 +3,12 @@ from __future__ import division, absolute_import, unicode_literals
from collections import defaultdict
from django.db.models import ProtectedError
from django.db.models import ManyToManyRel
from django.db.models.deletion import Collector
from django.db.models.fields.related import ForeignObjectRel
from django.db.models.deletion import Collector, ProtectedError
from django.db.models.sql.constants import QUERY_TERMS
from django.utils import six
from django.utils.encoding import force_bytes, force_text
def lookup_needs_distinct(opts, lookup_path):
"""
Returns True if 'distinct()' should be used to query the given lookup path.
@ -17,13 +16,24 @@ def lookup_needs_distinct(opts, lookup_path):
This is adopted from the Django core. django-admin2 mandates that code
doesn't depend on imports from django.contrib.admin.
https://github.com/django/django/blob/1.5.1/django/contrib/admin/util.py#L20
https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L22
"""
field_name = lookup_path.split('__', 1)[0]
field = opts.get_field_by_name(field_name)[0]
condition1 = hasattr(field, 'rel') and isinstance(field.rel, ManyToManyRel)
condition2 = isinstance(field, ForeignObjectRel) and not field.field.unique
return condition1 or condition2
lookup_fields = lookup_path.split('__')
# Remove the last item of the lookup path if it is a query term
if lookup_fields[-1] in QUERY_TERMS:
lookup_fields = lookup_fields[:-1]
# Now go through the fields (following all relations) and look for an m2m
for field_name in lookup_fields:
field = opts.get_field(field_name)
if hasattr(field, 'get_path_info'):
# This field is a relation, update opts to follow the relation
path_info = field.get_path_info()
opts = path_info[-1].to_opts
if any(path.m2m for path in path_info):
# This field is a m2m relation so we know we need to call distinct
return True
return False
def model_options(model):
@ -91,10 +101,8 @@ def get_attr(obj, attr):
and the __str__ attribute.
"""
if attr == '__str__':
if six.PY2:
value = unicode(obj)
else:
value = str(obj)
from builtins import str as text
value = text(obj)
else:
attribute = getattr(obj, attr)
value = attribute() if callable(attribute) else attribute
@ -106,15 +114,14 @@ class NestedObjects(Collector):
This is adopted from the Django core. django-admin2 mandates that code
doesn't depend on imports from django.contrib.admin.
https://github.com/django/django/blob/1.8c1/django/contrib/admin/utils.py#L160-L221
https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L171-L231
"""
def __init__(self, *args, **kwargs):
super(NestedObjects, self).__init__(*args, **kwargs)
self.edges = {} # {from_instance: [to_instances]}
self.protected = set()
self.model_count = defaultdict(int)
self.model_objs = defaultdict(set)
def add_edge(self, source, target):
self.edges.setdefault(source, []).append(target)
@ -129,7 +136,7 @@ class NestedObjects(Collector):
self.add_edge(getattr(obj, related_name), obj)
else:
self.add_edge(None, obj)
self.model_count[obj._meta.verbose_name_plural] += 1
self.model_objs[obj._meta.model].add(obj)
try:
return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
except ProtectedError as e:
@ -157,7 +164,6 @@ class NestedObjects(Collector):
def nested(self, format_callback=None):
"""
Return the graph as a nested list.
"""
seen = set()
roots = []
@ -183,14 +189,14 @@ def quote(s):
This is adopted from the Django core. django-admin2 mandates that code
doesn't depend on imports from django.contrib.admin.
https://github.com/django/django/blob/1.5.1/django/contrib/admin/util.py#L48-L62
https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L66-L73
"""
if not isinstance(s, six.string_types):
return s
res = list(s)
for i in range(len(res)):
c = res[i]
if c in """:/_#?;@&=+$,"<>%\\""":
if c in """:/_#?;@&=+$,"[]<>%\n\\""":
res[i] = '_%02X' % ord(c)
return ''.join(res)
@ -199,4 +205,4 @@ def type_str(text):
if six.PY2:
return force_bytes(text)
else:
return force_text(text)
return force_text(text)

View file

@ -116,11 +116,11 @@ class Admin2Mixin(PermissionMixin):
return super(Admin2Mixin, self).dispatch(request, *args, **kwargs)
class AdminModel2Mixin(Admin2Mixin):
class Admin2ModelMixin(Admin2Mixin):
model_admin = None
def get_context_data(self, **kwargs):
context = super(AdminModel2Mixin, self).get_context_data(**kwargs)
context = super(Admin2ModelMixin, self).get_context_data(**kwargs)
model = self.get_model()
model_meta = model_options(model)
app_verbose_names = self.model_admin.admin.app_verbose_names

View file

@ -2,11 +2,12 @@
from __future__ import division, absolute_import, unicode_literals
import operator
from datetime import datetime
from functools import reduce
from datetime import datetime
import extra_views
from django.conf import settings
from django.contrib.auth import update_session_auth_hash
from django.contrib.auth.forms import (PasswordChangeForm,
AdminPasswordChangeForm)
from django.contrib.auth.views import (logout as auth_logout,
@ -21,14 +22,13 @@ from django.utils.encoding import force_text
from django.utils.text import capfirst
from django.utils.translation import ugettext_lazy
from django.views import generic
import extra_views
from . import permissions, utils
from .forms import AdminAuthenticationForm
from .viewmixins import Admin2Mixin, AdminModel2Mixin, Admin2ModelFormMixin
from .filters import build_list_filter, build_date_filter
from .forms import AdminAuthenticationForm
from .models import LogEntry
from .viewmixins import Admin2Mixin, Admin2ModelMixin, Admin2ModelFormMixin
class AdminView(object):
@ -101,7 +101,7 @@ class AppIndexView(Admin2Mixin, generic.TemplateView):
return data
class ModelListView(AdminModel2Mixin, generic.ListView):
class ModelListView(Admin2ModelMixin, generic.ListView):
"""Context Variables
:is_paginated: If the page is paginated (page has a next button)
@ -186,7 +186,7 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
queryset = self.build_list_filter(queryset).qs
if self.model_admin.date_hierarchy:
queryset = self.build_date_filter(queryset).qs
queryset = self.build_date_filter(queryset, self.model_admin.date_hierarchy).qs
queryset = self._modify_queryset_for_sort(queryset)
@ -233,7 +233,7 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
)
return self._list_filter
def build_date_filter(self, queryset=None):
def build_date_filter(self, queryset=None, field_name=None):
if not hasattr(self, "_date_filter"):
if queryset is None:
queryset = self.get_queryset()
@ -241,6 +241,7 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
self.request,
self.model_admin,
queryset,
field_name
)
return self._date_filter
@ -271,38 +272,38 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
context["active_day"] = new_date.strftime("%B %d")
context["dates"] = self._format_days(context)
context["dates"] = self._format_days(self.get_queryset())
elif year and month:
context["previous_date"] = {
"link": "?year=%s" % (year),
"text": " %s" % year,
}
context["dates"] = self._format_days(context)
context["dates"] = self._format_days(self.get_queryset())
elif year:
context["previous_date"] = {
"link": "?",
"text": ugettext_lazy(" All dates"),
}
context["dates"] = self._format_months(context)
context["dates"] = self._format_months(self.get_queryset())
else:
context["dates"] = self._format_years(context)
context["dates"] = self._format_years(self.get_queryset())
return context
def _format_years(self, context):
years = self._qs_date_or_datetime(context['object_list'], 'year')
def _format_years(self, queryset):
years = self._qs_date_or_datetime(queryset, 'year')
if len(years) == 1:
return self._format_months(context)
return self._format_months(queryset)
else:
return [
(("?year=%s" % year.strftime("%Y")), year.strftime("%Y"))
for year in
self._qs_date_or_datetime(context['object_list'], 'year')
self._qs_date_or_datetime(queryset, 'year')
]
def _format_months(self, context):
def _format_months(self, queryset):
return [
(
"?year=%s&month=%s" % (
@ -310,10 +311,10 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
),
date.strftime("%B %Y")
) for date in
self._qs_date_or_datetime(context['object_list'], 'month')
self._qs_date_or_datetime(queryset, 'month')
]
def _format_days(self, context):
def _format_days(self, queryset):
return [
(
"?year=%s&month=%s&day=%s" % (
@ -323,7 +324,7 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
),
date.strftime("%B %d")
) for date in
self._qs_date_or_datetime(context['object_list'], 'day')
self._qs_date_or_datetime(queryset, 'day')
]
def _qs_date_or_datetime(self, object_list, type):
@ -345,7 +346,7 @@ class ModelListView(AdminModel2Mixin, generic.ListView):
return self.model_admin.search_fields
class ModelDetailView(AdminModel2Mixin, generic.DetailView):
class ModelDetailView(Admin2ModelMixin, generic.DetailView):
"""Context Variables
:model: Type of object you are editing
@ -363,7 +364,7 @@ class ModelDetailView(AdminModel2Mixin, generic.DetailView):
permissions.ModelViewPermission)
class ModelEditFormView(AdminModel2Mixin, Admin2ModelFormMixin,
class ModelEditFormView(Admin2ModelMixin, Admin2ModelFormMixin,
extra_views.UpdateWithInlinesView):
"""Context Variables
@ -399,7 +400,7 @@ class ModelEditFormView(AdminModel2Mixin, Admin2ModelFormMixin,
return response
class ModelAddFormView(AdminModel2Mixin, Admin2ModelFormMixin,
class ModelAddFormView(Admin2ModelMixin, Admin2ModelFormMixin,
extra_views.CreateWithInlinesView):
"""Context Variables
@ -435,7 +436,7 @@ class ModelAddFormView(AdminModel2Mixin, Admin2ModelFormMixin,
return response
class ModelDeleteView(AdminModel2Mixin, generic.DeleteView):
class ModelDeleteView(Admin2ModelMixin, generic.DeleteView):
"""Context Variables
:model: Type of object you are editing
@ -461,7 +462,7 @@ class ModelDeleteView(AdminModel2Mixin, generic.DeleteView):
opts = utils.model_options(obj)
return '%s: %s' % (force_text(capfirst(opts.verbose_name)),
force_text(obj))
using = router.db_for_write(self.get_object()._meta.model)
collector = utils.NestedObjects(using=using)
collector.collect([self.get_object()])
@ -479,7 +480,7 @@ class ModelDeleteView(AdminModel2Mixin, generic.DeleteView):
return super(ModelDeleteView, self).delete(request, *args, **kwargs)
class ModelHistoryView(AdminModel2Mixin, generic.ListView):
class ModelHistoryView(Admin2ModelMixin, generic.ListView):
"""Context Variables
:model: Type of object you are editing
@ -541,6 +542,13 @@ class PasswordChangeView(Admin2Mixin, generic.UpdateView):
from django.contrib.auth import get_user_model
return get_user_model()._default_manager.all()
def form_valid(self, form):
self.object = form.save()
if self.request.user == self.get_object():
update_session_auth_hash(self.request, form.user)
return HttpResponseRedirect(self.get_success_url())
class PasswordChangeDoneView(Admin2Mixin, generic.TemplateView):
default_template_name = 'auth/password_change_done.html'

View file

@ -33,17 +33,17 @@ Add djadmin2 urls to your URLconf:
.. code-block:: python
# urls.py
from django.conf.urls import patterns, include
from django.conf.urls import include
import djadmin2
djadmin2.default.autodiscover()
urlpatterns = patterns(
urlpatterns = [
...
url(r'^admin2/', include(djadmin2.default.urls)),
)
]
Development Installation
=========================

View file

@ -8,7 +8,7 @@ How To Create a Theme
A Django Admin 2 theme is merely a packaged Django app. Here are the necessary steps to create a theme called '*dandy*':
1. Make sure you have Django 1.5 or higher installed.
1. Make sure you have Django 1.8 or higher installed.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
.. code-block:: python

View file

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy, pgettext_lazy
from django.contrib import messages
from django.utils.translation import ugettext_lazy, pgettext_lazy
from djadmin2.actions import BaseListAction
from djadmin2 import permissions
from djadmin2.actions import BaseListAction

View file

@ -3,21 +3,22 @@ from __future__ import division, absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy
import djadmin2
from djadmin2 import renderers
from djadmin2.actions import DeleteSelectedAction
# Import your custom models
from djadmin2.site import djadmin2_site
from djadmin2.types import Admin2TabularInline, ModelAdmin2
from .actions import (CustomPublishAction, PublishAllItemsAction,
unpublish_items, unpublish_all_items)
from .models import Post, Comment
class CommentInline(djadmin2.Admin2TabularInline):
class CommentInline(Admin2TabularInline):
model = Comment
class PostAdmin(djadmin2.ModelAdmin2):
class PostAdmin(ModelAdmin2):
list_actions = [
DeleteSelectedAction, CustomPublishAction,
PublishAllItemsAction, unpublish_items,
@ -34,7 +35,7 @@ class PostAdmin(djadmin2.ModelAdmin2):
ordering = ["-published_date", "title",]
class CommentAdmin(djadmin2.ModelAdmin2):
class CommentAdmin(ModelAdmin2):
search_fields = ('body', '=post__title')
list_filter = ['post', ]
actions_on_top = True
@ -42,12 +43,12 @@ class CommentAdmin(djadmin2.ModelAdmin2):
actions_selection_counter = False
# Register the blog app with a verbose name
djadmin2.default.register_app_verbose_name(
djadmin2_site.register_app_verbose_name(
'blog',
ugettext_lazy('My Blog')
)
# Register each model with the admin
djadmin2.default.register(Post, PostAdmin)
djadmin2.default.register(Comment, CommentAdmin)
djadmin2_site.register(Post, PostAdmin)
djadmin2_site.register(Comment, CommentAdmin)

View file

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Comment',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('body', models.TextField(verbose_name='body')),
],
options={
'verbose_name': 'comment',
'verbose_name_plural': 'comments',
},
),
migrations.CreateModel(
name='Count',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('num', models.PositiveSmallIntegerField()),
('parent', models.ForeignKey(to='blog.Count', null=True)),
],
),
migrations.CreateModel(
name='Event',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('date', models.DateTimeField(auto_now_add=True)),
],
),
migrations.CreateModel(
name='EventGuide',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('event', models.ForeignKey(to='blog.Event', on_delete=django.db.models.deletion.DO_NOTHING)),
],
),
migrations.CreateModel(
name='Guest',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('name', models.CharField(max_length=255)),
('event', models.OneToOneField(to='blog.Event')),
],
options={
'verbose_name': 'awesome guest',
},
),
migrations.CreateModel(
name='Location',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('event', models.OneToOneField(verbose_name='awesome event', to='blog.Event')),
],
),
migrations.CreateModel(
name='Post',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('title', models.CharField(max_length=255, verbose_name='title')),
('body', models.TextField(verbose_name='body')),
('published', models.BooleanField(verbose_name='published', default=False)),
('published_date', models.DateField(blank=True, null=True)),
],
options={
'verbose_name': 'post',
'verbose_name_plural': 'posts',
},
),
migrations.AddField(
model_name='comment',
name='post',
field=models.ForeignKey(related_name='comments', verbose_name='post', to='blog.Post'),
),
]

View file

View file

@ -1,4 +1,4 @@
{% load i18n static %}<!DOCTYPE html>
{% load i18n staticfiles %}<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
@ -7,8 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Bootstrap -->
{% block css %}
<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">
<link href="{% static "themes/bootstrap/css/bootstrap.min.css" %}" rel="stylesheet" media="screen">
<link href="{% static "themes/bootstrap/css/bootstrap-custom.css" %}" rel="stylesheet" media="screen">
{% endblock css %}
</head>
<body>
@ -19,8 +19,8 @@
{% block javascript %}
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script>
<script>window.jQuery || document.write('<script src="{{ STATIC_URL }}themes/bootstrap/js/jquery.min.js">\x3C/script>')</script>
<script src="{{ STATIC_URL }}themes/bootstrap/js/bootstrap.min.js"></script>
<script>window.jQuery || document.write('<script src="{% static "themes/bootstrap/js/jquery.min.js" %}">\x3C/script>')</script>
<script src="{% static "themes/bootstrap/js/bootstrap.min.js" %}"></script>
{% endblock javascript %}
</body>
</html>

View file

@ -1,16 +1,17 @@
from __future__ import unicode_literals
import json
from django.contrib.auth.models import AnonymousUser, User
from django.core.exceptions import PermissionDenied
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import RequestFactory
from django.utils.encoding import force_text
import json
from djadmin2 import apiviews
from djadmin2 import default
from djadmin2 import ModelAdmin2
from djadmin2.site import djadmin2_site
from djadmin2.types import ModelAdmin2
from ..models import Post
@ -24,21 +25,21 @@ class APITestCase(TestCase):
self.user.save()
def get_model_admin(self, model):
return ModelAdmin2(model, default)
return ModelAdmin2(model, djadmin2_site)
class IndexAPIViewTest(APITestCase):
def test_response_ok(self):
request = self.factory.get(reverse('admin2:api_index'))
request.user = self.user
view = apiviews.IndexAPIView.as_view(**default.get_api_index_kwargs())
view = apiviews.IndexAPIView.as_view(**djadmin2_site.get_api_index_kwargs())
response = view(request)
self.assertEqual(response.status_code, 200)
def test_view_permission(self):
request = self.factory.get(reverse('admin2:api_index'))
request.user = AnonymousUser()
view = apiviews.IndexAPIView.as_view(**default.get_api_index_kwargs())
view = apiviews.IndexAPIView.as_view(**djadmin2_site.get_api_index_kwargs())
self.assertRaises(PermissionDenied, view, request)
@ -71,7 +72,7 @@ class ListCreateAPIViewTest(APITestCase):
response.render()
self.assertEqual(response.status_code, 200)
self.assertIn('"__unicode__": "Foo"', force_text(response.content))
self.assertIn('"__unicode__":"Foo"', force_text(response.content))
def test_pagination(self):
request = self.factory.get(reverse('admin2:blog_post_api_list'))

View file

@ -1,16 +1,15 @@
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
import django_filters
from django.core.urlresolvers import reverse
from django.test import TestCase
from django.test.client import RequestFactory
from django.core.urlresolvers import reverse
from djadmin2 import filters as djadmin2_filters
from djadmin2.types import ModelAdmin2
from ..models import Post
import djadmin2
import djadmin2.filters as djadmin2_filters
import django_filters
class ListFilterBuilderTest(TestCase):
@ -18,11 +17,11 @@ class ListFilterBuilderTest(TestCase):
self.rf = RequestFactory()
def test_filter_building(self):
class PostAdminSimple(djadmin2.ModelAdmin2):
class PostAdminSimple(ModelAdmin2):
list_filter = ['published', ]
class PostAdminWithFilterInstances(djadmin2.ModelAdmin2):
class PostAdminWithFilterInstances(ModelAdmin2):
list_filter = [
django_filters.BooleanFilter(name='published'),
]
@ -33,7 +32,7 @@ class ListFilterBuilderTest(TestCase):
fields = ['published']
class PostAdminWithFilterSetInst(djadmin2.ModelAdmin2):
class PostAdminWithFilterSetInst(ModelAdmin2):
list_filter = FS
Post.objects.create(title="post_1_title", body="body")

View file

@ -1,10 +1,9 @@
from __future__ import unicode_literals
import floppyforms
from django import forms
from django.test import TestCase
import floppyforms
from djadmin2.forms import floppify_widget, floppify_form, modelform_factory
from ..models import Post
@ -103,7 +102,6 @@ class GetFloppyformWidgetTest(TestCase):
floppyforms.widgets.HiddenInput)
widget = forms.widgets.HiddenInput()
widget.is_hidden = False
self.assertExpectWidget(
widget,
floppyforms.widgets.HiddenInput,
@ -162,7 +160,7 @@ class GetFloppyformWidgetTest(TestCase):
forms.DateInput(),
floppyforms.DateInput)
widget = forms.widgets.DateInput(format='DATE_FORMAT')
widget = forms.widgets.DateInput(format='%Y-%m-%d')
self.assertExpectWidget(
widget,
floppyforms.widgets.DateInput,
@ -311,13 +309,13 @@ class GetFloppyformWidgetTest(TestCase):
floppyforms.widgets.SplitDateTimeWidget)
widget = forms.widgets.SplitDateTimeWidget(
date_format='DATE_FORMAT', time_format='TIME_FORMAT')
date_format='%Y-%m-%d', time_format='TIME_FORMAT')
new_widget = floppify_widget(widget)
self.assertTrue(isinstance(
new_widget.widgets[0], floppyforms.widgets.DateInput))
self.assertTrue(isinstance(
new_widget.widgets[1], floppyforms.widgets.TimeInput))
self.assertEqual(new_widget.widgets[0].format, 'DATE_FORMAT')
self.assertEqual(new_widget.widgets[0].format, '%Y-%m-%d')
self.assertEqual(new_widget.widgets[1].format, 'TIME_FORMAT')
def test_splithiddendatetime_widget(self):
@ -327,13 +325,13 @@ class GetFloppyformWidgetTest(TestCase):
floppyforms.widgets.SplitHiddenDateTimeWidget)
widget = forms.widgets.SplitHiddenDateTimeWidget(
date_format='DATE_FORMAT', time_format='TIME_FORMAT')
date_format='%Y-%m-%d', time_format='TIME_FORMAT')
new_widget = floppify_widget(widget)
self.assertTrue(isinstance(
new_widget.widgets[0], floppyforms.widgets.DateInput))
self.assertTrue(isinstance(
new_widget.widgets[1], floppyforms.widgets.TimeInput))
self.assertEqual(new_widget.widgets[0].format, 'DATE_FORMAT')
self.assertEqual(new_widget.widgets[0].format, '%Y-%m-%d')
self.assertEqual(new_widget.widgets[0].is_hidden, True)
self.assertEqual(new_widget.widgets[1].format, 'TIME_FORMAT')
self.assertEqual(new_widget.widgets[1].is_hidden, True)
@ -489,13 +487,13 @@ class FieldWidgetTest(TestCase):
self.assertTrue(isinstance(widget, floppyforms.widgets.SlugInput))
self.assertEqual(widget.input_type, 'text')
def test_ipaddress_field(self):
def test_genericipaddress_field(self):
class MyForm(forms.ModelForm):
ipaddress = forms.IPAddressField()
ipaddress = forms.GenericIPAddressField()
form_class = modelform_factory(model=Post, form=MyForm, exclude=[])
widget = form_class().fields['ipaddress'].widget
self.assertTrue(isinstance(widget, floppyforms.widgets.IPAddressInput))
self.assertTrue(isinstance(widget, floppyforms.widgets.TextInput))
self.assertEqual(widget.input_type, 'text')
def test_splitdatetime_field(self):

View file

@ -1,9 +1,8 @@
from django.db import DEFAULT_DB_ALIAS
from django.db import DEFAULT_DB_ALIAS, router
from django.test import TestCase
from djadmin2.utils import NestedObjects
from ..models import Count, Event, EventGuide, Guest, Location
from ..models import Count, Event, EventGuide
class NestedObjectsTests(TestCase):
@ -66,7 +65,8 @@ class NestedObjectsTests(TestCase):
Check that the nested collector doesn't query for DO_NOTHING objects.
"""
objs = [Event.objects.create()]
n = NestedObjects(using=None)
using = router.db_for_write(Event._meta.model)
n = NestedObjects(using=using)
EventGuide.objects.create(event=objs[0])
with self.assertNumQueries(2):
# One for Location, one for Guest, and no query for EventGuide

View file

@ -1,14 +1,14 @@
from blog.models import Post
from django.contrib.auth.models import User, Permission
from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
from django.template import Template, Context
from django.test import TestCase
from django.test.client import RequestFactory
import djadmin2
from djadmin2 import ModelAdmin2
from djadmin2.permissions import TemplatePermissionChecker
from blog.models import Post
from djadmin2.site import djadmin2_site
from djadmin2.types import ModelAdmin2
class TemplatePermissionTest(TestCase):
@ -26,7 +26,7 @@ class TemplatePermissionTest(TestCase):
return template.render(context)
def test_permission_wrapper(self):
model_admin = ModelAdmin2(Post, djadmin2.default)
model_admin = ModelAdmin2(Post, djadmin2_site)
request = self.factory.get(reverse('admin2:blog_post_index'))
request.user = self.user
permissions = TemplatePermissionChecker(request, model_admin)
@ -48,8 +48,8 @@ class TemplatePermissionTest(TestCase):
codename='add_post')
self.user.user_permissions.add(post_add_permission)
# invalidate the users permission cache
if hasattr(self.user, '_perm_cache'):
del self.user._perm_cache
self.user = get_object_or_404(User, pk=self.user.id)
request.user = self.user
result = self.render('{{ permissions.has_add_permission }}', context)
self.assertEqual(result, 'True')
@ -61,7 +61,7 @@ class TemplatePermissionTest(TestCase):
codename='add_post')
self.user.user_permissions.add(post_add_permission)
model_admin = ModelAdmin2(Post, djadmin2.default)
model_admin = ModelAdmin2(Post, djadmin2_site)
request = self.factory.get(reverse('admin2:blog_post_index'))
request.user = self.user
permissions = TemplatePermissionChecker(request, model_admin)
@ -89,8 +89,8 @@ class TemplatePermissionTest(TestCase):
self.assertEqual(result, '')
def test_admin_binding(self):
user_admin = djadmin2.default.get_admin_by_name('auth_user')
post_admin = djadmin2.default.get_admin_by_name('blog_post')
user_admin = djadmin2_site.get_admin_by_name('auth_user')
post_admin = djadmin2_site.get_admin_by_name('blog_post')
request = self.factory.get(reverse('admin2:auth_user_index'))
request.user = self.user
permissions = TemplatePermissionChecker(request, user_admin)
@ -121,10 +121,11 @@ class TemplatePermissionTest(TestCase):
content_type__app_label='blog',
content_type__model='post',
codename='add_post')
self.user.user_permissions.add(post_add_permission)
# invalidate the users permission cache
if hasattr(self.user, '_perm_cache'):
del self.user._perm_cache
self.user = get_object_or_404(User, pk=self.user.id)
request.user = self.user
result = self.render(
'{% load admin2_tags %}'
@ -155,8 +156,8 @@ class TemplatePermissionTest(TestCase):
self.assertEqual(result, '')
def test_view_binding(self):
user_admin = djadmin2.default.get_admin_by_name('auth_user')
post_admin = djadmin2.default.get_admin_by_name('blog_post')
user_admin = djadmin2_site.get_admin_by_name('auth_user')
post_admin = djadmin2_site.get_admin_by_name('blog_post')
request = self.factory.get(reverse('admin2:auth_user_index'))
request.user = self.user
permissions = TemplatePermissionChecker(request, user_admin)
@ -203,8 +204,8 @@ class TemplatePermissionTest(TestCase):
self.user.user_permissions.add(user_change_permission)
# invalidate the users permission cache
if hasattr(self.user, '_perm_cache'):
del self.user._perm_cache
self.user = get_object_or_404(User, pk=self.user.id)
request.user = self.user
result = self.render(
'{% load admin2_tags %}'
@ -235,7 +236,7 @@ class TemplatePermissionTest(TestCase):
self.assertEqual(result, '1True2False34True')
def test_object_level_permission(self):
model_admin = ModelAdmin2(Post, djadmin2.default)
model_admin = ModelAdmin2(Post, djadmin2_site)
request = self.factory.get(reverse('admin2:blog_post_index'))
request.user = self.user
permissions = TemplatePermissionChecker(request, model_admin)
@ -264,8 +265,8 @@ class TemplatePermissionTest(TestCase):
codename='add_post')
self.user.user_permissions.add(post_add_permission)
# invalidate the users permission cache
if hasattr(self.user, '_perm_cache'):
del self.user._perm_cache
self.user = get_object_or_404(User, pk=self.user.id)
request.user = self.user
# object level permission are not supported by default. So this will
# return ``False``.

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from datetime import datetime
from django.contrib.auth import get_user_model
@ -515,18 +516,19 @@ class TestAuthViews(TestCase):
def test_change_password_for_myself(self):
self.client.login(username=self.user.username,
password='password')
request = self.client.post(reverse('admin2:password_change',
kwargs={'pk': self.user.pk}),
{'old_password': 'password',
'new_password1': 'user',
'new_password2': 'user'})
'new_password1': 'new_password',
'new_password2': 'new_password'})
self.assertRedirects(request, reverse('admin2:password_change_done'))
self.client.logout()
self.assertFalse(self.client.login(username=self.user.username,
password='password'))
self.assertTrue(self.client.login(username=self.user.username,
password='user'))
password='new_password'))
def test_change_password(self):
self.client.login(username=self.user.username,
@ -538,8 +540,7 @@ class TestAuthViews(TestCase):
request = self.client.post(reverse('admin2:password_change',
kwargs={'pk': new_user.pk}),
{'old_password': 'new_user',
'password1': 'new_user_password',
{'password1': 'new_user_password',
'password2': 'new_user_password'})
self.assertRedirects(request, reverse('admin2:password_change_done'))
self.client.logout()

BIN
example/db.sqlite3 Normal file

Binary file not shown.

View file

@ -1,68 +1,137 @@
# Django settings for example project.
"""
Django settings for example project.
Generated by 'django-admin startproject' using Django 1.9.6.
For more information on this file, see
https://docs.djangoproject.com/en/1.9/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.9/ref/settings/
"""
import os
BASE_DIR = os.path.dirname(os.path.dirname(__file__))
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '*ymubzn8p_s7vrm%jsqvr6$qnea_5mcp(ao0z-yh1q0gro!0g1'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = (
# ('Your Name', 'your_email@example.com'),
)
ALLOWED_HOSTS = []
MANAGERS = ADMINS
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'floppyforms',
'rest_framework',
'crispy_forms',
'djadmin2',
'djadmin2.tests',
'djadmin2.themes.djadmin2theme_default',
'blog',
'files',
'polls'
]
MIDDLEWARE_CLASSES = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'example.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'example.wsgi.application'
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': 'example.db',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
}
}
# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
ALLOWED_HOSTS = []
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = 'America/Chicago'
# Password validation
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale.
USE_L10N = True
# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/var/www/example.com/media/"
MEDIA_ROOT = ''
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://example.com/media/", "http://media.example.com/"
MEDIA_URL = ''
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/var/www/example.com/static/"
STATIC_ROOT = ''
# URL prefix for static files.
# Example: "http://example.com/static/", "http://static.example.com/"
STATIC_URL = '/static/'
# Additional locations of static files
@ -73,94 +142,12 @@ STATICFILES_DIRS = (
os.path.join(BASE_DIR, "static"),
)
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = "/media/"
# Make this unique, and don't share it with anybody.
SECRET_KEY = '*ymubzn8p_s7vrm%jsqvr6$qnea_5mcp(ao0z-yh1q0gro!0g1'
ADMIN2_THEME_DIRECTORY = "djadmin2theme_default"
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
# 'django.template.loaders.eggs.Loader',
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
# Uncomment the next line for simple clickjacking protection:
# 'django.middleware.clickjacking.XFrameOptionsMiddleware',
)
ROOT_URLCONF = 'example.urls'
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'example.wsgi.application'
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'django.contrib.admin',
# Uncomment the next line to enable admin documentation:
# 'django.contrib.admindocs',
'floppyforms',
'rest_framework',
'crispy_forms',
'djadmin2',
'djadmin2.themes.djadmin2theme_default',
'blog',
'files',
'polls'
)
# A sample logging configuration. The only tangible logging
# performed by this configuration is to send an email to
# the site admins on every HTTP 500 error when DEBUG=False.
# See http://docs.djangoproject.com/en/dev/topics/logging for
# more details on how to customize your logging configuration.
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'filters': {
'require_debug_false': {
'()': 'django.utils.log.RequireDebugFalse'
}
},
'handlers': {
'mail_admins': {
'level': 'ERROR',
'filters': ['require_debug_false'],
'class': 'django.utils.log.AdminEmailHandler'
}
},
'loggers': {
'django.request': {
'handlers': ['mail_admins'],
'level': 'ERROR',
'propagate': True,
},
}
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'PAGE_SIZE': 10
}
ADMIN2_THEME_DIRECTORY = "djadmin2theme_default"

View file

@ -1,18 +1,21 @@
from django.conf.urls import patterns, include, url
from django.contrib import admin
from __future__ import unicode_literals
from blog.views import BlogListView, BlogDetailView
from django.conf import settings
from django.conf.urls import include, url
from django.conf.urls.static import static
from django.contrib import admin
from djadmin2.site import djadmin2_site
admin.autodiscover()
djadmin2_site.autodiscover()
import djadmin2
djadmin2.default.autodiscover()
urlpatterns = patterns('',
url(r'^admin2/', include(djadmin2.default.urls)),
urlpatterns = [
url(r'^admin2/', include(djadmin2_site.urls)),
url(r'^admin/', include(admin.site.urls)),
url(r'^blog/', BlogListView.as_view(template_name="blog/blog_list.html"), name='blog_list'),
url(r'^blog/detail(?P<pk>\d+)/$', BlogDetailView.as_view(template_name="blog/blog_detail.html"), name='blog_detail'),
url(r'^$', BlogListView.as_view(template_name="blog/home.html"), name='home'),
)
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

View file

@ -1,32 +1,16 @@
"""
WSGI config for example project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
"""
import os
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use
# os.environ["DJANGO_SETTINGS_MODULE"] = "example.settings"
from django.core.wsgi import get_wsgi_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)

View file

@ -1,10 +1,9 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
import djadmin2
from djadmin2.site import djadmin2_site
from .models import CaptionedFile, UncaptionedFile
djadmin2.default.register(CaptionedFile)
djadmin2.default.register(UncaptionedFile)
djadmin2_site.register(CaptionedFile)
djadmin2_site.register(UncaptionedFile)

View file

@ -0,0 +1,36 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='CaptionedFile',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('caption', models.CharField(max_length=200, verbose_name='caption')),
('publication', models.FileField(verbose_name='Uploaded File', upload_to='captioned-files')),
],
options={
'verbose_name': 'Captioned File',
'verbose_name_plural': 'Captioned Files',
},
),
migrations.CreateModel(
name='UncaptionedFile',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('publication', models.FileField(verbose_name='Uploaded File', upload_to='uncaptioned-files')),
],
options={
'verbose_name': 'Uncaptioned File',
'verbose_name_plural': 'Uncaptioned Files',
},
),
]

View file

View file

@ -9,8 +9,7 @@ from django.utils.translation import ugettext_lazy as _
@python_2_unicode_compatible
class CaptionedFile(models.Model):
caption = models.CharField(max_length=200, verbose_name=_('caption'))
publication = models.FileField(
upload_to='media', verbose_name=_('Uploaded File'))
publication = models.FileField(upload_to='captioned-files', verbose_name=_('Uploaded File'))
def __str__(self):
return self.caption
@ -22,11 +21,10 @@ class CaptionedFile(models.Model):
@python_2_unicode_compatible
class UncaptionedFile(models.Model):
publication = models.FileField(
upload_to='media', verbose_name=_('Uploaded File'))
publication = models.FileField(upload_to='uncaptioned-files', verbose_name=_('Uploaded File'))
def __str__(self):
return self.publication
return self.publication.name
class Meta:
verbose_name = _('Uncaptioned File')

View file

@ -1,12 +1,8 @@
from django.test import TestCase
from django.utils import timezone
from files.models import CaptionedFile
from files.models import UncaptionedFile
from os import path
from django.test import TestCase
from files.models import CaptionedFile
fixture_dir = path.join(path.abspath(path.dirname(__file__)), 'fixtures')

View file

@ -1,13 +1,12 @@
from os import path
from django.contrib.auth import get_user_model
from django.core.urlresolvers import reverse
from django.test import TestCase, Client
from django.utils import timezone
from django.utils.encoding import force_text
from ..models import CaptionedFile
from os import path
fixture_dir = path.join(path.abspath(path.dirname(__file__)), 'fixtures')
fixture_file = path.join(fixture_dir, 'pubtest.txt')

View file

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals
import djadmin2
from djadmin2.site import djadmin2_site
from djadmin2.types import Admin2TabularInline, ModelAdmin2
from .models import Poll, Choice
class ChoiceInline(djadmin2.Admin2TabularInline):
class ChoiceInline(Admin2TabularInline):
model = Choice
extra = 3
class PollAdmin(djadmin2.ModelAdmin2):
class PollAdmin(ModelAdmin2):
fieldsets = [
(None, {'fields': ['question']}),
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
@ -23,4 +23,4 @@ class PollAdmin(djadmin2.ModelAdmin2):
date_hierarchy = 'pub_date'
djadmin2.default.register(Poll, PollAdmin)
djadmin2_site.register(Poll, PollAdmin)

View file

@ -0,0 +1,42 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='Choice',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('choice_text', models.CharField(max_length=200, verbose_name='choice text')),
('votes', models.IntegerField(verbose_name='votes', default=0)),
],
options={
'verbose_name': 'choice',
'verbose_name_plural': 'choices',
},
),
migrations.CreateModel(
name='Poll',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', auto_created=True, primary_key=True)),
('question', models.CharField(max_length=200, verbose_name='question')),
('pub_date', models.DateTimeField(verbose_name='date published')),
],
options={
'verbose_name': 'poll',
'verbose_name_plural': 'polls',
},
),
migrations.AddField(
model_name='choice',
name='poll',
field=models.ForeignKey(verbose_name='poll', to='polls.Poll'),
),
]

View file

View file

@ -3,9 +3,9 @@ from __future__ import division, absolute_import, unicode_literals
import datetime
from django.utils.encoding import python_2_unicode_compatible
from django.db import models
from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _

View file

@ -1,8 +1,7 @@
from django.test import TestCase
from django.utils import timezone
from polls.models import Poll
from polls.models import Choice
from polls.models import Poll
class PollTestCase(TestCase):

View file

@ -1,8 +1,9 @@
django-extra-views>=0.6.5
django-braces>=1.3.0
djangorestframework<=2.4.4
django-floppyforms<=1.2
django-filter<0.12.0
djangorestframework>=3.3.3
django-floppyforms>=1.6.2
django-filter>=0.13.0
django-crispy-forms>=1.3.2
django-debug-toolbar>=0.9.4
pytz==2014.7
future>=0.15.2
pytz==2016.4

View file

@ -1,3 +1,4 @@
-rrequirements.txt
flake8==2.5.4
pytest
pytest-django

View file

@ -127,14 +127,15 @@ setup(
include_package_data=True,
#test_suite='runtests.runtests',
install_requires=[
'django>=1.6.0',
'django>=1.8.0',
'django-extra-views>=0.6.5',
'django-braces>=1.3.0',
'djangorestframework<=2.4.4',
'django-floppyforms<=1.2',
'django-filter<0.12.0',
'djangorestframework>=3.3.3',
'django-floppyforms>=1.6.2',
'django-filter>=0.13.0',
'django-crispy-forms>=1.3.2',
'pytz==2014.7'
'pytz==2014.7',
'future>=0.15.2',
],
extras_require={
'testing': ['pytest', 'pytest-django', 'pytest-ipdb'],

151
tox.ini
View file

@ -1,138 +1,27 @@
[flake8]
ignore = E265,E501
max-line-length = 100
max-complexity = 10
exclude = migrations/*,docs/*
[tox]
envlist = py27-dj1.6.x, py33-dj1.6.x, py34-dj1.6.x, pypy-dj1.6.x,
pypy3-dj1.6.x,
py27-dj1.7.x, py33-dj1.7.x, py34-dj1.7.x, pypy-dj1.7.x,
pypy3-dj1.7.x,
py27-dj1.8.x, py33-dj1.8.x, py34-dj1.8.x, pypy-dj1.8.x,
pypy3-dj1.8.x,
py27-dj1.9.x, py33-dj1.9.x, py34-dj1.9.x, pypy-dj1.9.x,
pypy3-dj1.9.x,
skipsdist = True
envlist =
py27-{1.8,1.9,master},
py33-{1.8},
py34-{1.8,1.9,master},
py35-{1.8,1.9,master},
[testenv]
commands = py.test []
deps = -rrequirements_test.txt
commands =
flake8 djadmin2
py.test []
deps =
-rrequirements_test.txt
1.8: Django>=1.8,<1.9
1.9: Django>=1.9,<1.10
master: https://github.com/django/django/tarball/master
usedevelop = True
setenv=
DJANGO_SETTINGS_MODULE = example.settings
PYTHONPATH = {toxinidir}/example:{toxinidir}
[testenv:py27-dj1.6.x]
basepython=python2.7
deps =
Django>=1.6,<1.7
coverage
{[testenv]deps}
[testenv:py27-dj1.7.x]
basepython=python2.7
deps =
Django>=1.7,<1.8
{[testenv]deps}
[testenv:py33-dj1.6.x]
basepython=python3.3
deps =
Django>=1.6,<1.7
{[testenv]deps}
[testenv:py34-dj1.6.x]
basepython=python3.4
deps =
Django>=1.6,<1.7
{[testenv]deps}
[testenv:py33-dj1.7.x]
basepython=python3.3
deps =
Django>=1.7,<1.8
{[testenv]deps}
[testenv:py34-dj1.7.x]
basepython=python3.4
deps =
Django>=1.7,<1.8
{[testenv]deps}
[testenv:pypy-dj1.6.x]
basepython=pypy
deps =
Django>=1.6,<1.7
{[testenv]deps}
[testenv:pypy3-dj1.6.x]
basepython=pypy3
deps =
Django>=1.6,<1.7
{[testenv]deps}
[testenv:pypy-dj1.7.x]
basepython=pypy
deps =
Django>=1.7,<1.8
{[testenv]deps}
[testenv:pypy3-dj1.7.x]
basepython=pypy3
deps =
Django>=1.7,<1.8
{[testenv]deps}
[testenv:py27-dj1.8.x]
basepython=python2.7
deps =
Django>=1.8,<1.9
{[testenv]deps}
[testenv:py34-dj1.8.x]
basepython=python3.4
deps =
Django>=1.8,<1.9
{[testenv]deps}
[testenv:py33-dj1.8.x]
basepython=python2.7
deps =
Django>=1.8,<1.9
{[testenv]deps}
[testenv:pypy-dj1.8.x]
basepython=pypy
deps =
Django>=1.8,<1.9
{[testenv]deps}
[testenv:pypy3-dj1.8.x]
basepython=pypy3
deps =
Django>=1.8,<1.9
{[testenv]deps}
[testenv:py27-dj1.9.x]
basepython=python2.7
deps =
Django>=1.9,<1.9.999
{[testenv]deps}
[testenv:py34-dj1.9.x]
basepython=python3.4
deps =
Django>=1.9,<1.9.999
{[testenv]deps}
[testenv:py33-dj1.9.x]
basepython=python2.7
deps =
Django>=1.9,<1.9.999
{[testenv]deps}
[testenv:pypy-dj1.9.x]
basepython=pypy
deps =
Django>=1.9,<1.9.999
{[testenv]deps}
[testenv:pypy3-dj1.9.x]
basepython=pypy3
deps =
Django>=1.9,<1.9.999
{[testenv]deps}