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 # test media upload
media media
# PyCharm
.idea/
.cache

View file

@ -1,47 +1,28 @@
sudo: false sudo: false
language: python language: python
python: "2.7" python:
- "2.7"
- "3.3"
- "3.4"
- "3.5"
env: env:
matrix: - DJANGO=1.8
- TOX_ENV=py27-dj1.6.x - DJANGO=1.9
- TOX_ENV=py27-dj1.7.x - DJANGO=master
- TOX_ENV=py27-dj1.8.x matrix:
- TOX_ENV=py27-dj1.9.x exclude:
- TOX_ENV=py33-dj1.6.x - python: "3.3"
- TOX_ENV=py33-dj1.7.x env: DJANGO=1.9
- TOX_ENV=py33-dj1.8.x - python: "3.3"
- TOX_ENV=py33-dj1.9.x env: DJANGO=master
- TOX_ENV=py34-dj1.6.x allow_failures:
- TOX_ENV=py34-dj1.7.x - python: "2.7"
- TOX_ENV=py34-dj1.8.x env: DJANGO=master
- TOX_ENV=py34-dj1.9.x - python: "3.4"
- TOX_ENV=pypy-dj1.6.x env: DJANGO=master
- TOX_ENV=pypy-dj1.7.x - python: "3.5"
- TOX_ENV=pypy-dj1.8.x env: DJANGO=master
- TOX_ENV=pypy-dj1.9.x
- TOX_ENV=pypy3-dj1.6.x
- TOX_ENV=pypy3-dj1.8.x
- TOX_ENV=pypy3-dj1.9.x
install: install:
- pip install tox - pip install tox
script: script:
- tox -e $TOX_ENV - tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-$DJANGO
# 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

View file

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

View file

@ -1,6 +1,20 @@
History 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) 0.6.1 (2014-02-26)
* Fix empty form display * Fix empty form display

View file

@ -1,6 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals from __future__ import division, absolute_import, unicode_literals
__version__ = '0.6.1' __version__ = '0.6.1'
__author__ = 'Daniel Greenfeld & Contributors' __author__ = 'Daniel Greenfeld & Contributors'
@ -10,17 +11,4 @@ VERSION = __version__ # synonym
# Default datetime input and output formats # Default datetime input and output formats
ISO_8601 = 'iso-8601' ISO_8601 = 'iso-8601'
from . import core default_app_config = "djadmin2.apps.Djadmin2Config"
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

View file

@ -2,14 +2,15 @@
from __future__ import division, absolute_import, unicode_literals from __future__ import division, absolute_import, unicode_literals
from django.contrib import messages 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.encoding import force_text
from django.utils.text import capfirst 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 as _
from django.utils.translation import ugettext_lazy, ungettext, pgettext_lazy
from django.views.generic import TemplateView
from . import permissions, utils from . import permissions, utils
from .viewmixins import AdminModel2Mixin from .viewmixins import Admin2ModelMixin
def get_description(action): def get_description(action):
@ -21,7 +22,7 @@ def get_description(action):
return capfirst(action.__name__.replace('_', ' ')) return capfirst(action.__name__.replace('_', ' '))
class BaseListAction(AdminModel2Mixin, TemplateView): class BaseListAction(Admin2ModelMixin, TemplateView):
permission_classes = (permissions.IsStaffPermission,) permission_classes = (permissions.IsStaffPermission,)
@ -54,7 +55,7 @@ class BaseListAction(AdminModel2Mixin, TemplateView):
super(BaseListAction, self).__init__(*args, **kwargs) super(BaseListAction, self).__init__(*args, **kwargs)
def get_queryset(self): def get_queryset(self):
""" Replaced `get_queryset` from `AdminModel2Mixin`""" """ Replaced `get_queryset` from `Admin2ModelMixin`"""
return self.queryset return self.queryset
def description(self): def description(self):
@ -94,7 +95,9 @@ class BaseListAction(AdminModel2Mixin, TemplateView):
return '%s: %s' % (force_text(capfirst(opts.verbose_name)), return '%s: %s' % (force_text(capfirst(opts.verbose_name)),
force_text(obj)) force_text(obj))
collector = utils.NestedObjects(using=None) using = router.db_for_write(self.model)
collector = utils.NestedObjects(using=using)
collector.collect(self.queryset) collector.collect(self.queryset)
context.update({ context.update({
@ -166,7 +169,6 @@ class DeleteSelectedAction(BaseListAction):
# objects, so render a template asking for their confirmation. # objects, so render a template asking for their confirmation.
return self.get(request) return self.get(request)
def process_queryset(self): def process_queryset(self):
# The user has confirmed that they want to delete the objects. # The user has confirmed that they want to delete the objects.
self.get_queryset().delete() 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.conf import settings
from django.contrib.auth.models import Group, User from django.contrib.auth.models import Group, User
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from rest_framework.relations import PrimaryKeyRelatedField from rest_framework.relations import PrimaryKeyRelatedField
import djadmin2
from djadmin2.forms import UserCreationForm, UserChangeForm
from djadmin2.apiviews import Admin2APISerializer 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): class GroupSerializer(Admin2APISerializer):
permissions = PrimaryKeyRelatedField(many=True) permissions = PrimaryKeyRelatedField(many=True, read_only=True)
class Meta: class Meta:
model = Group model = Group
class GroupAdmin2(djadmin2.ModelAdmin2): class GroupAdmin2(ModelAdmin2):
api_serializer_class = GroupSerializer api_serializer_class = GroupSerializer
class UserSerializer(Admin2APISerializer): class UserSerializer(Admin2APISerializer):
user_permissions = PrimaryKeyRelatedField(many=True) user_permissions = PrimaryKeyRelatedField(many=True, read_only=True)
class Meta: class Meta:
model = User model = User
exclude = ('passwords',) exclude = ('password',)
class UserAdmin2(djadmin2.ModelAdmin2): class UserAdmin2(ModelAdmin2):
create_form_class = UserCreationForm create_form_class = UserCreationForm
update_form_class = UserChangeForm update_form_class = UserChangeForm
search_fields = ('username', 'groups__name', 'first_name', 'last_name', search_fields = ('username', 'groups__name', 'first_name', 'last_name',
@ -43,15 +43,15 @@ class UserAdmin2(djadmin2.ModelAdmin2):
# Register each model with the admin # Register each model with the admin
djadmin2.default.register(User, UserAdmin2) djadmin2_site.register(User, UserAdmin2)
djadmin2.default.register(Group, GroupAdmin2) djadmin2_site.register(Group, GroupAdmin2)
# Register the sites app if it's been activated in INSTALLED_APPS # Register the sites app if it's been activated in INSTALLED_APPS
if "django.contrib.sites" in settings.INSTALLED_APPS: if "django.contrib.sites" in settings.INSTALLED_APPS:
class SiteAdmin2(djadmin2.ModelAdmin2): class SiteAdmin2(ModelAdmin2):
list_display = ('domain', 'name') list_display = ('domain', 'name')
search_fields = ('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 __future__ import division, absolute_import, unicode_literals
from django.utils.encoding import force_str from django.utils.encoding import force_str
from rest_framework import fields, generics, serializers from rest_framework import fields, generics, serializers
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.reverse import reverse from rest_framework.reverse import reverse
@ -17,11 +16,30 @@ API_VERSION = '0.1'
class Admin2APISerializer(serializers.HyperlinkedModelSerializer): class Admin2APISerializer(serializers.HyperlinkedModelSerializer):
_default_view_name = 'admin2:%(app_label)s_%(model_name)s_api_detail' _default_view_name = 'admin2:%(app_label)s_%(model_name)s_api_detail'
pk = fields.Field(source='pk') pk = fields.ReadOnlyField()
__unicode__ = fields.Field(source='__str__') __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): class Admin2APIMixin(Admin2Mixin):
model = None
raise_exception = True raise_exception = True
def get_serializer_class(self): 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 __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 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 apiviews
from . import types from . import types
@ -160,8 +159,7 @@ class Admin2(object):
} }
def get_urls(self): def get_urls(self):
urlpatterns = patterns( urlpatterns = [
'',
url(regex=r'^$', url(regex=r'^$',
view=self.index_view.as_view(**self.get_index_kwargs()), view=self.index_view.as_view(**self.get_index_kwargs()),
name='dashboard' name='dashboard'
@ -188,11 +186,10 @@ class Admin2(object):
**self.get_api_index_kwargs()), **self.get_api_index_kwargs()),
name='api_index' name='api_index'
), ),
) ]
for model, model_admin in self.registry.items(): for model, model_admin in self.registry.items():
model_options = utils.model_options(model) model_options = utils.model_options(model)
urlpatterns += patterns( urlpatterns += [
'',
url('^{}/{}/'.format( url('^{}/{}/'.format(
model_options.app_label, model_options.app_label,
model_options.object_name.lower()), model_options.object_name.lower()),
@ -201,11 +198,10 @@ class Admin2(object):
model_options.app_label, model_options.app_label,
model_options.object_name.lower()), model_options.object_name.lower()),
include(model_admin.api_urls)), include(model_admin.api_urls)),
) ]
return urlpatterns return urlpatterns
@property @property
def urls(self): def urls(self):
# We set the application and instance namespace here # We set the application and instance namespace here
return self.get_urls(), self.name, self.name return self.get_urls(), self.name, self.name

View file

@ -2,19 +2,17 @@
from __future__ import division, absolute_import, unicode_literals from __future__ import division, absolute_import, unicode_literals
import collections import collections
from itertools import chain 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 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 from .utils import type_str
@ -97,21 +95,21 @@ def build_list_filter(request, model_admin, queryset):
'fields': fields, '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 = { filterset_dict = {
"year": NumericDateFilter( "year": NumericDateFilter(
name="published_date", name=field_name,
lookup_type="year", lookup_type="year",
), ),
"month": NumericDateFilter( "month": NumericDateFilter(
name="published_date", name=field_name,
lookup_type="month", lookup_type="month",
), ),
"day": NumericDateFilter( "day": NumericDateFilter(
name="published_date", name=field_name,
lookup_type="day", lookup_type="day",
) )
} }

View file

@ -3,16 +3,16 @@ from __future__ import division, absolute_import, unicode_literals
from copy import deepcopy 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 import authenticate
from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import AuthenticationForm
from django.contrib.auth.forms import UserCreationForm, UserChangeForm from django.contrib.auth.forms import UserCreationForm, UserChangeForm
import django from django.core.urlresolvers import reverse_lazy
import django.forms from django.utils.translation import ugettext_lazy as _
import django.forms.models
import django.forms.extras.widgets
from django.utils.translation import ugettext_lazy
import floppyforms
_WIDGET_COMMON_ATTRIBUTES = ( _WIDGET_COMMON_ATTRIBUTES = (
@ -166,9 +166,7 @@ _django_to_floppyforms_widget = {
django.forms.extras.widgets.SelectDateWidget: django.forms.extras.widgets.SelectDateWidget:
_create_widget( _create_widget(
floppyforms.widgets.SelectDateWidget, floppyforms.widgets.SelectDateWidget,
init_arguments= init_arguments=('years',) if django.VERSION >= (1, 7) else ('years', 'required')),
('years',)
if django.VERSION >= (1, 7) else ('years', 'required')),
} }
_django_field_to_floppyform_widget = { _django_field_to_floppyform_widget = {
@ -184,8 +182,8 @@ _django_field_to_floppyform_widget = {
_create_widget(floppyforms.widgets.URLInput), _create_widget(floppyforms.widgets.URLInput),
django.forms.fields.SlugField: django.forms.fields.SlugField:
_create_widget(floppyforms.widgets.SlugInput), _create_widget(floppyforms.widgets.SlugInput),
django.forms.fields.IPAddressField: django.forms.fields.GenericIPAddressField:
_create_widget(floppyforms.widgets.IPAddressInput), _create_widget(floppyforms.widgets.TextInput),
django.forms.fields.SplitDateTimeField: django.forms.fields.SplitDateTimeField:
_create_splitdatetimewidget(floppyforms.widgets.SplitDateTimeWidget), _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 # replaces the default TextInput with a NumberInput, if localization is
# turned off. That applies for Django 1.6 upwards. # turned off. That applies for Django 1.6 upwards.
# See the relevant source code in django: # See the relevant source code in django:
# https://github.com/django/django/blob/1.6/django/forms/fields.py#L225 # https://github.com/django/django/blob/1.9.6/django/forms/fields.py#L261-264
if django.VERSION >= (1, 6): if isinstance(field, django.forms.IntegerField) and not field.localize:
if isinstance(field, django.forms.IntegerField) and not field.localize: if field.widget.__class__ is django.forms.NumberInput:
if field.widget.__class__ is django.forms.NumberInput: return True
return True
# We can check if the widget was replaced by comparing the class of the # 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 # 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 # Translators : %(username)s will be replaced by the username_field name
# (default : username, but could be email, or something else) # (default : username, but could be email, or something else)
ERROR_MESSAGE = ugettext_lazy("Please enter the correct %(username)s and password " ERROR_MESSAGE = _(
"for a staff account. Note that both fields may be case-sensitive.") "Please enter the correct %(username)s and password "
"for a staff account. Note that both fields may be case-sensitive."
)
class AdminAuthenticationForm(AuthenticationForm): class AdminAuthenticationForm(AuthenticationForm):
@ -283,10 +282,13 @@ class AdminAuthenticationForm(AuthenticationForm):
""" """
error_messages = { 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, this_is_the_login_form = django.forms.BooleanField(
initial=1, error_messages=error_messages) widget=floppyforms.HiddenInput,
initial=1,
error_messages=error_messages
)
def clean(self): def clean(self):
username = self.cleaned_data.get('username') username = self.cleaned_data.get('username')
@ -306,5 +308,17 @@ class AdminAuthenticationForm(AuthenticationForm):
return self.cleaned_data 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) 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.conf import settings
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.db import models from django.db import models
from django.db.models import signals
from django.utils.encoding import force_text 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 python_2_unicode_compatible
from django.utils.encoding import smart_text
from django.utils.translation import ugettext, ugettext_lazy as _ from django.utils.translation import ugettext, ugettext_lazy as _
from . import permissions
from .utils import quote from .utils import quote
@ -99,10 +97,3 @@ class LogEntry(models.Model):
quote(self.object_id) quote(self.object_id)
) )
return None 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 logging
import re import re
from django.contrib.auth import models as auth_models from django.contrib.auth import get_permission_codename
from django.contrib.contenttypes import models as contenttypes_models from django.db.utils import DEFAULT_DB_ALIAS
from django.db.models import get_models from django.apps import apps
from django.core.exceptions import ValidationError
from django.db import router
from django.utils import six from django.utils import six
from . import utils
from django.utils.encoding import python_2_unicode_compatible, force_text from django.utils.encoding import python_2_unicode_compatible, force_text
logger = logging.getLogger('djadmin2') logger = logging.getLogger('djadmin2')
@ -82,13 +81,13 @@ def model_permission(permission):
assert model_class, ( assert model_class, (
'Cannot apply model permissions on a view that does not ' 'Cannot apply model permissions on a view that does not '
'have a `.model` or `.queryset` property.') 'have a `.model` or `.queryset` property.')
try: try:
# django 1.8+ # django 1.8+
model_name = model_class._meta.model_name model_name = model_class._meta.model_name
except AttributeError: except AttributeError:
model_name = model_class._meta.module_name model_name = model_class._meta.module_name
permission_name = permission.format( permission_name = permission.format(
app_label=model_class._meta.app_label, app_label=model_class._meta.app_label,
model_name=model_name) model_name=model_name)
@ -363,14 +362,13 @@ class TemplatePermissionChecker(object):
else: else:
return self._view.has_permission(self._obj) return self._view.has_permission(self._obj)
def __str__(self): def __str__(self):
if self._view is None: if self._view is None:
return '' return ''
return force_text(bool(self)) 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. 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 Since we want to support read-only views, we need to add our own
permission. permission.
Copied from ``django.contrib.auth.management.create_permissions``. Copied from ``https://github.com/django/django/blob/1.9.6/django/contrib/auth/management/__init__.py#L60``.
"""
# Is there any reason for doing this import here?
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 # This will hold the permissions we're looking for as
# (content_type, (codename, name)) # (content_type, (codename, name))
searched_perms = list() searched_perms = list()
# The codenames and ctypes that should exist. # The codenames and ctypes that should exist.
ctypes = set() ctypes = set()
for klass in app_models: for klass in app_config.get_models():
ctype = contenttypes_models.ContentType.objects.get_for_model(klass) # Force looking up the content types in the current database
ctypes.add(ctype) # before creating foreign keys to them.
ctype = ContentType.objects.db_manager(using).get_for_model(klass)
opts = utils.model_options(klass) ctypes.add(ctype)
perm = ('view_%s' % opts.object_name.lower(), u'Can view %s' % opts.verbose_name_raw) perm = (get_permission_codename('view', klass._meta), 'Can view %s' % (klass._meta.verbose_name_raw))
searched_perms.append((ctype, perm)) searched_perms.append((ctype, perm))
# Find all the Permissions that have a content_type for a model we're # Find all the Permissions that have a content_type for a model we're
# looking for. We don't need to check for codenames since we already have # looking for. We don't need to check for codenames since we already have
# a list of the ones we're going to create. # 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, content_type__in=ctypes,
).values_list( ).values_list(
"content_type", "codename" "content_type", "codename"
)) ))
perms = [ perms = [
auth_models.Permission(codename=codename, name=name, content_type=ctype) Permission(codename=codename, name=name, content_type=ct)
for ctype, (codename, name) in searched_perms for ct, (codename, name) in searched_perms
if (ctype.pk, codename) not in all_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: if verbosity >= 2:
for perm in perms: 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 datetime import date, time, datetime
from django.db import models from django.db import models
from django.template.loader import render_to_string
from django.utils import formats, timezone from django.utils import formats, timezone
from django.utils.encoding import force_text from django.utils.encoding import force_text
from django.template.loader import render_to_string
from djadmin2 import settings 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 django.test import TestCase
from ..core import Admin2 from ..core import Admin2
from ..actions import get_description from ..actions import get_description
from .models import Thing
class Thing(models.Model):
pass
class TestAction(object): class TestAction(object):
@ -26,24 +22,24 @@ class ActionTest(TestCase):
self.admin2.registry[Thing].list_actions.extend([ self.admin2.registry[Thing].list_actions.extend([
TestAction, TestAction,
test_function, test_function,
]) ])
self.assertEquals( self.assertEquals(
get_description( get_description(
self.admin2.registry[Thing].list_actions[0] self.admin2.registry[Thing].list_actions[0]
), ),
'Delete selected items' 'Delete selected items'
) )
self.assertEquals( self.assertEquals(
get_description( get_description(
self.admin2.registry[Thing].list_actions[1] self.admin2.registry[Thing].list_actions[1]
), ),
'Test Action Class' 'Test Action Class'
) )
self.assertEquals( self.assertEquals(
get_description( get_description(
self.admin2.registry[Thing].list_actions[2] self.admin2.registry[Thing].list_actions[2]
), ),
'Test function' 'Test function'
) )
self.admin2.registry[Thing].list_actions.remove(TestAction) self.admin2.registry[Thing].list_actions.remove(TestAction)
self.admin2.registry[Thing].list_actions.remove(test_function) 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 import forms
from django.forms.formsets import formset_factory from django.forms.formsets import formset_factory
from django.test import TestCase from django.test import TestCase
from ..templatetags import admin2_tags from ..templatetags import admin2_tags
from ..views import IndexView from ..views import IndexView
from .models import TagsTestsModel
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 TagsTestForm(forms.Form): class TagsTestForm(forms.Form):
@ -89,7 +74,7 @@ class TagsTests(TestCase):
self.assertEquals( self.assertEquals(
admin2_tags.formset_visible_fieldlist(formset), admin2_tags.formset_visible_fieldlist(formset),
[u'Visible 1', u'Visible 2'] [u'Visible 1', u'Visible 2']
) )
def test_verbose_name_for(self): def test_verbose_name_for(self):
app_verbose_names = { app_verbose_names = {

View file

@ -5,7 +5,7 @@ from django.test.client import RequestFactory
import floppyforms import floppyforms
import djadmin2 from djadmin2.site import djadmin2_site
from ..admin2 import UserAdmin2 from ..admin2 import UserAdmin2
@ -27,7 +27,7 @@ class UserAdminTest(TestCase):
request = self.factory.get(reverse('admin2:auth_user_create')) request = self.factory.get(reverse('admin2:auth_user_create'))
request.user = self.user request.user = self.user
model_admin = UserAdmin2(User, djadmin2.default) model_admin = UserAdmin2(User, djadmin2_site)
view = model_admin.create_view.view.as_view( view = model_admin.create_view.view.as_view(
**model_admin.get_create_kwargs()) **model_admin.get_create_kwargs())
response = view(request) response = view(request)
@ -48,7 +48,7 @@ class UserAdminTest(TestCase):
request = self.factory.get( request = self.factory.get(
reverse('admin2:auth_user_update', args=(self.user.pk,))) reverse('admin2:auth_user_update', args=(self.user.pk,)))
request.user = self.user request.user = self.user
model_admin = UserAdmin2(User, djadmin2.default) model_admin = UserAdmin2(User, djadmin2_site)
view = model_admin.update_view.view.as_view( view = model_admin.update_view.view.as_view(
**model_admin.get_update_kwargs()) **model_admin.get_update_kwargs())
response = view(request, pk=self.user.pk) 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.auth.models import Group, User
from django.contrib.sites.models import Site from django.contrib.sites.models import Site
from django.core.exceptions import ImproperlyConfigured
from django.test import TestCase
import djadmin2 from djadmin2.site import djadmin2_site
from ..types import ModelAdmin2 from .models import SmallThing
from ..core import Admin2 from ..core import Admin2
from ..types import ModelAdmin2
class SmallThing(models.Model):
pass
APP_LABEL, APP_VERBOSE_NAME = 'app_one_label', 'App One Verbose Name' APP_LABEL, APP_VERBOSE_NAME = 'app_one_label', 'App One Verbose Name'
@ -71,4 +66,4 @@ class Admin2Test(TestCase):
def test_default_entries(self): def test_default_entries(self):
expected_default_models = (User, Group, Site) expected_default_models = (User, Group, Site)
for model in expected_default_models: 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 decimal import Decimal
from django.test import TestCase from django.test import TestCase
from django.db import models
from django.utils.translation import activate
from django.utils import six from django.utils import six
from django.utils.translation import activate
from .. import renderers from .. import renderers
from .models import RendererTestModel
class RendererTestModel(models.Model):
decimal = models.DecimalField(decimal_places=5, max_digits=10)
class BooleanRendererTest(TestCase): class BooleanRendererTest(TestCase):
@ -109,7 +105,7 @@ class NumberRendererTest(TestCase):
self.assertEqual('42.5', out) self.assertEqual('42.5', out)
def testEndlessFloat(self): def testEndlessFloat(self):
out = self.renderer(1.0/3, None) out = self.renderer(1.0 / 3, None)
if six.PY2: if six.PY2:
self.assertEqual('0.333333333333', out) self.assertEqual('0.333333333333', out)
else: else:

View file

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

View file

@ -1,27 +1,9 @@
from django.db import models
from django.test import TestCase from django.test import TestCase
from django.utils import six from django.utils import six
from .. import utils from .. import utils
from ..views import IndexView from ..views import IndexView
from .models import UtilsTestModel
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"
class UtilsTest(TestCase): class UtilsTest(TestCase):
@ -40,7 +22,7 @@ class UtilsTest(TestCase):
self.assertEquals( self.assertEquals(
UtilsTestModel._meta, UtilsTestModel._meta,
utils.model_options(UtilsTestModel) utils.model_options(UtilsTestModel)
) )
UtilsTestModel._meta.verbose_name = "Utils Test Model" UtilsTestModel._meta.verbose_name = "Utils Test Model"
UtilsTestModel._meta.verbose_name_plural = "Utils Test Models" UtilsTestModel._meta.verbose_name_plural = "Utils Test Models"
@ -55,7 +37,7 @@ class UtilsTest(TestCase):
self.assertEquals( self.assertEquals(
self.instance._meta, self.instance._meta,
utils.model_options(self.instance) utils.model_options(self.instance)
) )
self.instance._meta.verbose_name = "Utils Test Model" self.instance._meta.verbose_name = "Utils Test Model"
self.instance._meta.verbose_name_plural = "Utils Test Models" self.instance._meta.verbose_name_plural = "Utils Test Models"
@ -166,8 +148,6 @@ class UtilsTest(TestCase):
"str" "str"
) )
def test_get_attr(self): def test_get_attr(self):
class Klass(object): class Klass(object):
attr = "value" attr = "value"

View file

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

View file

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

View file

@ -3,13 +3,12 @@ from __future__ import division, absolute_import, unicode_literals
from collections import defaultdict from collections import defaultdict
from django.db.models import ProtectedError from django.db.models.deletion import Collector, ProtectedError
from django.db.models import ManyToManyRel from django.db.models.sql.constants import QUERY_TERMS
from django.db.models.deletion import Collector
from django.db.models.fields.related import ForeignObjectRel
from django.utils import six from django.utils import six
from django.utils.encoding import force_bytes, force_text from django.utils.encoding import force_bytes, force_text
def lookup_needs_distinct(opts, lookup_path): def lookup_needs_distinct(opts, lookup_path):
""" """
Returns True if 'distinct()' should be used to query the given 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 This is adopted from the Django core. django-admin2 mandates that code
doesn't depend on imports from django.contrib.admin. 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] lookup_fields = lookup_path.split('__')
condition1 = hasattr(field, 'rel') and isinstance(field.rel, ManyToManyRel) # Remove the last item of the lookup path if it is a query term
condition2 = isinstance(field, ForeignObjectRel) and not field.field.unique if lookup_fields[-1] in QUERY_TERMS:
return condition1 or condition2 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): def model_options(model):
@ -91,10 +101,8 @@ def get_attr(obj, attr):
and the __str__ attribute. and the __str__ attribute.
""" """
if attr == '__str__': if attr == '__str__':
if six.PY2: from builtins import str as text
value = unicode(obj) value = text(obj)
else:
value = str(obj)
else: else:
attribute = getattr(obj, attr) attribute = getattr(obj, attr)
value = attribute() if callable(attribute) else attribute 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 This is adopted from the Django core. django-admin2 mandates that code
doesn't depend on imports from django.contrib.admin. 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): def __init__(self, *args, **kwargs):
super(NestedObjects, self).__init__(*args, **kwargs) super(NestedObjects, self).__init__(*args, **kwargs)
self.edges = {} # {from_instance: [to_instances]} self.edges = {} # {from_instance: [to_instances]}
self.protected = set() self.protected = set()
self.model_count = defaultdict(int) self.model_objs = defaultdict(set)
def add_edge(self, source, target): def add_edge(self, source, target):
self.edges.setdefault(source, []).append(target) self.edges.setdefault(source, []).append(target)
@ -129,7 +136,7 @@ class NestedObjects(Collector):
self.add_edge(getattr(obj, related_name), obj) self.add_edge(getattr(obj, related_name), obj)
else: else:
self.add_edge(None, obj) self.add_edge(None, obj)
self.model_count[obj._meta.verbose_name_plural] += 1 self.model_objs[obj._meta.model].add(obj)
try: try:
return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs) return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs)
except ProtectedError as e: except ProtectedError as e:
@ -157,7 +164,6 @@ class NestedObjects(Collector):
def nested(self, format_callback=None): def nested(self, format_callback=None):
""" """
Return the graph as a nested list. Return the graph as a nested list.
""" """
seen = set() seen = set()
roots = [] roots = []
@ -183,14 +189,14 @@ def quote(s):
This is adopted from the Django core. django-admin2 mandates that code This is adopted from the Django core. django-admin2 mandates that code
doesn't depend on imports from django.contrib.admin. 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): if not isinstance(s, six.string_types):
return s return s
res = list(s) res = list(s)
for i in range(len(res)): for i in range(len(res)):
c = res[i] c = res[i]
if c in """:/_#?;@&=+$,"<>%\\""": if c in """:/_#?;@&=+$,"[]<>%\n\\""":
res[i] = '_%02X' % ord(c) res[i] = '_%02X' % ord(c)
return ''.join(res) return ''.join(res)
@ -199,4 +205,4 @@ def type_str(text):
if six.PY2: if six.PY2:
return force_bytes(text) return force_bytes(text)
else: 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) return super(Admin2Mixin, self).dispatch(request, *args, **kwargs)
class AdminModel2Mixin(Admin2Mixin): class Admin2ModelMixin(Admin2Mixin):
model_admin = None model_admin = None
def get_context_data(self, **kwargs): 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 = self.get_model()
model_meta = model_options(model) model_meta = model_options(model)
app_verbose_names = self.model_admin.admin.app_verbose_names app_verbose_names = self.model_admin.admin.app_verbose_names

View file

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

View file

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

View file

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals from __future__ import division, absolute_import, unicode_literals
from django.utils.translation import ugettext_lazy, pgettext_lazy
from django.contrib import messages 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 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 from django.utils.translation import ugettext_lazy
import djadmin2
from djadmin2 import renderers from djadmin2 import renderers
from djadmin2.actions import DeleteSelectedAction from djadmin2.actions import DeleteSelectedAction
# Import your custom models # Import your custom models
from djadmin2.site import djadmin2_site
from djadmin2.types import Admin2TabularInline, ModelAdmin2
from .actions import (CustomPublishAction, PublishAllItemsAction, from .actions import (CustomPublishAction, PublishAllItemsAction,
unpublish_items, unpublish_all_items) unpublish_items, unpublish_all_items)
from .models import Post, Comment from .models import Post, Comment
class CommentInline(djadmin2.Admin2TabularInline): class CommentInline(Admin2TabularInline):
model = Comment model = Comment
class PostAdmin(djadmin2.ModelAdmin2): class PostAdmin(ModelAdmin2):
list_actions = [ list_actions = [
DeleteSelectedAction, CustomPublishAction, DeleteSelectedAction, CustomPublishAction,
PublishAllItemsAction, unpublish_items, PublishAllItemsAction, unpublish_items,
@ -34,7 +35,7 @@ class PostAdmin(djadmin2.ModelAdmin2):
ordering = ["-published_date", "title",] ordering = ["-published_date", "title",]
class CommentAdmin(djadmin2.ModelAdmin2): class CommentAdmin(ModelAdmin2):
search_fields = ('body', '=post__title') search_fields = ('body', '=post__title')
list_filter = ['post', ] list_filter = ['post', ]
actions_on_top = True actions_on_top = True
@ -42,12 +43,12 @@ class CommentAdmin(djadmin2.ModelAdmin2):
actions_selection_counter = False actions_selection_counter = False
# Register the blog app with a verbose name # Register the blog app with a verbose name
djadmin2.default.register_app_verbose_name( djadmin2_site.register_app_verbose_name(
'blog', 'blog',
ugettext_lazy('My Blog') ugettext_lazy('My Blog')
) )
# Register each model with the admin # Register each model with the admin
djadmin2.default.register(Post, PostAdmin) djadmin2_site.register(Post, PostAdmin)
djadmin2.default.register(Comment, CommentAdmin) 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> <html>
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
@ -7,8 +7,8 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Bootstrap --> <!-- Bootstrap -->
{% block css %} {% block css %}
<link href="{{ STATIC_URL }}themes/bootstrap/css/bootstrap.min.css" rel="stylesheet" media="screen"> <link href="{% static "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-custom.css" %}" rel="stylesheet" media="screen">
{% endblock css %} {% endblock css %}
</head> </head>
<body> <body>
@ -19,8 +19,8 @@
{% block javascript %} {% block javascript %}
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.1/jquery.min.js"></script> <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>window.jQuery || document.write('<script src="{% static "themes/bootstrap/js/jquery.min.js" %}">\x3C/script>')</script>
<script src="{{ STATIC_URL }}themes/bootstrap/js/bootstrap.min.js"></script> <script src="{% static "themes/bootstrap/js/bootstrap.min.js" %}"></script>
{% endblock javascript %} {% endblock javascript %}
</body> </body>
</html> </html>

View file

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

View file

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

View file

@ -1,10 +1,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import floppyforms
from django import forms from django import forms
from django.test import TestCase from django.test import TestCase
import floppyforms
from djadmin2.forms import floppify_widget, floppify_form, modelform_factory from djadmin2.forms import floppify_widget, floppify_form, modelform_factory
from ..models import Post from ..models import Post
@ -103,7 +102,6 @@ class GetFloppyformWidgetTest(TestCase):
floppyforms.widgets.HiddenInput) floppyforms.widgets.HiddenInput)
widget = forms.widgets.HiddenInput() widget = forms.widgets.HiddenInput()
widget.is_hidden = False
self.assertExpectWidget( self.assertExpectWidget(
widget, widget,
floppyforms.widgets.HiddenInput, floppyforms.widgets.HiddenInput,
@ -162,7 +160,7 @@ class GetFloppyformWidgetTest(TestCase):
forms.DateInput(), forms.DateInput(),
floppyforms.DateInput) floppyforms.DateInput)
widget = forms.widgets.DateInput(format='DATE_FORMAT') widget = forms.widgets.DateInput(format='%Y-%m-%d')
self.assertExpectWidget( self.assertExpectWidget(
widget, widget,
floppyforms.widgets.DateInput, floppyforms.widgets.DateInput,
@ -311,13 +309,13 @@ class GetFloppyformWidgetTest(TestCase):
floppyforms.widgets.SplitDateTimeWidget) floppyforms.widgets.SplitDateTimeWidget)
widget = forms.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) new_widget = floppify_widget(widget)
self.assertTrue(isinstance( self.assertTrue(isinstance(
new_widget.widgets[0], floppyforms.widgets.DateInput)) new_widget.widgets[0], floppyforms.widgets.DateInput))
self.assertTrue(isinstance( self.assertTrue(isinstance(
new_widget.widgets[1], floppyforms.widgets.TimeInput)) 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') self.assertEqual(new_widget.widgets[1].format, 'TIME_FORMAT')
def test_splithiddendatetime_widget(self): def test_splithiddendatetime_widget(self):
@ -327,13 +325,13 @@ class GetFloppyformWidgetTest(TestCase):
floppyforms.widgets.SplitHiddenDateTimeWidget) floppyforms.widgets.SplitHiddenDateTimeWidget)
widget = forms.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) new_widget = floppify_widget(widget)
self.assertTrue(isinstance( self.assertTrue(isinstance(
new_widget.widgets[0], floppyforms.widgets.DateInput)) new_widget.widgets[0], floppyforms.widgets.DateInput))
self.assertTrue(isinstance( self.assertTrue(isinstance(
new_widget.widgets[1], floppyforms.widgets.TimeInput)) 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[0].is_hidden, True)
self.assertEqual(new_widget.widgets[1].format, 'TIME_FORMAT') self.assertEqual(new_widget.widgets[1].format, 'TIME_FORMAT')
self.assertEqual(new_widget.widgets[1].is_hidden, True) self.assertEqual(new_widget.widgets[1].is_hidden, True)
@ -489,13 +487,13 @@ class FieldWidgetTest(TestCase):
self.assertTrue(isinstance(widget, floppyforms.widgets.SlugInput)) self.assertTrue(isinstance(widget, floppyforms.widgets.SlugInput))
self.assertEqual(widget.input_type, 'text') self.assertEqual(widget.input_type, 'text')
def test_ipaddress_field(self): def test_genericipaddress_field(self):
class MyForm(forms.ModelForm): class MyForm(forms.ModelForm):
ipaddress = forms.IPAddressField() ipaddress = forms.GenericIPAddressField()
form_class = modelform_factory(model=Post, form=MyForm, exclude=[]) form_class = modelform_factory(model=Post, form=MyForm, exclude=[])
widget = form_class().fields['ipaddress'].widget 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') self.assertEqual(widget.input_type, 'text')
def test_splitdatetime_field(self): 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 django.test import TestCase
from djadmin2.utils import NestedObjects from djadmin2.utils import NestedObjects
from ..models import Count, Event, EventGuide
from ..models import Count, Event, EventGuide, Guest, Location
class NestedObjectsTests(TestCase): class NestedObjectsTests(TestCase):
@ -66,7 +65,8 @@ class NestedObjectsTests(TestCase):
Check that the nested collector doesn't query for DO_NOTHING objects. Check that the nested collector doesn't query for DO_NOTHING objects.
""" """
objs = [Event.objects.create()] 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]) EventGuide.objects.create(event=objs[0])
with self.assertNumQueries(2): with self.assertNumQueries(2):
# One for Location, one for Guest, and no query for EventGuide # 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.contrib.auth.models import User, Permission
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.shortcuts import get_object_or_404
from django.template import Template, Context from django.template import Template, Context
from django.test import TestCase from django.test import TestCase
from django.test.client import RequestFactory from django.test.client import RequestFactory
import djadmin2
from djadmin2 import ModelAdmin2
from djadmin2.permissions import TemplatePermissionChecker from djadmin2.permissions import TemplatePermissionChecker
from djadmin2.site import djadmin2_site
from blog.models import Post from djadmin2.types import ModelAdmin2
class TemplatePermissionTest(TestCase): class TemplatePermissionTest(TestCase):
@ -26,7 +26,7 @@ class TemplatePermissionTest(TestCase):
return template.render(context) return template.render(context)
def test_permission_wrapper(self): 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 = self.factory.get(reverse('admin2:blog_post_index'))
request.user = self.user request.user = self.user
permissions = TemplatePermissionChecker(request, model_admin) permissions = TemplatePermissionChecker(request, model_admin)
@ -48,8 +48,8 @@ class TemplatePermissionTest(TestCase):
codename='add_post') codename='add_post')
self.user.user_permissions.add(post_add_permission) self.user.user_permissions.add(post_add_permission)
# invalidate the users permission cache # invalidate the users permission cache
if hasattr(self.user, '_perm_cache'): self.user = get_object_or_404(User, pk=self.user.id)
del self.user._perm_cache request.user = self.user
result = self.render('{{ permissions.has_add_permission }}', context) result = self.render('{{ permissions.has_add_permission }}', context)
self.assertEqual(result, 'True') self.assertEqual(result, 'True')
@ -61,7 +61,7 @@ class TemplatePermissionTest(TestCase):
codename='add_post') codename='add_post')
self.user.user_permissions.add(post_add_permission) 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 = self.factory.get(reverse('admin2:blog_post_index'))
request.user = self.user request.user = self.user
permissions = TemplatePermissionChecker(request, model_admin) permissions = TemplatePermissionChecker(request, model_admin)
@ -89,8 +89,8 @@ class TemplatePermissionTest(TestCase):
self.assertEqual(result, '') self.assertEqual(result, '')
def test_admin_binding(self): def test_admin_binding(self):
user_admin = djadmin2.default.get_admin_by_name('auth_user') user_admin = djadmin2_site.get_admin_by_name('auth_user')
post_admin = djadmin2.default.get_admin_by_name('blog_post') post_admin = djadmin2_site.get_admin_by_name('blog_post')
request = self.factory.get(reverse('admin2:auth_user_index')) request = self.factory.get(reverse('admin2:auth_user_index'))
request.user = self.user request.user = self.user
permissions = TemplatePermissionChecker(request, user_admin) permissions = TemplatePermissionChecker(request, user_admin)
@ -121,10 +121,11 @@ class TemplatePermissionTest(TestCase):
content_type__app_label='blog', content_type__app_label='blog',
content_type__model='post', content_type__model='post',
codename='add_post') codename='add_post')
self.user.user_permissions.add(post_add_permission) self.user.user_permissions.add(post_add_permission)
# invalidate the users permission cache # invalidate the users permission cache
if hasattr(self.user, '_perm_cache'): self.user = get_object_or_404(User, pk=self.user.id)
del self.user._perm_cache request.user = self.user
result = self.render( result = self.render(
'{% load admin2_tags %}' '{% load admin2_tags %}'
@ -155,8 +156,8 @@ class TemplatePermissionTest(TestCase):
self.assertEqual(result, '') self.assertEqual(result, '')
def test_view_binding(self): def test_view_binding(self):
user_admin = djadmin2.default.get_admin_by_name('auth_user') user_admin = djadmin2_site.get_admin_by_name('auth_user')
post_admin = djadmin2.default.get_admin_by_name('blog_post') post_admin = djadmin2_site.get_admin_by_name('blog_post')
request = self.factory.get(reverse('admin2:auth_user_index')) request = self.factory.get(reverse('admin2:auth_user_index'))
request.user = self.user request.user = self.user
permissions = TemplatePermissionChecker(request, user_admin) permissions = TemplatePermissionChecker(request, user_admin)
@ -203,8 +204,8 @@ class TemplatePermissionTest(TestCase):
self.user.user_permissions.add(user_change_permission) self.user.user_permissions.add(user_change_permission)
# invalidate the users permission cache # invalidate the users permission cache
if hasattr(self.user, '_perm_cache'): self.user = get_object_or_404(User, pk=self.user.id)
del self.user._perm_cache request.user = self.user
result = self.render( result = self.render(
'{% load admin2_tags %}' '{% load admin2_tags %}'
@ -235,7 +236,7 @@ class TemplatePermissionTest(TestCase):
self.assertEqual(result, '1True2False34True') self.assertEqual(result, '1True2False34True')
def test_object_level_permission(self): 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 = self.factory.get(reverse('admin2:blog_post_index'))
request.user = self.user request.user = self.user
permissions = TemplatePermissionChecker(request, model_admin) permissions = TemplatePermissionChecker(request, model_admin)
@ -264,8 +265,8 @@ class TemplatePermissionTest(TestCase):
codename='add_post') codename='add_post')
self.user.user_permissions.add(post_add_permission) self.user.user_permissions.add(post_add_permission)
# invalidate the users permission cache # invalidate the users permission cache
if hasattr(self.user, '_perm_cache'): self.user = get_object_or_404(User, pk=self.user.id)
del self.user._perm_cache request.user = self.user
# object level permission are not supported by default. So this will # object level permission are not supported by default. So this will
# return ``False``. # return ``False``.

View file

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import unicode_literals from __future__ import unicode_literals
from datetime import datetime from datetime import datetime
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
@ -515,18 +516,19 @@ class TestAuthViews(TestCase):
def test_change_password_for_myself(self): def test_change_password_for_myself(self):
self.client.login(username=self.user.username, self.client.login(username=self.user.username,
password='password') password='password')
request = self.client.post(reverse('admin2:password_change', request = self.client.post(reverse('admin2:password_change',
kwargs={'pk': self.user.pk}), kwargs={'pk': self.user.pk}),
{'old_password': 'password', {'old_password': 'password',
'new_password1': 'user', 'new_password1': 'new_password',
'new_password2': 'user'}) 'new_password2': 'new_password'})
self.assertRedirects(request, reverse('admin2:password_change_done')) self.assertRedirects(request, reverse('admin2:password_change_done'))
self.client.logout() self.client.logout()
self.assertFalse(self.client.login(username=self.user.username, self.assertFalse(self.client.login(username=self.user.username,
password='password')) password='password'))
self.assertTrue(self.client.login(username=self.user.username, self.assertTrue(self.client.login(username=self.user.username,
password='user')) password='new_password'))
def test_change_password(self): def test_change_password(self):
self.client.login(username=self.user.username, self.client.login(username=self.user.username,
@ -538,8 +540,7 @@ class TestAuthViews(TestCase):
request = self.client.post(reverse('admin2:password_change', request = self.client.post(reverse('admin2:password_change',
kwargs={'pk': new_user.pk}), kwargs={'pk': new_user.pk}),
{'old_password': 'new_user', {'password1': 'new_user_password',
'password1': 'new_user_password',
'password2': 'new_user_password'}) 'password2': 'new_user_password'})
self.assertRedirects(request, reverse('admin2:password_change_done')) self.assertRedirects(request, reverse('admin2:password_change_done'))
self.client.logout() 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 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 DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = ( ALLOWED_HOSTS = []
# ('Your Name', 'your_email@example.com'),
)
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 = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.db.backends.sqlite3', '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: # Password validation
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone. AUTH_PASSWORD_VALIDATORS = [
TIME_ZONE = 'America/Chicago' {
'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' LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = True
SITE_ID = 1 SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not # Static files (CSS, JavaScript, Images)
# to load the internationalization machinery. # https://docs.djangoproject.com/en/1.9/howto/static-files/
USE_I18N = True
# 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/' STATIC_URL = '/static/'
# Additional locations of static files # Additional locations of static files
@ -73,94 +142,12 @@ STATICFILES_DIRS = (
os.path.join(BASE_DIR, "static"), os.path.join(BASE_DIR, "static"),
) )
# List of finder classes that know how to find static files in MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
# various locations. MEDIA_URL = "/media/"
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody. ADMIN2_THEME_DIRECTORY = "djadmin2theme_default"
SECRET_KEY = '*ymubzn8p_s7vrm%jsqvr6$qnea_5mcp(ao0z-yh1q0gro!0g1'
# List of callables that know how to import templates from various sources. REST_FRAMEWORK = {
TEMPLATE_LOADERS = ( 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
'django.template.loaders.filesystem.Loader', 'PAGE_SIZE': 10
'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,
},
}
} }
ADMIN2_THEME_DIRECTORY = "djadmin2theme_default"

View file

@ -1,18 +1,21 @@
from django.conf.urls import patterns, include, url from __future__ import unicode_literals
from django.contrib import admin
from blog.views import BlogListView, BlogDetailView 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() admin.autodiscover()
djadmin2_site.autodiscover()
import djadmin2 urlpatterns = [
url(r'^admin2/', include(djadmin2_site.urls)),
djadmin2.default.autodiscover()
urlpatterns = patterns('',
url(r'^admin2/', include(djadmin2.default.urls)),
url(r'^admin/', include(admin.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/', 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'^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'), 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. WSGI config for example project.
This module contains the WSGI application used by Django's development server It exposes the WSGI callable as a module-level variable named ``application``.
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.
For more information on this file, see
https://docs.djangoproject.com/en/1.9/howto/deployment/wsgi/
""" """
import os import os
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks from django.core.wsgi import get_wsgi_application
# 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"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "example.settings") 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() 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 -*- # -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals from __future__ import division, absolute_import, unicode_literals
import djadmin2 from djadmin2.site import djadmin2_site
from .models import CaptionedFile, UncaptionedFile from .models import CaptionedFile, UncaptionedFile
djadmin2.default.register(CaptionedFile) djadmin2_site.register(CaptionedFile)
djadmin2.default.register(UncaptionedFile) 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 @python_2_unicode_compatible
class CaptionedFile(models.Model): class CaptionedFile(models.Model):
caption = models.CharField(max_length=200, verbose_name=_('caption')) caption = models.CharField(max_length=200, verbose_name=_('caption'))
publication = models.FileField( publication = models.FileField(upload_to='captioned-files', verbose_name=_('Uploaded File'))
upload_to='media', verbose_name=_('Uploaded File'))
def __str__(self): def __str__(self):
return self.caption return self.caption
@ -22,11 +21,10 @@ class CaptionedFile(models.Model):
@python_2_unicode_compatible @python_2_unicode_compatible
class UncaptionedFile(models.Model): class UncaptionedFile(models.Model):
publication = models.FileField( publication = models.FileField(upload_to='uncaptioned-files', verbose_name=_('Uploaded File'))
upload_to='media', verbose_name=_('Uploaded File'))
def __str__(self): def __str__(self):
return self.publication return self.publication.name
class Meta: class Meta:
verbose_name = _('Uncaptioned File') 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 os import path
from django.test import TestCase
from files.models import CaptionedFile
fixture_dir = path.join(path.abspath(path.dirname(__file__)), 'fixtures') 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.contrib.auth import get_user_model
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.test import TestCase, Client from django.test import TestCase, Client
from django.utils import timezone
from django.utils.encoding import force_text from django.utils.encoding import force_text
from ..models import CaptionedFile from ..models import CaptionedFile
from os import path
fixture_dir = path.join(path.abspath(path.dirname(__file__)), 'fixtures') fixture_dir = path.join(path.abspath(path.dirname(__file__)), 'fixtures')
fixture_file = path.join(fixture_dir, 'pubtest.txt') fixture_file = path.join(fixture_dir, 'pubtest.txt')

View file

@ -1,17 +1,17 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from __future__ import division, absolute_import, unicode_literals 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 from .models import Poll, Choice
class ChoiceInline(djadmin2.Admin2TabularInline): class ChoiceInline(Admin2TabularInline):
model = Choice model = Choice
extra = 3 extra = 3
class PollAdmin(djadmin2.ModelAdmin2): class PollAdmin(ModelAdmin2):
fieldsets = [ fieldsets = [
(None, {'fields': ['question']}), (None, {'fields': ['question']}),
('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}), ('Date information', {'fields': ['pub_date'], 'classes': ['collapse']}),
@ -23,4 +23,4 @@ class PollAdmin(djadmin2.ModelAdmin2):
date_hierarchy = 'pub_date' 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 import datetime
from django.utils.encoding import python_2_unicode_compatible
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.encoding import python_2_unicode_compatible
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _

View file

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

View file

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

View file

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

View file

@ -127,14 +127,15 @@ setup(
include_package_data=True, include_package_data=True,
#test_suite='runtests.runtests', #test_suite='runtests.runtests',
install_requires=[ install_requires=[
'django>=1.6.0', 'django>=1.8.0',
'django-extra-views>=0.6.5', 'django-extra-views>=0.6.5',
'django-braces>=1.3.0', 'django-braces>=1.3.0',
'djangorestframework<=2.4.4', 'djangorestframework>=3.3.3',
'django-floppyforms<=1.2', 'django-floppyforms>=1.6.2',
'django-filter<0.12.0', 'django-filter>=0.13.0',
'django-crispy-forms>=1.3.2', 'django-crispy-forms>=1.3.2',
'pytz==2014.7' 'pytz==2014.7',
'future>=0.15.2',
], ],
extras_require={ extras_require={
'testing': ['pytest', 'pytest-django', 'pytest-ipdb'], '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] [tox]
envlist = py27-dj1.6.x, py33-dj1.6.x, py34-dj1.6.x, pypy-dj1.6.x, envlist =
pypy3-dj1.6.x, py27-{1.8,1.9,master},
py27-dj1.7.x, py33-dj1.7.x, py34-dj1.7.x, pypy-dj1.7.x, py33-{1.8},
pypy3-dj1.7.x, py34-{1.8,1.9,master},
py27-dj1.8.x, py33-dj1.8.x, py34-dj1.8.x, pypy-dj1.8.x, py35-{1.8,1.9,master},
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
[testenv] [testenv]
commands = py.test [] commands =
deps = -rrequirements_test.txt 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= setenv=
DJANGO_SETTINGS_MODULE = example.settings DJANGO_SETTINGS_MODULE = example.settings
PYTHONPATH = {toxinidir}/example:{toxinidir} 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}