diff --git a/.eggs/README.txt b/.eggs/README.txt new file mode 100644 index 0000000..5d01668 --- /dev/null +++ b/.eggs/README.txt @@ -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. + diff --git a/.gitignore b/.gitignore index 8e44d11..70be80d 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,8 @@ logfile # test media upload media + +# PyCharm +.idea/ + +.cache \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 51897c8..49235bc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,47 +1,28 @@ sudo: false language: python -python: "2.7" +python: + - "2.7" + - "3.3" + - "3.4" + - "3.5" env: - matrix: - - TOX_ENV=py27-dj1.6.x - - TOX_ENV=py27-dj1.7.x - - TOX_ENV=py27-dj1.8.x - - TOX_ENV=py27-dj1.9.x - - TOX_ENV=py33-dj1.6.x - - TOX_ENV=py33-dj1.7.x - - TOX_ENV=py33-dj1.8.x - - TOX_ENV=py33-dj1.9.x - - TOX_ENV=py34-dj1.6.x - - TOX_ENV=py34-dj1.7.x - - TOX_ENV=py34-dj1.8.x - - TOX_ENV=py34-dj1.9.x - - TOX_ENV=pypy-dj1.6.x - - TOX_ENV=pypy-dj1.7.x - - TOX_ENV=pypy-dj1.8.x - - TOX_ENV=pypy-dj1.9.x - - TOX_ENV=pypy3-dj1.6.x - - TOX_ENV=pypy3-dj1.8.x - - TOX_ENV=pypy3-dj1.9.x + - DJANGO=1.8 + - DJANGO=1.9 + - DJANGO=master +matrix: + exclude: + - python: "3.3" + env: DJANGO=1.9 + - python: "3.3" + env: DJANGO=master + allow_failures: + - python: "2.7" + env: DJANGO=master + - python: "3.4" + env: DJANGO=master + - python: "3.5" + env: DJANGO=master install: - pip install tox script: - - tox -e $TOX_ENV -# for now commented. We have to figure which version use for coverage -# and coveralls -#after_success: -# - coverage report -# - pip install --quiet python-coveralls -# - coveralls -matrix: - allow_failures: - - env: TOX_ENV=py27-dj1.8.x - - env: TOX_ENV=py27-dj1.9.x - - env: TOX_ENV=py33-dj1.8.x - - env: TOX_ENV=py33-dj1.9.x - - env: TOX_ENV=py34-dj1.8.x - - env: TOX_ENV=py34-dj1.9.x - - env: TOX_ENV=pypy-dj1.8.x - - env: TOX_ENV=pypy-dj1.9.x - - env: TOX_ENV=pypy3-dj1.8.x - - env: TOX_ENV=pypy3-dj1.9.x - - env: TOX_ENV=pypy3-dj1.9.x + - tox -e py${TRAVIS_PYTHON_VERSION//[.]/}-$DJANGO \ No newline at end of file diff --git a/AUTHORS.rst b/AUTHORS.rst index af669c3..afc7c06 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -52,6 +52,7 @@ Developers * marangonico * Kamil Gałuszka (@galuszkak / galuszkak@gmail.com) * Germano Gabbianelli (@tyrion) +* Arthur (@arthur-wsw / arthur@wallstreetweb.net) Translators ----------- diff --git a/HISTORY.rst b/HISTORY.rst index af185ae..794b0f7 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,6 +1,20 @@ History ========= +0.6.2 (?) + +* Fix Django 1.8 issues and add 1.9 compatibility +* Update all dependancies (DRF, floppyforms, filters, ...) +* Regenerate example project to make it django 1.9 compatible +* Update tox and travis and add flake8 +* Rename AdminModel2Mixin to Admin2ModelMixin +* Add migrations +* Replace IPAddressField with GenericIPAddressField +* Fix password link in user admin +* Fix user logout on password change +* Fix tests + + 0.6.1 (2014-02-26) * Fix empty form display diff --git a/djadmin2/__init__.py b/djadmin2/__init__.py index d7ea63c..9e4414b 100644 --- a/djadmin2/__init__.py +++ b/djadmin2/__init__.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import division, absolute_import, unicode_literals + __version__ = '0.6.1' __author__ = 'Daniel Greenfeld & Contributors' @@ -10,17 +11,4 @@ VERSION = __version__ # synonym # Default datetime input and output formats ISO_8601 = 'iso-8601' -from . import core -from . import types - - -default = core.Admin2() -ModelAdmin2 = types.ModelAdmin2 -Admin2TabularInline = types.Admin2TabularInline -Admin2StackedInline = types.Admin2StackedInline - -# Utility to make migration between versions easier -sites = default -ModelAdmin = ModelAdmin2 -AdminInline = Admin2TabularInline -Admin2Inline = Admin2TabularInline +default_app_config = "djadmin2.apps.Djadmin2Config" diff --git a/djadmin2/actions.py b/djadmin2/actions.py index 38e21eb..9638726 100644 --- a/djadmin2/actions.py +++ b/djadmin2/actions.py @@ -2,14 +2,15 @@ from __future__ import division, absolute_import, unicode_literals from django.contrib import messages -from django.views.generic import TemplateView +from django.db import router from django.utils.encoding import force_text from django.utils.text import capfirst -from django.utils.translation import ugettext_lazy, ungettext, pgettext_lazy from django.utils.translation import ugettext as _ +from django.utils.translation import ugettext_lazy, ungettext, pgettext_lazy +from django.views.generic import TemplateView from . import permissions, utils -from .viewmixins import AdminModel2Mixin +from .viewmixins import Admin2ModelMixin def get_description(action): @@ -21,7 +22,7 @@ def get_description(action): return capfirst(action.__name__.replace('_', ' ')) -class BaseListAction(AdminModel2Mixin, TemplateView): +class BaseListAction(Admin2ModelMixin, TemplateView): permission_classes = (permissions.IsStaffPermission,) @@ -54,7 +55,7 @@ class BaseListAction(AdminModel2Mixin, TemplateView): super(BaseListAction, self).__init__(*args, **kwargs) def get_queryset(self): - """ Replaced `get_queryset` from `AdminModel2Mixin`""" + """ Replaced `get_queryset` from `Admin2ModelMixin`""" return self.queryset def description(self): @@ -94,7 +95,9 @@ class BaseListAction(AdminModel2Mixin, TemplateView): return '%s: %s' % (force_text(capfirst(opts.verbose_name)), force_text(obj)) - collector = utils.NestedObjects(using=None) + using = router.db_for_write(self.model) + + collector = utils.NestedObjects(using=using) collector.collect(self.queryset) context.update({ @@ -166,7 +169,6 @@ class DeleteSelectedAction(BaseListAction): # objects, so render a template asking for their confirmation. return self.get(request) - def process_queryset(self): # The user has confirmed that they want to delete the objects. self.get_queryset().delete() diff --git a/djadmin2/admin2.py b/djadmin2/admin2.py index c229716..5cfece8 100644 --- a/djadmin2/admin2.py +++ b/djadmin2/admin2.py @@ -4,34 +4,34 @@ from __future__ import division, absolute_import, unicode_literals from django.conf import settings from django.contrib.auth.models import Group, User from django.contrib.sites.models import Site - from rest_framework.relations import PrimaryKeyRelatedField -import djadmin2 -from djadmin2.forms import UserCreationForm, UserChangeForm from djadmin2.apiviews import Admin2APISerializer +from djadmin2.forms import UserCreationForm, UserChangeForm +from djadmin2.site import djadmin2_site +from djadmin2.types import ModelAdmin2 class GroupSerializer(Admin2APISerializer): - permissions = PrimaryKeyRelatedField(many=True) + permissions = PrimaryKeyRelatedField(many=True, read_only=True) class Meta: model = Group -class GroupAdmin2(djadmin2.ModelAdmin2): +class GroupAdmin2(ModelAdmin2): api_serializer_class = GroupSerializer class UserSerializer(Admin2APISerializer): - user_permissions = PrimaryKeyRelatedField(many=True) + user_permissions = PrimaryKeyRelatedField(many=True, read_only=True) class Meta: model = User - exclude = ('passwords',) + exclude = ('password',) -class UserAdmin2(djadmin2.ModelAdmin2): +class UserAdmin2(ModelAdmin2): create_form_class = UserCreationForm update_form_class = UserChangeForm search_fields = ('username', 'groups__name', 'first_name', 'last_name', @@ -43,15 +43,15 @@ class UserAdmin2(djadmin2.ModelAdmin2): # Register each model with the admin -djadmin2.default.register(User, UserAdmin2) -djadmin2.default.register(Group, GroupAdmin2) +djadmin2_site.register(User, UserAdmin2) +djadmin2_site.register(Group, GroupAdmin2) # Register the sites app if it's been activated in INSTALLED_APPS if "django.contrib.sites" in settings.INSTALLED_APPS: - class SiteAdmin2(djadmin2.ModelAdmin2): + class SiteAdmin2(ModelAdmin2): list_display = ('domain', 'name') search_fields = ('domain', 'name') - djadmin2.default.register(Site, SiteAdmin2) + djadmin2_site.register(Site, SiteAdmin2) diff --git a/djadmin2/apiviews.py b/djadmin2/apiviews.py index 39e7e32..e126f5f 100644 --- a/djadmin2/apiviews.py +++ b/djadmin2/apiviews.py @@ -2,7 +2,6 @@ from __future__ import division, absolute_import, unicode_literals from django.utils.encoding import force_str - from rest_framework import fields, generics, serializers from rest_framework.response import Response from rest_framework.reverse import reverse @@ -17,11 +16,30 @@ API_VERSION = '0.1' class Admin2APISerializer(serializers.HyperlinkedModelSerializer): _default_view_name = 'admin2:%(app_label)s_%(model_name)s_api_detail' - pk = fields.Field(source='pk') - __unicode__ = fields.Field(source='__str__') + pk = fields.ReadOnlyField() + __unicode__ = fields.ReadOnlyField(source='__str__') + + def get_extra_kwargs(self): + extra_kwargs = super(Admin2APISerializer, self).get_extra_kwargs() + extra_kwargs.update({ + 'url': {'view_name': self._get_default_view_name(self.Meta.model)} + }) + return extra_kwargs + + def _get_default_view_name(self, model): + """ + Return the view name to use if 'view_name' is not specified in 'Meta' + """ + model_meta = model._meta + format_kwargs = { + 'app_label': model_meta.app_label, + 'model_name': model_meta.object_name.lower() + } + return self._default_view_name % format_kwargs class Admin2APIMixin(Admin2Mixin): + model = None raise_exception = True def get_serializer_class(self): diff --git a/djadmin2/apps.py b/djadmin2/apps.py new file mode 100644 index 0000000..16eab43 --- /dev/null +++ b/djadmin2/apps.py @@ -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") diff --git a/djadmin2/core.py b/djadmin2/core.py index fcd8e3d..5adafdf 100644 --- a/djadmin2/core.py +++ b/djadmin2/core.py @@ -5,12 +5,11 @@ Issue #99. """ from __future__ import division, absolute_import, unicode_literals -from django.conf.urls import patterns, include, url -from django.conf import settings -from django.core.exceptions import ImproperlyConfigured - from importlib import import_module +from django.conf import settings +from django.conf.urls import include, url +from django.core.exceptions import ImproperlyConfigured from . import apiviews from . import types @@ -160,8 +159,7 @@ class Admin2(object): } def get_urls(self): - urlpatterns = patterns( - '', + urlpatterns = [ url(regex=r'^$', view=self.index_view.as_view(**self.get_index_kwargs()), name='dashboard' @@ -188,11 +186,10 @@ class Admin2(object): **self.get_api_index_kwargs()), name='api_index' ), - ) + ] for model, model_admin in self.registry.items(): model_options = utils.model_options(model) - urlpatterns += patterns( - '', + urlpatterns += [ url('^{}/{}/'.format( model_options.app_label, model_options.object_name.lower()), @@ -201,11 +198,10 @@ class Admin2(object): model_options.app_label, model_options.object_name.lower()), include(model_admin.api_urls)), - ) + ] return urlpatterns @property def urls(self): # We set the application and instance namespace here return self.get_urls(), self.name, self.name - diff --git a/djadmin2/filters.py b/djadmin2/filters.py index 7c06c49..557b2f7 100644 --- a/djadmin2/filters.py +++ b/djadmin2/filters.py @@ -2,19 +2,17 @@ from __future__ import division, absolute_import, unicode_literals import collections - from itertools import chain -from django import forms -from django.forms.util import flatatt -from django.utils.html import format_html -from django.utils.encoding import force_text, force_bytes -from django.utils.safestring import mark_safe -from django.forms import widgets as django_widgets -from django.utils import six -from django.utils.translation import ugettext_lazy - import django_filters +from django import forms +from django.forms import widgets as django_widgets +from django.forms.utils import flatatt +from django.utils import six +from django.utils.encoding import force_text +from django.utils.html import format_html +from django.utils.safestring import mark_safe +from django.utils.translation import ugettext_lazy from .utils import type_str @@ -97,21 +95,21 @@ def build_list_filter(request, model_admin, queryset): 'fields': fields, }, ) - return type(type_str('%sFilterSet' % queryset.model.__name__),(django_filters.FilterSet, ),filterset_dict,)(request.GET, queryset=queryset) + return type(type_str('%sFilterSet' % queryset.model.__name__), (django_filters.FilterSet, ), filterset_dict,)(request.GET, queryset=queryset) -def build_date_filter(request, model_admin, queryset): +def build_date_filter(request, model_admin, queryset, field_name="published_date"): filterset_dict = { "year": NumericDateFilter( - name="published_date", + name=field_name, lookup_type="year", ), "month": NumericDateFilter( - name="published_date", + name=field_name, lookup_type="month", ), "day": NumericDateFilter( - name="published_date", + name=field_name, lookup_type="day", ) } diff --git a/djadmin2/forms.py b/djadmin2/forms.py index 7721f97..ab7c24d 100644 --- a/djadmin2/forms.py +++ b/djadmin2/forms.py @@ -3,16 +3,16 @@ from __future__ import division, absolute_import, unicode_literals from copy import deepcopy +import django +import django.forms +import django.forms.extras.widgets +import django.forms.models +import floppyforms from django.contrib.auth import authenticate from django.contrib.auth.forms import AuthenticationForm from django.contrib.auth.forms import UserCreationForm, UserChangeForm -import django -import django.forms -import django.forms.models -import django.forms.extras.widgets -from django.utils.translation import ugettext_lazy - -import floppyforms +from django.core.urlresolvers import reverse_lazy +from django.utils.translation import ugettext_lazy as _ _WIDGET_COMMON_ATTRIBUTES = ( @@ -166,9 +166,7 @@ _django_to_floppyforms_widget = { django.forms.extras.widgets.SelectDateWidget: _create_widget( floppyforms.widgets.SelectDateWidget, - init_arguments= - ('years',) - if django.VERSION >= (1, 7) else ('years', 'required')), + init_arguments=('years',) if django.VERSION >= (1, 7) else ('years', 'required')), } _django_field_to_floppyform_widget = { @@ -184,8 +182,8 @@ _django_field_to_floppyform_widget = { _create_widget(floppyforms.widgets.URLInput), django.forms.fields.SlugField: _create_widget(floppyforms.widgets.SlugInput), - django.forms.fields.IPAddressField: - _create_widget(floppyforms.widgets.IPAddressInput), + django.forms.fields.GenericIPAddressField: + _create_widget(floppyforms.widgets.TextInput), django.forms.fields.SplitDateTimeField: _create_splitdatetimewidget(floppyforms.widgets.SplitDateTimeWidget), } @@ -201,11 +199,10 @@ def allow_floppify_widget_for_field(field): # replaces the default TextInput with a NumberInput, if localization is # turned off. That applies for Django 1.6 upwards. # See the relevant source code in django: - # https://github.com/django/django/blob/1.6/django/forms/fields.py#L225 - if django.VERSION >= (1, 6): - if isinstance(field, django.forms.IntegerField) and not field.localize: - if field.widget.__class__ is django.forms.NumberInput: - return True + # https://github.com/django/django/blob/1.9.6/django/forms/fields.py#L261-264 + if isinstance(field, django.forms.IntegerField) and not field.localize: + if field.widget.__class__ is django.forms.NumberInput: + return True # We can check if the widget was replaced by comparing the class of the # specified widget with the default widget that is specified on the field @@ -272,8 +269,10 @@ def modelform_factory(model, form=django.forms.models.ModelForm, fields=None, # Translators : %(username)s will be replaced by the username_field name # (default : username, but could be email, or something else) -ERROR_MESSAGE = ugettext_lazy("Please enter the correct %(username)s and password " - "for a staff account. Note that both fields may be case-sensitive.") +ERROR_MESSAGE = _( + "Please enter the correct %(username)s and password " + "for a staff account. Note that both fields may be case-sensitive." +) class AdminAuthenticationForm(AuthenticationForm): @@ -283,10 +282,13 @@ class AdminAuthenticationForm(AuthenticationForm): """ error_messages = { - 'required': ugettext_lazy("Please log in again, because your session has expired."), + 'required': _("Please log in again, because your session has expired."), } - this_is_the_login_form = django.forms.BooleanField(widget=floppyforms.HiddenInput, - initial=1, error_messages=error_messages) + this_is_the_login_form = django.forms.BooleanField( + widget=floppyforms.HiddenInput, + initial=1, + error_messages=error_messages + ) def clean(self): username = self.cleaned_data.get('username') @@ -306,5 +308,17 @@ class AdminAuthenticationForm(AuthenticationForm): return self.cleaned_data +class Admin2UserChangeForm(UserChangeForm): + + def __init__(self, *args, **kwargs): + super(Admin2UserChangeForm, self).__init__(*args, **kwargs) + print(self.fields['password'].help_text) + self.fields['password'].help_text = _("Raw passwords are not stored, so there is no way to see this user's password, but you can change the password using this form." % self.get_update_password_url()) + + def get_update_password_url(self): + if self.instance and self.instance.pk: + return reverse_lazy('admin2:password_change', args=[self.instance.pk]) + return 'password/' + UserCreationForm = floppify_form(UserCreationForm) -UserChangeForm = floppify_form(UserChangeForm) +UserChangeForm = floppify_form(Admin2UserChangeForm) diff --git a/djadmin2/migrations/0001_initial.py b/djadmin2/migrations/0001_initial.py new file mode 100644 index 0000000..3931cd3 --- /dev/null +++ b/djadmin2/migrations/0001_initial.py @@ -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', + }, + ), + ] diff --git a/djadmin2/migrations/__init__.py b/djadmin2/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djadmin2/models.py b/djadmin2/models.py index f39e086..0cb781b 100644 --- a/djadmin2/models.py +++ b/djadmin2/models.py @@ -5,13 +5,11 @@ from __future__ import division, absolute_import, unicode_literals from django.conf import settings from django.contrib.contenttypes.models import ContentType from django.db import models -from django.db.models import signals from django.utils.encoding import force_text -from django.utils.encoding import smart_text from django.utils.encoding import python_2_unicode_compatible +from django.utils.encoding import smart_text from django.utils.translation import ugettext, ugettext_lazy as _ -from . import permissions from .utils import quote @@ -99,10 +97,3 @@ class LogEntry(models.Model): quote(self.object_id) ) return None - -# setup signal handlers here, since ``models.py`` will be imported by django -# for sure if ``djadmin2`` is listed in the ``INSTALLED_APPS``. - -signals.post_syncdb.connect( - permissions.create_view_permissions, - dispatch_uid="django-admin2.djadmin2.permissions.create_view_permissions") diff --git a/djadmin2/permissions.py b/djadmin2/permissions.py index 08acbd2..8bbee29 100644 --- a/djadmin2/permissions.py +++ b/djadmin2/permissions.py @@ -20,15 +20,14 @@ from __future__ import division, absolute_import, unicode_literals import logging import re -from django.contrib.auth import models as auth_models -from django.contrib.contenttypes import models as contenttypes_models -from django.db.models import get_models +from django.contrib.auth import get_permission_codename +from django.db.utils import DEFAULT_DB_ALIAS +from django.apps import apps +from django.core.exceptions import ValidationError +from django.db import router from django.utils import six - -from . import utils from django.utils.encoding import python_2_unicode_compatible, force_text - logger = logging.getLogger('djadmin2') @@ -82,13 +81,13 @@ def model_permission(permission): assert model_class, ( 'Cannot apply model permissions on a view that does not ' 'have a `.model` or `.queryset` property.') - + try: # django 1.8+ model_name = model_class._meta.model_name except AttributeError: model_name = model_class._meta.module_name - + permission_name = permission.format( app_label=model_class._meta.app_label, model_name=model_name) @@ -363,14 +362,13 @@ class TemplatePermissionChecker(object): else: return self._view.has_permission(self._obj) - def __str__(self): if self._view is None: return '' return force_text(bool(self)) -def create_view_permissions(app, created_models, verbosity, **kwargs): +def create_view_permissions(app_config, verbosity=2, interactive=True, using=DEFAULT_DB_ALIAS, **kwargs): # noqa """ Create 'view' permissions for all models. @@ -378,40 +376,64 @@ def create_view_permissions(app, created_models, verbosity, **kwargs): Since we want to support read-only views, we need to add our own permission. - Copied from ``django.contrib.auth.management.create_permissions``. - """ - # Is there any reason for doing this import here? + Copied from ``https://github.com/django/django/blob/1.9.6/django/contrib/auth/management/__init__.py#L60``. - app_models = get_models(app) + """ + if not app_config.models_module: + return + + try: + Permission = apps.get_model('auth', 'Permission') + except LookupError: + return + + if not router.allow_migrate_model(using, Permission): + return + + from django.contrib.contenttypes.models import ContentType # This will hold the permissions we're looking for as # (content_type, (codename, name)) searched_perms = list() # The codenames and ctypes that should exist. ctypes = set() - for klass in app_models: - ctype = contenttypes_models.ContentType.objects.get_for_model(klass) - ctypes.add(ctype) + for klass in app_config.get_models(): + # Force looking up the content types in the current database + # before creating foreign keys to them. + ctype = ContentType.objects.db_manager(using).get_for_model(klass) - opts = utils.model_options(klass) - perm = ('view_%s' % opts.object_name.lower(), u'Can view %s' % opts.verbose_name_raw) + ctypes.add(ctype) + perm = (get_permission_codename('view', klass._meta), 'Can view %s' % (klass._meta.verbose_name_raw)) searched_perms.append((ctype, perm)) # Find all the Permissions that have a content_type for a model we're # looking for. We don't need to check for codenames since we already have # a list of the ones we're going to create. - all_perms = set(auth_models.Permission.objects.filter( + all_perms = set(Permission.objects.using(using).filter( content_type__in=ctypes, ).values_list( "content_type", "codename" )) perms = [ - auth_models.Permission(codename=codename, name=name, content_type=ctype) - for ctype, (codename, name) in searched_perms - if (ctype.pk, codename) not in all_perms + Permission(codename=codename, name=name, content_type=ct) + for ct, (codename, name) in searched_perms + if (ct.pk, codename) not in all_perms ] - auth_models.Permission.objects.bulk_create(perms) + # Validate the permissions before bulk_creation to avoid cryptic + # database error when the verbose_name is longer than 50 characters + permission_name_max_length = Permission._meta.get_field('name').max_length + verbose_name_max_length = permission_name_max_length - 11 # len('Can change ') prefix + for perm in perms: + if len(perm.name) > permission_name_max_length: + raise ValidationError( + "The verbose_name of %s.%s is longer than %s characters" % ( + perm.content_type.app_label, + perm.content_type.model, + verbose_name_max_length, + ) + ) + Permission.objects.using(using).bulk_create(perms) if verbosity >= 2: for perm in perms: - logger.info("Adding permission '%s'" % perm) + print("Adding permission '%s'" % perm) diff --git a/djadmin2/renderers.py b/djadmin2/renderers.py index fbfc9b6..614528f 100644 --- a/djadmin2/renderers.py +++ b/djadmin2/renderers.py @@ -9,9 +9,9 @@ import os.path from datetime import date, time, datetime from django.db import models +from django.template.loader import render_to_string from django.utils import formats, timezone from django.utils.encoding import force_text -from django.template.loader import render_to_string from djadmin2 import settings diff --git a/djadmin2/site.py b/djadmin2/site.py new file mode 100644 index 0000000..111cf76 --- /dev/null +++ b/djadmin2/site.py @@ -0,0 +1,3 @@ +from . import core + +djadmin2_site = core.Admin2() diff --git a/djadmin2/tests/migrations/0001_initial.py b/djadmin2/tests/migrations/0001_initial.py new file mode 100644 index 0000000..700cd16 --- /dev/null +++ b/djadmin2/tests/migrations/0001_initial.py @@ -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', + }, + ), + ] diff --git a/djadmin2/tests/migrations/__init__.py b/djadmin2/tests/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/djadmin2/tests/models.py b/djadmin2/tests/models.py new file mode 100644 index 0000000..22ffe33 --- /dev/null +++ b/djadmin2/tests/models.py @@ -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" diff --git a/djadmin2/tests/test_actions.py b/djadmin2/tests/test_actions.py index ff11118..7a48e55 100644 --- a/djadmin2/tests/test_actions.py +++ b/djadmin2/tests/test_actions.py @@ -1,12 +1,8 @@ -from django.db import models from django.test import TestCase from ..core import Admin2 from ..actions import get_description - - -class Thing(models.Model): - pass +from .models import Thing class TestAction(object): @@ -26,24 +22,24 @@ class ActionTest(TestCase): self.admin2.registry[Thing].list_actions.extend([ TestAction, test_function, - ]) + ]) self.assertEquals( get_description( self.admin2.registry[Thing].list_actions[0] - ), + ), 'Delete selected items' - ) + ) self.assertEquals( get_description( self.admin2.registry[Thing].list_actions[1] - ), + ), 'Test Action Class' - ) + ) self.assertEquals( get_description( self.admin2.registry[Thing].list_actions[2] - ), + ), 'Test function' - ) + ) self.admin2.registry[Thing].list_actions.remove(TestAction) self.admin2.registry[Thing].list_actions.remove(test_function) diff --git a/djadmin2/tests/test_admin2tags.py b/djadmin2/tests/test_admin2tags.py index 0d9bf5b..6c3dcb9 100644 --- a/djadmin2/tests/test_admin2tags.py +++ b/djadmin2/tests/test_admin2tags.py @@ -1,25 +1,10 @@ -from django.db import models from django import forms from django.forms.formsets import formset_factory from django.test import TestCase from ..templatetags import admin2_tags from ..views import IndexView - - -class TagsTestsModel(models.Model): - - field1 = models.CharField(max_length=23) - field2 = models.CharField('second field', max_length=42) - - def was_published_recently(self): - return True - was_published_recently.boolean = True - was_published_recently.short_description = 'Published recently?' - - class Meta: - verbose_name = "Tags Test Model" - verbose_name_plural = "Tags Test Models" +from .models import TagsTestsModel class TagsTestForm(forms.Form): @@ -89,7 +74,7 @@ class TagsTests(TestCase): self.assertEquals( admin2_tags.formset_visible_fieldlist(formset), [u'Visible 1', u'Visible 2'] - ) + ) def test_verbose_name_for(self): app_verbose_names = { diff --git a/djadmin2/tests/test_auth_admin.py b/djadmin2/tests/test_auth_admin.py index 5afdb1c..88ae67d 100644 --- a/djadmin2/tests/test_auth_admin.py +++ b/djadmin2/tests/test_auth_admin.py @@ -5,7 +5,7 @@ from django.test.client import RequestFactory import floppyforms -import djadmin2 +from djadmin2.site import djadmin2_site from ..admin2 import UserAdmin2 @@ -27,7 +27,7 @@ class UserAdminTest(TestCase): request = self.factory.get(reverse('admin2:auth_user_create')) request.user = self.user - model_admin = UserAdmin2(User, djadmin2.default) + model_admin = UserAdmin2(User, djadmin2_site) view = model_admin.create_view.view.as_view( **model_admin.get_create_kwargs()) response = view(request) @@ -48,7 +48,7 @@ class UserAdminTest(TestCase): request = self.factory.get( reverse('admin2:auth_user_update', args=(self.user.pk,))) request.user = self.user - model_admin = UserAdmin2(User, djadmin2.default) + model_admin = UserAdmin2(User, djadmin2_site) view = model_admin.update_view.view.as_view( **model_admin.get_update_kwargs()) response = view(request, pk=self.user.pk) diff --git a/djadmin2/tests/test_core.py b/djadmin2/tests/test_core.py index fe091cd..3a61ec0 100644 --- a/djadmin2/tests/test_core.py +++ b/djadmin2/tests/test_core.py @@ -1,17 +1,12 @@ -from django.db import models -from django.core.exceptions import ImproperlyConfigured -from django.test import TestCase from django.contrib.auth.models import Group, User from django.contrib.sites.models import Site +from django.core.exceptions import ImproperlyConfigured +from django.test import TestCase -import djadmin2 -from ..types import ModelAdmin2 +from djadmin2.site import djadmin2_site +from .models import SmallThing from ..core import Admin2 - - -class SmallThing(models.Model): - pass - +from ..types import ModelAdmin2 APP_LABEL, APP_VERBOSE_NAME = 'app_one_label', 'App One Verbose Name' @@ -71,4 +66,4 @@ class Admin2Test(TestCase): def test_default_entries(self): expected_default_models = (User, Group, Site) for model in expected_default_models: - self.assertTrue(isinstance(djadmin2.default.registry[model], ModelAdmin2)) + self.assertTrue(isinstance(djadmin2_site.registry[model], ModelAdmin2)) diff --git a/djadmin2/tests/test_renderers.py b/djadmin2/tests/test_renderers.py index 76c3ef6..964e695 100644 --- a/djadmin2/tests/test_renderers.py +++ b/djadmin2/tests/test_renderers.py @@ -5,15 +5,11 @@ import datetime as dt from decimal import Decimal from django.test import TestCase -from django.db import models -from django.utils.translation import activate from django.utils import six +from django.utils.translation import activate from .. import renderers - - -class RendererTestModel(models.Model): - decimal = models.DecimalField(decimal_places=5, max_digits=10) +from .models import RendererTestModel class BooleanRendererTest(TestCase): @@ -109,7 +105,7 @@ class NumberRendererTest(TestCase): self.assertEqual('42.5', out) def testEndlessFloat(self): - out = self.renderer(1.0/3, None) + out = self.renderer(1.0 / 3, None) if six.PY2: self.assertEqual('0.333333333333', out) else: diff --git a/djadmin2/tests/test_types.py b/djadmin2/tests/test_types.py index 088f083..a3f7d40 100644 --- a/djadmin2/tests/test_types.py +++ b/djadmin2/tests/test_types.py @@ -1,10 +1,9 @@ -from django.db import models from django.test import TestCase -from django.views.generic import View from .. import views from ..types import ModelAdmin2, immutable_admin_factory from ..core import Admin2 +from .models import BigThing class ModelAdmin(object): @@ -40,10 +39,6 @@ class ImmutableAdminFactoryTests(TestCase): self.immutable_admin.d -class BigThing(models.Model): - pass - - class ModelAdminTest(TestCase): def setUp(self): diff --git a/djadmin2/tests/test_utils.py b/djadmin2/tests/test_utils.py index 6c69d3e..39767ec 100644 --- a/djadmin2/tests/test_utils.py +++ b/djadmin2/tests/test_utils.py @@ -1,27 +1,9 @@ -from django.db import models from django.test import TestCase from django.utils import six from .. import utils from ..views import IndexView - - -class UtilsTestModel(models.Model): - - field1 = models.CharField(max_length=23) - field2 = models.CharField('second field', max_length=42) - - def simple_method(self): - return 42 - - def was_published_recently(self): - return True - was_published_recently.boolean = True - was_published_recently.short_description = 'Published recently?' - - class Meta: - verbose_name = "Utils Test Model" - verbose_name_plural = "Utils Test Models" +from .models import UtilsTestModel class UtilsTest(TestCase): @@ -40,7 +22,7 @@ class UtilsTest(TestCase): self.assertEquals( UtilsTestModel._meta, utils.model_options(UtilsTestModel) - ) + ) UtilsTestModel._meta.verbose_name = "Utils Test Model" UtilsTestModel._meta.verbose_name_plural = "Utils Test Models" @@ -55,7 +37,7 @@ class UtilsTest(TestCase): self.assertEquals( self.instance._meta, utils.model_options(self.instance) - ) + ) self.instance._meta.verbose_name = "Utils Test Model" self.instance._meta.verbose_name_plural = "Utils Test Models" @@ -166,8 +148,6 @@ class UtilsTest(TestCase): "str" ) - - def test_get_attr(self): class Klass(object): attr = "value" diff --git a/djadmin2/tests/test_views.py b/djadmin2/tests/test_views.py index 8609e71..1e25e76 100644 --- a/djadmin2/tests/test_views.py +++ b/djadmin2/tests/test_views.py @@ -1,5 +1,4 @@ from django.test import TestCase -from django.views.generic import View from .. import views diff --git a/djadmin2/types.py b/djadmin2/types.py index 4b36250..22d522c 100644 --- a/djadmin2/types.py +++ b/djadmin2/types.py @@ -1,22 +1,21 @@ # -*- coding: utf-8 -*- from __future__ import division, absolute_import, unicode_literals -from collections import namedtuple import logging import os import sys - -from django.core.urlresolvers import reverse -from django.conf.urls import patterns, url -from django.utils.six import with_metaclass +from collections import namedtuple import extra_views +from django.conf.urls import url +from django.core.urlresolvers import reverse +from django.utils.six import with_metaclass +from . import actions from . import apiviews from . import settings -from . import views -from . import actions from . import utils +from . import views from .forms import modelform_factory @@ -202,7 +201,8 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)): def get_api_list_kwargs(self): kwargs = self.get_default_api_view_kwargs() kwargs.update({ - 'paginate_by': self.list_per_page, + 'queryset': self.model.objects.all(), + # 'paginate_by': self.list_per_page, }) return kwargs @@ -236,11 +236,10 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)): name=self.get_prefixed_view_name(admin_view.name) ) ) - return patterns('', *pattern_list) + return pattern_list def get_api_urls(self): - return patterns( - '', + return [ url( regex=r'^$', view=self.api_list_view.as_view(**self.get_api_list_kwargs()), @@ -252,7 +251,7 @@ class ModelAdmin2(with_metaclass(ModelAdminBase2)): **self.get_api_detail_kwargs()), name=self.get_prefixed_view_name('api_detail'), ), - ) + ] @property def urls(self): diff --git a/djadmin2/utils.py b/djadmin2/utils.py index df7a91e..a831864 100644 --- a/djadmin2/utils.py +++ b/djadmin2/utils.py @@ -3,13 +3,12 @@ from __future__ import division, absolute_import, unicode_literals from collections import defaultdict -from django.db.models import ProtectedError -from django.db.models import ManyToManyRel -from django.db.models.deletion import Collector -from django.db.models.fields.related import ForeignObjectRel +from django.db.models.deletion import Collector, ProtectedError +from django.db.models.sql.constants import QUERY_TERMS from django.utils import six from django.utils.encoding import force_bytes, force_text + def lookup_needs_distinct(opts, lookup_path): """ Returns True if 'distinct()' should be used to query the given lookup path. @@ -17,13 +16,24 @@ def lookup_needs_distinct(opts, lookup_path): This is adopted from the Django core. django-admin2 mandates that code doesn't depend on imports from django.contrib.admin. - https://github.com/django/django/blob/1.5.1/django/contrib/admin/util.py#L20 + https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L22 """ - field_name = lookup_path.split('__', 1)[0] - field = opts.get_field_by_name(field_name)[0] - condition1 = hasattr(field, 'rel') and isinstance(field.rel, ManyToManyRel) - condition2 = isinstance(field, ForeignObjectRel) and not field.field.unique - return condition1 or condition2 + + lookup_fields = lookup_path.split('__') + # Remove the last item of the lookup path if it is a query term + if lookup_fields[-1] in QUERY_TERMS: + lookup_fields = lookup_fields[:-1] + # Now go through the fields (following all relations) and look for an m2m + for field_name in lookup_fields: + field = opts.get_field(field_name) + if hasattr(field, 'get_path_info'): + # This field is a relation, update opts to follow the relation + path_info = field.get_path_info() + opts = path_info[-1].to_opts + if any(path.m2m for path in path_info): + # This field is a m2m relation so we know we need to call distinct + return True + return False def model_options(model): @@ -91,10 +101,8 @@ def get_attr(obj, attr): and the __str__ attribute. """ if attr == '__str__': - if six.PY2: - value = unicode(obj) - else: - value = str(obj) + from builtins import str as text + value = text(obj) else: attribute = getattr(obj, attr) value = attribute() if callable(attribute) else attribute @@ -106,15 +114,14 @@ class NestedObjects(Collector): This is adopted from the Django core. django-admin2 mandates that code doesn't depend on imports from django.contrib.admin. - https://github.com/django/django/blob/1.8c1/django/contrib/admin/utils.py#L160-L221 + https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L171-L231 """ - def __init__(self, *args, **kwargs): super(NestedObjects, self).__init__(*args, **kwargs) self.edges = {} # {from_instance: [to_instances]} self.protected = set() - self.model_count = defaultdict(int) + self.model_objs = defaultdict(set) def add_edge(self, source, target): self.edges.setdefault(source, []).append(target) @@ -129,7 +136,7 @@ class NestedObjects(Collector): self.add_edge(getattr(obj, related_name), obj) else: self.add_edge(None, obj) - self.model_count[obj._meta.verbose_name_plural] += 1 + self.model_objs[obj._meta.model].add(obj) try: return super(NestedObjects, self).collect(objs, source_attr=source_attr, **kwargs) except ProtectedError as e: @@ -157,7 +164,6 @@ class NestedObjects(Collector): def nested(self, format_callback=None): """ Return the graph as a nested list. - """ seen = set() roots = [] @@ -183,14 +189,14 @@ def quote(s): This is adopted from the Django core. django-admin2 mandates that code doesn't depend on imports from django.contrib.admin. - https://github.com/django/django/blob/1.5.1/django/contrib/admin/util.py#L48-L62 + https://github.com/django/django/blob/1.9.6/django/contrib/admin/utils.py#L66-L73 """ if not isinstance(s, six.string_types): return s res = list(s) for i in range(len(res)): c = res[i] - if c in """:/_#?;@&=+$,"<>%\\""": + if c in """:/_#?;@&=+$,"[]<>%\n\\""": res[i] = '_%02X' % ord(c) return ''.join(res) @@ -199,4 +205,4 @@ def type_str(text): if six.PY2: return force_bytes(text) else: - return force_text(text) \ No newline at end of file + return force_text(text) diff --git a/djadmin2/viewmixins.py b/djadmin2/viewmixins.py index 09ef357..cf7888e 100644 --- a/djadmin2/viewmixins.py +++ b/djadmin2/viewmixins.py @@ -116,11 +116,11 @@ class Admin2Mixin(PermissionMixin): return super(Admin2Mixin, self).dispatch(request, *args, **kwargs) -class AdminModel2Mixin(Admin2Mixin): +class Admin2ModelMixin(Admin2Mixin): model_admin = None def get_context_data(self, **kwargs): - context = super(AdminModel2Mixin, self).get_context_data(**kwargs) + context = super(Admin2ModelMixin, self).get_context_data(**kwargs) model = self.get_model() model_meta = model_options(model) app_verbose_names = self.model_admin.admin.app_verbose_names diff --git a/djadmin2/views.py b/djadmin2/views.py index 9a27989..321909c 100644 --- a/djadmin2/views.py +++ b/djadmin2/views.py @@ -2,11 +2,12 @@ from __future__ import division, absolute_import, unicode_literals import operator +from datetime import datetime from functools import reduce -from datetime import datetime - +import extra_views from django.conf import settings +from django.contrib.auth import update_session_auth_hash from django.contrib.auth.forms import (PasswordChangeForm, AdminPasswordChangeForm) from django.contrib.auth.views import (logout as auth_logout, @@ -21,14 +22,13 @@ from django.utils.encoding import force_text from django.utils.text import capfirst from django.utils.translation import ugettext_lazy from django.views import generic -import extra_views - from . import permissions, utils -from .forms import AdminAuthenticationForm -from .viewmixins import Admin2Mixin, AdminModel2Mixin, Admin2ModelFormMixin from .filters import build_list_filter, build_date_filter +from .forms import AdminAuthenticationForm from .models import LogEntry +from .viewmixins import Admin2Mixin, Admin2ModelMixin, Admin2ModelFormMixin + class AdminView(object): @@ -101,7 +101,7 @@ class AppIndexView(Admin2Mixin, generic.TemplateView): return data -class ModelListView(AdminModel2Mixin, generic.ListView): +class ModelListView(Admin2ModelMixin, generic.ListView): """Context Variables :is_paginated: If the page is paginated (page has a next button) @@ -186,7 +186,7 @@ class ModelListView(AdminModel2Mixin, generic.ListView): queryset = self.build_list_filter(queryset).qs if self.model_admin.date_hierarchy: - queryset = self.build_date_filter(queryset).qs + queryset = self.build_date_filter(queryset, self.model_admin.date_hierarchy).qs queryset = self._modify_queryset_for_sort(queryset) @@ -233,7 +233,7 @@ class ModelListView(AdminModel2Mixin, generic.ListView): ) return self._list_filter - def build_date_filter(self, queryset=None): + def build_date_filter(self, queryset=None, field_name=None): if not hasattr(self, "_date_filter"): if queryset is None: queryset = self.get_queryset() @@ -241,6 +241,7 @@ class ModelListView(AdminModel2Mixin, generic.ListView): self.request, self.model_admin, queryset, + field_name ) return self._date_filter @@ -271,38 +272,38 @@ class ModelListView(AdminModel2Mixin, generic.ListView): context["active_day"] = new_date.strftime("%B %d") - context["dates"] = self._format_days(context) + context["dates"] = self._format_days(self.get_queryset()) elif year and month: context["previous_date"] = { "link": "?year=%s" % (year), "text": "‹ %s" % year, } - context["dates"] = self._format_days(context) + context["dates"] = self._format_days(self.get_queryset()) elif year: context["previous_date"] = { "link": "?", "text": ugettext_lazy("‹ All dates"), } - context["dates"] = self._format_months(context) + context["dates"] = self._format_months(self.get_queryset()) else: - context["dates"] = self._format_years(context) + context["dates"] = self._format_years(self.get_queryset()) return context - def _format_years(self, context): - years = self._qs_date_or_datetime(context['object_list'], 'year') + def _format_years(self, queryset): + years = self._qs_date_or_datetime(queryset, 'year') if len(years) == 1: - return self._format_months(context) + return self._format_months(queryset) else: return [ (("?year=%s" % year.strftime("%Y")), year.strftime("%Y")) for year in - self._qs_date_or_datetime(context['object_list'], 'year') + self._qs_date_or_datetime(queryset, 'year') ] - def _format_months(self, context): + def _format_months(self, queryset): return [ ( "?year=%s&month=%s" % ( @@ -310,10 +311,10 @@ class ModelListView(AdminModel2Mixin, generic.ListView): ), date.strftime("%B %Y") ) for date in - self._qs_date_or_datetime(context['object_list'], 'month') + self._qs_date_or_datetime(queryset, 'month') ] - def _format_days(self, context): + def _format_days(self, queryset): return [ ( "?year=%s&month=%s&day=%s" % ( @@ -323,7 +324,7 @@ class ModelListView(AdminModel2Mixin, generic.ListView): ), date.strftime("%B %d") ) for date in - self._qs_date_or_datetime(context['object_list'], 'day') + self._qs_date_or_datetime(queryset, 'day') ] def _qs_date_or_datetime(self, object_list, type): @@ -345,7 +346,7 @@ class ModelListView(AdminModel2Mixin, generic.ListView): return self.model_admin.search_fields -class ModelDetailView(AdminModel2Mixin, generic.DetailView): +class ModelDetailView(Admin2ModelMixin, generic.DetailView): """Context Variables :model: Type of object you are editing @@ -363,7 +364,7 @@ class ModelDetailView(AdminModel2Mixin, generic.DetailView): permissions.ModelViewPermission) -class ModelEditFormView(AdminModel2Mixin, Admin2ModelFormMixin, +class ModelEditFormView(Admin2ModelMixin, Admin2ModelFormMixin, extra_views.UpdateWithInlinesView): """Context Variables @@ -399,7 +400,7 @@ class ModelEditFormView(AdminModel2Mixin, Admin2ModelFormMixin, return response -class ModelAddFormView(AdminModel2Mixin, Admin2ModelFormMixin, +class ModelAddFormView(Admin2ModelMixin, Admin2ModelFormMixin, extra_views.CreateWithInlinesView): """Context Variables @@ -435,7 +436,7 @@ class ModelAddFormView(AdminModel2Mixin, Admin2ModelFormMixin, return response -class ModelDeleteView(AdminModel2Mixin, generic.DeleteView): +class ModelDeleteView(Admin2ModelMixin, generic.DeleteView): """Context Variables :model: Type of object you are editing @@ -461,7 +462,7 @@ class ModelDeleteView(AdminModel2Mixin, generic.DeleteView): opts = utils.model_options(obj) return '%s: %s' % (force_text(capfirst(opts.verbose_name)), force_text(obj)) - + using = router.db_for_write(self.get_object()._meta.model) collector = utils.NestedObjects(using=using) collector.collect([self.get_object()]) @@ -479,7 +480,7 @@ class ModelDeleteView(AdminModel2Mixin, generic.DeleteView): return super(ModelDeleteView, self).delete(request, *args, **kwargs) -class ModelHistoryView(AdminModel2Mixin, generic.ListView): +class ModelHistoryView(Admin2ModelMixin, generic.ListView): """Context Variables :model: Type of object you are editing @@ -541,6 +542,13 @@ class PasswordChangeView(Admin2Mixin, generic.UpdateView): from django.contrib.auth import get_user_model return get_user_model()._default_manager.all() + def form_valid(self, form): + self.object = form.save() + if self.request.user == self.get_object(): + update_session_auth_hash(self.request, form.user) + return HttpResponseRedirect(self.get_success_url()) + + class PasswordChangeDoneView(Admin2Mixin, generic.TemplateView): default_template_name = 'auth/password_change_done.html' diff --git a/docs/installation.rst b/docs/installation.rst index 4d39e3b..e8edc43 100644 --- a/docs/installation.rst +++ b/docs/installation.rst @@ -33,17 +33,17 @@ Add djadmin2 urls to your URLconf: .. code-block:: python # urls.py - from django.conf.urls import patterns, include + from django.conf.urls import include import djadmin2 djadmin2.default.autodiscover() - urlpatterns = patterns( + urlpatterns = [ ... url(r'^admin2/', include(djadmin2.default.urls)), - ) + ] Development Installation ========================= diff --git a/docs/ref/themes.rst b/docs/ref/themes.rst index 26eca22..ded7997 100644 --- a/docs/ref/themes.rst +++ b/docs/ref/themes.rst @@ -8,7 +8,7 @@ How To Create a Theme A Django Admin 2 theme is merely a packaged Django app. Here are the necessary steps to create a theme called '*dandy*': -1. Make sure you have Django 1.5 or higher installed. +1. Make sure you have Django 1.8 or higher installed. ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ .. code-block:: python diff --git a/example/blog/actions.py b/example/blog/actions.py index f090e1a..fcb6e5d 100644 --- a/example/blog/actions.py +++ b/example/blog/actions.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from __future__ import division, absolute_import, unicode_literals -from django.utils.translation import ugettext_lazy, pgettext_lazy from django.contrib import messages +from django.utils.translation import ugettext_lazy, pgettext_lazy -from djadmin2.actions import BaseListAction from djadmin2 import permissions +from djadmin2.actions import BaseListAction diff --git a/example/blog/admin2.py b/example/blog/admin2.py index f71ae01..d63a0ab 100644 --- a/example/blog/admin2.py +++ b/example/blog/admin2.py @@ -3,21 +3,22 @@ from __future__ import division, absolute_import, unicode_literals from django.utils.translation import ugettext_lazy -import djadmin2 from djadmin2 import renderers from djadmin2.actions import DeleteSelectedAction # Import your custom models +from djadmin2.site import djadmin2_site +from djadmin2.types import Admin2TabularInline, ModelAdmin2 from .actions import (CustomPublishAction, PublishAllItemsAction, unpublish_items, unpublish_all_items) from .models import Post, Comment -class CommentInline(djadmin2.Admin2TabularInline): +class CommentInline(Admin2TabularInline): model = Comment -class PostAdmin(djadmin2.ModelAdmin2): +class PostAdmin(ModelAdmin2): list_actions = [ DeleteSelectedAction, CustomPublishAction, PublishAllItemsAction, unpublish_items, @@ -34,7 +35,7 @@ class PostAdmin(djadmin2.ModelAdmin2): ordering = ["-published_date", "title",] -class CommentAdmin(djadmin2.ModelAdmin2): +class CommentAdmin(ModelAdmin2): search_fields = ('body', '=post__title') list_filter = ['post', ] actions_on_top = True @@ -42,12 +43,12 @@ class CommentAdmin(djadmin2.ModelAdmin2): actions_selection_counter = False # Register the blog app with a verbose name -djadmin2.default.register_app_verbose_name( +djadmin2_site.register_app_verbose_name( 'blog', ugettext_lazy('My Blog') ) # Register each model with the admin -djadmin2.default.register(Post, PostAdmin) -djadmin2.default.register(Comment, CommentAdmin) +djadmin2_site.register(Post, PostAdmin) +djadmin2_site.register(Comment, CommentAdmin) diff --git a/example/blog/migrations/0001_initial.py b/example/blog/migrations/0001_initial.py new file mode 100644 index 0000000..6761aea --- /dev/null +++ b/example/blog/migrations/0001_initial.py @@ -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'), + ), + ] diff --git a/example/blog/migrations/__init__.py b/example/blog/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/example/blog/templates/base.html b/example/blog/templates/base.html index 31d2fdf..77743b1 100644 --- a/example/blog/templates/base.html +++ b/example/blog/templates/base.html @@ -1,4 +1,4 @@ -{% load i18n static %} +{% load i18n staticfiles %}
@@ -7,8 +7,8 @@ {% block css %} - - + + {% endblock css %} @@ -19,8 +19,8 @@ {% block javascript %} - - + + {% endblock javascript %} diff --git a/example/blog/tests/test_apiviews.py b/example/blog/tests/test_apiviews.py index aca0e8d..0d4586d 100644 --- a/example/blog/tests/test_apiviews.py +++ b/example/blog/tests/test_apiviews.py @@ -1,16 +1,17 @@ from __future__ import unicode_literals + +import json + from django.contrib.auth.models import AnonymousUser, User from django.core.exceptions import PermissionDenied from django.core.urlresolvers import reverse from django.test import TestCase from django.test.client import RequestFactory from django.utils.encoding import force_text -import json - from djadmin2 import apiviews -from djadmin2 import default -from djadmin2 import ModelAdmin2 +from djadmin2.site import djadmin2_site +from djadmin2.types import ModelAdmin2 from ..models import Post @@ -24,21 +25,21 @@ class APITestCase(TestCase): self.user.save() def get_model_admin(self, model): - return ModelAdmin2(model, default) + return ModelAdmin2(model, djadmin2_site) class IndexAPIViewTest(APITestCase): def test_response_ok(self): request = self.factory.get(reverse('admin2:api_index')) request.user = self.user - view = apiviews.IndexAPIView.as_view(**default.get_api_index_kwargs()) + view = apiviews.IndexAPIView.as_view(**djadmin2_site.get_api_index_kwargs()) response = view(request) self.assertEqual(response.status_code, 200) def test_view_permission(self): request = self.factory.get(reverse('admin2:api_index')) request.user = AnonymousUser() - view = apiviews.IndexAPIView.as_view(**default.get_api_index_kwargs()) + view = apiviews.IndexAPIView.as_view(**djadmin2_site.get_api_index_kwargs()) self.assertRaises(PermissionDenied, view, request) @@ -71,7 +72,7 @@ class ListCreateAPIViewTest(APITestCase): response.render() self.assertEqual(response.status_code, 200) - self.assertIn('"__unicode__": "Foo"', force_text(response.content)) + self.assertIn('"__unicode__":"Foo"', force_text(response.content)) def test_pagination(self): request = self.factory.get(reverse('admin2:blog_post_api_list')) diff --git a/example/blog/tests/test_filters.py b/example/blog/tests/test_filters.py index f3a8986..8f3d3bd 100644 --- a/example/blog/tests/test_filters.py +++ b/example/blog/tests/test_filters.py @@ -1,16 +1,15 @@ # -*- coding: utf-8 -*- # vim:fenc=utf-8 +import django_filters +from django.core.urlresolvers import reverse from django.test import TestCase from django.test.client import RequestFactory -from django.core.urlresolvers import reverse +from djadmin2 import filters as djadmin2_filters +from djadmin2.types import ModelAdmin2 from ..models import Post -import djadmin2 -import djadmin2.filters as djadmin2_filters - -import django_filters class ListFilterBuilderTest(TestCase): @@ -18,11 +17,11 @@ class ListFilterBuilderTest(TestCase): self.rf = RequestFactory() def test_filter_building(self): - class PostAdminSimple(djadmin2.ModelAdmin2): + class PostAdminSimple(ModelAdmin2): list_filter = ['published', ] - class PostAdminWithFilterInstances(djadmin2.ModelAdmin2): + class PostAdminWithFilterInstances(ModelAdmin2): list_filter = [ django_filters.BooleanFilter(name='published'), ] @@ -33,7 +32,7 @@ class ListFilterBuilderTest(TestCase): fields = ['published'] - class PostAdminWithFilterSetInst(djadmin2.ModelAdmin2): + class PostAdminWithFilterSetInst(ModelAdmin2): list_filter = FS Post.objects.create(title="post_1_title", body="body") diff --git a/example/blog/tests/test_modelforms.py b/example/blog/tests/test_modelforms.py index 7d63b11..34cb204 100644 --- a/example/blog/tests/test_modelforms.py +++ b/example/blog/tests/test_modelforms.py @@ -1,10 +1,9 @@ from __future__ import unicode_literals +import floppyforms from django import forms from django.test import TestCase -import floppyforms - from djadmin2.forms import floppify_widget, floppify_form, modelform_factory from ..models import Post @@ -103,7 +102,6 @@ class GetFloppyformWidgetTest(TestCase): floppyforms.widgets.HiddenInput) widget = forms.widgets.HiddenInput() - widget.is_hidden = False self.assertExpectWidget( widget, floppyforms.widgets.HiddenInput, @@ -162,7 +160,7 @@ class GetFloppyformWidgetTest(TestCase): forms.DateInput(), floppyforms.DateInput) - widget = forms.widgets.DateInput(format='DATE_FORMAT') + widget = forms.widgets.DateInput(format='%Y-%m-%d') self.assertExpectWidget( widget, floppyforms.widgets.DateInput, @@ -311,13 +309,13 @@ class GetFloppyformWidgetTest(TestCase): floppyforms.widgets.SplitDateTimeWidget) widget = forms.widgets.SplitDateTimeWidget( - date_format='DATE_FORMAT', time_format='TIME_FORMAT') + date_format='%Y-%m-%d', time_format='TIME_FORMAT') new_widget = floppify_widget(widget) self.assertTrue(isinstance( new_widget.widgets[0], floppyforms.widgets.DateInput)) self.assertTrue(isinstance( new_widget.widgets[1], floppyforms.widgets.TimeInput)) - self.assertEqual(new_widget.widgets[0].format, 'DATE_FORMAT') + self.assertEqual(new_widget.widgets[0].format, '%Y-%m-%d') self.assertEqual(new_widget.widgets[1].format, 'TIME_FORMAT') def test_splithiddendatetime_widget(self): @@ -327,13 +325,13 @@ class GetFloppyformWidgetTest(TestCase): floppyforms.widgets.SplitHiddenDateTimeWidget) widget = forms.widgets.SplitHiddenDateTimeWidget( - date_format='DATE_FORMAT', time_format='TIME_FORMAT') + date_format='%Y-%m-%d', time_format='TIME_FORMAT') new_widget = floppify_widget(widget) self.assertTrue(isinstance( new_widget.widgets[0], floppyforms.widgets.DateInput)) self.assertTrue(isinstance( new_widget.widgets[1], floppyforms.widgets.TimeInput)) - self.assertEqual(new_widget.widgets[0].format, 'DATE_FORMAT') + self.assertEqual(new_widget.widgets[0].format, '%Y-%m-%d') self.assertEqual(new_widget.widgets[0].is_hidden, True) self.assertEqual(new_widget.widgets[1].format, 'TIME_FORMAT') self.assertEqual(new_widget.widgets[1].is_hidden, True) @@ -489,13 +487,13 @@ class FieldWidgetTest(TestCase): self.assertTrue(isinstance(widget, floppyforms.widgets.SlugInput)) self.assertEqual(widget.input_type, 'text') - def test_ipaddress_field(self): + def test_genericipaddress_field(self): class MyForm(forms.ModelForm): - ipaddress = forms.IPAddressField() + ipaddress = forms.GenericIPAddressField() form_class = modelform_factory(model=Post, form=MyForm, exclude=[]) widget = form_class().fields['ipaddress'].widget - self.assertTrue(isinstance(widget, floppyforms.widgets.IPAddressInput)) + self.assertTrue(isinstance(widget, floppyforms.widgets.TextInput)) self.assertEqual(widget.input_type, 'text') def test_splitdatetime_field(self): diff --git a/example/blog/tests/test_nestedobjects.py b/example/blog/tests/test_nestedobjects.py index 1501906..49ab64f 100644 --- a/example/blog/tests/test_nestedobjects.py +++ b/example/blog/tests/test_nestedobjects.py @@ -1,9 +1,8 @@ -from django.db import DEFAULT_DB_ALIAS +from django.db import DEFAULT_DB_ALIAS, router from django.test import TestCase from djadmin2.utils import NestedObjects - -from ..models import Count, Event, EventGuide, Guest, Location +from ..models import Count, Event, EventGuide class NestedObjectsTests(TestCase): @@ -66,7 +65,8 @@ class NestedObjectsTests(TestCase): Check that the nested collector doesn't query for DO_NOTHING objects. """ objs = [Event.objects.create()] - n = NestedObjects(using=None) + using = router.db_for_write(Event._meta.model) + n = NestedObjects(using=using) EventGuide.objects.create(event=objs[0]) with self.assertNumQueries(2): # One for Location, one for Guest, and no query for EventGuide diff --git a/example/blog/tests/test_permissions.py b/example/blog/tests/test_permissions.py index 11c1f7a..8034e26 100644 --- a/example/blog/tests/test_permissions.py +++ b/example/blog/tests/test_permissions.py @@ -1,14 +1,14 @@ +from blog.models import Post from django.contrib.auth.models import User, Permission from django.core.urlresolvers import reverse +from django.shortcuts import get_object_or_404 from django.template import Template, Context from django.test import TestCase from django.test.client import RequestFactory -import djadmin2 -from djadmin2 import ModelAdmin2 from djadmin2.permissions import TemplatePermissionChecker - -from blog.models import Post +from djadmin2.site import djadmin2_site +from djadmin2.types import ModelAdmin2 class TemplatePermissionTest(TestCase): @@ -26,7 +26,7 @@ class TemplatePermissionTest(TestCase): return template.render(context) def test_permission_wrapper(self): - model_admin = ModelAdmin2(Post, djadmin2.default) + model_admin = ModelAdmin2(Post, djadmin2_site) request = self.factory.get(reverse('admin2:blog_post_index')) request.user = self.user permissions = TemplatePermissionChecker(request, model_admin) @@ -48,8 +48,8 @@ class TemplatePermissionTest(TestCase): codename='add_post') self.user.user_permissions.add(post_add_permission) # invalidate the users permission cache - if hasattr(self.user, '_perm_cache'): - del self.user._perm_cache + self.user = get_object_or_404(User, pk=self.user.id) + request.user = self.user result = self.render('{{ permissions.has_add_permission }}', context) self.assertEqual(result, 'True') @@ -61,7 +61,7 @@ class TemplatePermissionTest(TestCase): codename='add_post') self.user.user_permissions.add(post_add_permission) - model_admin = ModelAdmin2(Post, djadmin2.default) + model_admin = ModelAdmin2(Post, djadmin2_site) request = self.factory.get(reverse('admin2:blog_post_index')) request.user = self.user permissions = TemplatePermissionChecker(request, model_admin) @@ -89,8 +89,8 @@ class TemplatePermissionTest(TestCase): self.assertEqual(result, '') def test_admin_binding(self): - user_admin = djadmin2.default.get_admin_by_name('auth_user') - post_admin = djadmin2.default.get_admin_by_name('blog_post') + user_admin = djadmin2_site.get_admin_by_name('auth_user') + post_admin = djadmin2_site.get_admin_by_name('blog_post') request = self.factory.get(reverse('admin2:auth_user_index')) request.user = self.user permissions = TemplatePermissionChecker(request, user_admin) @@ -121,10 +121,11 @@ class TemplatePermissionTest(TestCase): content_type__app_label='blog', content_type__model='post', codename='add_post') + self.user.user_permissions.add(post_add_permission) # invalidate the users permission cache - if hasattr(self.user, '_perm_cache'): - del self.user._perm_cache + self.user = get_object_or_404(User, pk=self.user.id) + request.user = self.user result = self.render( '{% load admin2_tags %}' @@ -155,8 +156,8 @@ class TemplatePermissionTest(TestCase): self.assertEqual(result, '') def test_view_binding(self): - user_admin = djadmin2.default.get_admin_by_name('auth_user') - post_admin = djadmin2.default.get_admin_by_name('blog_post') + user_admin = djadmin2_site.get_admin_by_name('auth_user') + post_admin = djadmin2_site.get_admin_by_name('blog_post') request = self.factory.get(reverse('admin2:auth_user_index')) request.user = self.user permissions = TemplatePermissionChecker(request, user_admin) @@ -203,8 +204,8 @@ class TemplatePermissionTest(TestCase): self.user.user_permissions.add(user_change_permission) # invalidate the users permission cache - if hasattr(self.user, '_perm_cache'): - del self.user._perm_cache + self.user = get_object_or_404(User, pk=self.user.id) + request.user = self.user result = self.render( '{% load admin2_tags %}' @@ -235,7 +236,7 @@ class TemplatePermissionTest(TestCase): self.assertEqual(result, '1True2False34True') def test_object_level_permission(self): - model_admin = ModelAdmin2(Post, djadmin2.default) + model_admin = ModelAdmin2(Post, djadmin2_site) request = self.factory.get(reverse('admin2:blog_post_index')) request.user = self.user permissions = TemplatePermissionChecker(request, model_admin) @@ -264,8 +265,8 @@ class TemplatePermissionTest(TestCase): codename='add_post') self.user.user_permissions.add(post_add_permission) # invalidate the users permission cache - if hasattr(self.user, '_perm_cache'): - del self.user._perm_cache + self.user = get_object_or_404(User, pk=self.user.id) + request.user = self.user # object level permission are not supported by default. So this will # return ``False``. diff --git a/example/blog/tests/test_views.py b/example/blog/tests/test_views.py index 3301908..b154bdc 100644 --- a/example/blog/tests/test_views.py +++ b/example/blog/tests/test_views.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals + from datetime import datetime from django.contrib.auth import get_user_model @@ -515,18 +516,19 @@ class TestAuthViews(TestCase): def test_change_password_for_myself(self): self.client.login(username=self.user.username, password='password') + request = self.client.post(reverse('admin2:password_change', kwargs={'pk': self.user.pk}), {'old_password': 'password', - 'new_password1': 'user', - 'new_password2': 'user'}) + 'new_password1': 'new_password', + 'new_password2': 'new_password'}) self.assertRedirects(request, reverse('admin2:password_change_done')) self.client.logout() self.assertFalse(self.client.login(username=self.user.username, password='password')) self.assertTrue(self.client.login(username=self.user.username, - password='user')) + password='new_password')) def test_change_password(self): self.client.login(username=self.user.username, @@ -538,8 +540,7 @@ class TestAuthViews(TestCase): request = self.client.post(reverse('admin2:password_change', kwargs={'pk': new_user.pk}), - {'old_password': 'new_user', - 'password1': 'new_user_password', + {'password1': 'new_user_password', 'password2': 'new_user_password'}) self.assertRedirects(request, reverse('admin2:password_change_done')) self.client.logout() diff --git a/example/db.sqlite3 b/example/db.sqlite3 new file mode 100644 index 0000000..396feb1 Binary files /dev/null and b/example/db.sqlite3 differ diff --git a/example/example/settings.py b/example/example/settings.py index 7768d08..746ad4d 100644 --- a/example/example/settings.py +++ b/example/example/settings.py @@ -1,68 +1,137 @@ -# Django settings for example project. +""" +Django settings for example project. + +Generated by 'django-admin startproject' using Django 1.9.6. + +For more information on this file, see +https://docs.djangoproject.com/en/1.9/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/1.9/ref/settings/ +""" + import os -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +# Build paths inside the project like this: os.path.join(BASE_DIR, ...) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = '*ymubzn8p_s7vrm%jsqvr6$qnea_5mcp(ao0z-yh1q0gro!0g1' + +# SECURITY WARNING: don't run with debug turned on in production! DEBUG = True -TEMPLATE_DEBUG = DEBUG -ADMINS = ( - # ('Your Name', 'your_email@example.com'), -) +ALLOWED_HOSTS = [] -MANAGERS = ADMINS + +# Application definition + +INSTALLED_APPS = [ + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.sites', + 'django.contrib.messages', + 'django.contrib.staticfiles', + # Uncomment the next line to enable admin documentation: + # 'django.contrib.admindocs', + 'floppyforms', + 'rest_framework', + 'crispy_forms', + 'djadmin2', + 'djadmin2.tests', + 'djadmin2.themes.djadmin2theme_default', + 'blog', + 'files', + 'polls' +] + +MIDDLEWARE_CLASSES = [ + 'django.middleware.security.SecurityMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.locale.LocaleMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'example.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + ], + }, + }, +] + +WSGI_APPLICATION = 'example.wsgi.application' + + +# Database +# https://docs.djangoproject.com/en/1.9/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', - 'NAME': 'example.db', + 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), } } -# Hosts/domain names that are valid for this site; required if DEBUG is False -# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts -ALLOWED_HOSTS = [] -# Local time zone for this installation. Choices can be found here: -# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name -# although not all choices may be available on all operating systems. -# In a Windows environment this must be set to your system time zone. -TIME_ZONE = 'America/Chicago' +# Password validation +# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', + }, + { + 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', + }, +] + + +# Internationalization +# https://docs.djangoproject.com/en/1.9/topics/i18n/ -# Language code for this installation. All choices can be found here: -# http://www.i18nguy.com/unicode/language-identifiers.html LANGUAGE_CODE = 'en-us' +TIME_ZONE = 'UTC' + +USE_I18N = True + +USE_L10N = True + +USE_TZ = True + SITE_ID = 1 -# If you set this to False, Django will make some optimizations so as not -# to load the internationalization machinery. -USE_I18N = True +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/1.9/howto/static-files/ -# If you set this to False, Django will not format dates, numbers and -# calendars according to the current locale. -USE_L10N = True - -# If you set this to False, Django will not use timezone-aware datetimes. -USE_TZ = True - -# Absolute filesystem path to the directory that will hold user-uploaded files. -# Example: "/var/www/example.com/media/" -MEDIA_ROOT = '' - -# URL that handles the media served from MEDIA_ROOT. Make sure to use a -# trailing slash. -# Examples: "http://example.com/media/", "http://media.example.com/" -MEDIA_URL = '' - -# Absolute path to the directory static files should be collected to. -# Don't put anything in this directory yourself; store your static files -# in apps' "static/" subdirectories and in STATICFILES_DIRS. -# Example: "/var/www/example.com/static/" -STATIC_ROOT = '' - -# URL prefix for static files. -# Example: "http://example.com/static/", "http://static.example.com/" STATIC_URL = '/static/' # Additional locations of static files @@ -73,94 +142,12 @@ STATICFILES_DIRS = ( os.path.join(BASE_DIR, "static"), ) -# List of finder classes that know how to find static files in -# various locations. -STATICFILES_FINDERS = ( - 'django.contrib.staticfiles.finders.FileSystemFinder', - 'django.contrib.staticfiles.finders.AppDirectoriesFinder', -# 'django.contrib.staticfiles.finders.DefaultStorageFinder', -) +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') +MEDIA_URL = "/media/" -# Make this unique, and don't share it with anybody. -SECRET_KEY = '*ymubzn8p_s7vrm%jsqvr6$qnea_5mcp(ao0z-yh1q0gro!0g1' +ADMIN2_THEME_DIRECTORY = "djadmin2theme_default" -# List of callables that know how to import templates from various sources. -TEMPLATE_LOADERS = ( - 'django.template.loaders.filesystem.Loader', - 'django.template.loaders.app_directories.Loader', -# 'django.template.loaders.eggs.Loader', -) - -MIDDLEWARE_CLASSES = ( - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.middleware.locale.LocaleMiddleware', - 'django.middleware.common.CommonMiddleware', - 'django.middleware.csrf.CsrfViewMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - # Uncomment the next line for simple clickjacking protection: - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', -) - -ROOT_URLCONF = 'example.urls' - -# Python dotted path to the WSGI application used by Django's runserver. -WSGI_APPLICATION = 'example.wsgi.application' - -TEMPLATE_DIRS = ( - # Put strings here, like "/home/html/django_templates" or "C:/www/django/templates". - # Always use forward slashes, even on Windows. - # Don't forget to use absolute paths, not relative paths. -) - -INSTALLED_APPS = ( - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'django.contrib.messages', - 'django.contrib.staticfiles', - 'django.contrib.admin', - # Uncomment the next line to enable admin documentation: - # 'django.contrib.admindocs', - 'floppyforms', - 'rest_framework', - 'crispy_forms', - 'djadmin2', - 'djadmin2.themes.djadmin2theme_default', - 'blog', - 'files', - 'polls' -) - -# A sample logging configuration. The only tangible logging -# performed by this configuration is to send an email to -# the site admins on every HTTP 500 error when DEBUG=False. -# See http://docs.djangoproject.com/en/dev/topics/logging for -# more details on how to customize your logging configuration. -LOGGING = { - 'version': 1, - 'disable_existing_loggers': False, - 'filters': { - 'require_debug_false': { - '()': 'django.utils.log.RequireDebugFalse' - } - }, - 'handlers': { - 'mail_admins': { - 'level': 'ERROR', - 'filters': ['require_debug_false'], - 'class': 'django.utils.log.AdminEmailHandler' - } - }, - 'loggers': { - 'django.request': { - 'handlers': ['mail_admins'], - 'level': 'ERROR', - 'propagate': True, - }, - } +REST_FRAMEWORK = { + 'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', + 'PAGE_SIZE': 10 } - - -ADMIN2_THEME_DIRECTORY = "djadmin2theme_default" \ No newline at end of file diff --git a/example/example/urls.py b/example/example/urls.py index ad18380..89f0c48 100644 --- a/example/example/urls.py +++ b/example/example/urls.py @@ -1,18 +1,21 @@ -from django.conf.urls import patterns, include, url -from django.contrib import admin +from __future__ import unicode_literals from blog.views import BlogListView, BlogDetailView +from django.conf import settings +from django.conf.urls import include, url +from django.conf.urls.static import static +from django.contrib import admin + +from djadmin2.site import djadmin2_site admin.autodiscover() +djadmin2_site.autodiscover() -import djadmin2 - -djadmin2.default.autodiscover() - -urlpatterns = patterns('', - url(r'^admin2/', include(djadmin2.default.urls)), +urlpatterns = [ + url(r'^admin2/', include(djadmin2_site.urls)), url(r'^admin/', include(admin.site.urls)), url(r'^blog/', BlogListView.as_view(template_name="blog/blog_list.html"), name='blog_list'), url(r'^blog/detail(?P