Skip to content

Commit

Permalink
Updates for Django 2.x
Browse files Browse the repository at this point in the history
  • Loading branch information
David Krauth authored and dakrauth committed Jul 16, 2020
1 parent f13609e commit f397b58
Show file tree
Hide file tree
Showing 11 changed files with 200 additions and 66 deletions.
11 changes: 8 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
*.pyc
__pycache__
.DS_Store
build

*.egg-info
build/
dist/
.python-version
.tox/
*.egg-info/
venv/
db.sqlite3
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
This software is published under the BSD 2-Clause License as listed below.
http://www.opensource.org/licenses/bsd-license.php

Copyright (c) 2014-2019, Atlantic Media
Copyright (c) 2014-2020, Atlantic Media
All rights reserved.

Redistribution and use in source and binary forms, with or without
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

Gives users the option to recover their unsaved changes in the event of a browser crash or lost connection.

**Note:** Version 1.0 supports Django >= 1.11, Python 2.7, Python >= 3.5. Version 2.0 will drop support for Django < 2.0.
> **Note:**
>
> * Version 1.0 supports Django >= 1.11, Python 2.7, >= 3.5.
> * Version 2.0 will drop support for Django < 2.0.
## Setup

Expand Down
94 changes: 48 additions & 46 deletions autosave/mixins.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,43 @@
from __future__ import absolute_import
from __future__ import absolute_import, unicode_literals
import time
import json
import functools
import textwrap
from datetime import datetime
from django.utils.six.moves.urllib.parse import urlparse
from urllib.parse import urlparse

from django import forms
from django.contrib import messages
from django.contrib.admin.models import LogEntry, ADDITION
import six
from django.contrib.admin.utils import unquote
from django.contrib.contenttypes.models import ContentType
from django.urls import reverse
from django.conf.urls import url
from django.core.exceptions import ImproperlyConfigured, PermissionDenied
from django.db.models.fields import FieldDoesNotExist
from django.forms.utils import ErrorDict
from django.http import HttpResponse, Http404
from django.utils.encoding import force_text
from django.utils.html import escape
from django.utils import timezone
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django.utils.functional import cached_property

from . import __version__


class AdminAutoSaveMixin(object):

autosave_last_modified_field = None

@cached_property
def app_model_label(self):
opts = self.model._meta
app = opts.app_label
mod = getattr(opts, 'model_name', None) or getattr(opts, 'module_name', None)
return f'{app}_{mod}'

def get_form(self, request, obj=None, **kwargs):
"""
This is a filthy hack that allows us to return the posted
Expand All @@ -42,66 +53,63 @@ def full_clean(self):

kwargs['form'] = IllegalForm

refresh_action = 'view the original' if obj else 'clear the form'
messages.info(request, mark_safe((
'Successfully loaded from your latest autosave. '
'<a href="">Click here</a> to %(refresh_action)s. '
f'<a href="">Click here</a> to {refresh_action}. '
'<a href="#delete-autosave" class="delete-autosave">[discard autosave]</a>'
) % {
'refresh_action': 'view the original' if obj else 'clear the form',
}))
)))

return super(AdminAutoSaveMixin, self).get_form(request, obj, **kwargs)

def autosave_js(self, request, object_id, extra_context=None):
opts = self.model._meta
info = (opts.app_label, getattr(opts, 'model_name', None) or getattr(opts, 'module_name', None))

try:
object_id = int(unquote(object_id))
except ValueError:
return HttpResponse(u"", status=404, content_type='application/x-javascript')
return HttpResponse(status=404, content_type='application/x-javascript')

obj = None
updated = None

# Raise exception if the admin doesn't have a 'autosave_last_modified_field' property
if not self.autosave_last_modified_field:
raise ImproperlyConfigured((
u"Autosave is not configured correctly. %(cls_name)s "
u"is missing property 'autosave_last_modified_field', which "
u"should be set to the model's last updated datetime field.") % {
'cls_name': ".".join([self.__module__, self.__class__.__name__]),
})
cls_name = f"{self.__module__}.{self.__class__.__name__}"
raise ImproperlyConfigured(
f"Autosave is not configured correctly. {cls_name} "
"is missing property 'autosave_last_modified_field', which "
"should be set to the model's last updated datetime field.")

# Raise exception if self.autosave_last_modified_field is not set
try:
opts.get_field(self.autosave_last_modified_field)
self.model._meta.get_field(self.autosave_last_modified_field)
except FieldDoesNotExist:
raise

prefix = self.app_model_label
if not object_id:
autosave_url = reverse("admin:%s_%s_add" % info)
autosave_url = reverse(f"admin:{prefix}_add")
add_log_entries = LogEntry.objects.filter(
user=request.user,
content_type=ContentType.objects.get_for_model(self.model),
action_flag=ADDITION)
user=request.user,
content_type=ContentType.objects.get_for_model(self.model),
action_flag=ADDITION)
try:
updated = add_log_entries[0].action_time
except IndexError:
pass
else:
autosave_url = reverse("admin:%s_%s_change" % info, args=[str(object_id)])
autosave_url = reverse(f"admin:{prefix}_change", args=[str(object_id)])
try:
obj = self.get_object(request, object_id)
except (ValueError, self.model.DoesNotExist):
raise Http404(_('%(name)s object with primary key %(key)r does not exist.') % {
'name': force_text(opts.verbose_name),
'key': escape(object_id),
})
name = force_text(opts.verbose_name)
key = escape(object_id)
raise Http404(_(f'{name} object with primary key {key} does not exist.'))
else:
updated = getattr(obj, self.autosave_last_modified_field, None)
# Make sure date modified time doesn't predate Unix-time.
if updated:
if timezone.is_aware(updated):
updated = timezone.make_naive(updated)
# I'm pretty confident they didn't do any Django autosaving in 1969.
updated = max(updated, datetime(year=1970, month=1, day=1))

Expand Down Expand Up @@ -133,13 +141,6 @@ def autosave_js(self, request, object_id, extra_context=None):

def get_urls(self):
"""Adds a last-modified checker to the admin urls."""
try:
from django.conf.urls.defaults import url
except ImportError:
from django.conf.urls import url

opts = self.model._meta
info = (opts.app_label, getattr(opts, 'model_name', None) or getattr(opts, 'module_name', None))

# Use admin_site.admin_view to add permission checking
def wrap(view):
Expand All @@ -149,10 +150,14 @@ def wrapper(*args, **kwargs):

# This has to be \w because if it's not, parameters following the obj_id will be
# caught up in the regular change_view url pattern, and 500.
prefix = self.app_model_label
opts = self.model._meta
info = (opts.app_label, getattr(opts, 'model_name', None) or getattr(opts, 'module_name', None))
name = "%s_%s_autosave_js" % info
return [
url(r'^(.+)/autosave_variables\.js',
wrap(self.autosave_js),
name="%s_%s_autosave_js" % info),
name=name),
] + super(AdminAutoSaveMixin, self).get_urls()

def autosave_media(self, obj=None, get_params=''):
Expand All @@ -162,14 +167,11 @@ def autosave_media(self, obj=None, get_params=''):
This can be appended to the media in add_view and change_view, and
enables us to pull autosave information specific to a given object.
"""
opts = self.model._meta
info = (opts.app_label, getattr(opts, 'model_name', None) or getattr(opts, 'module_name', None))

prefix = self.app_model_label
pk = getattr(obj, 'pk', None) or 0

return forms.Media(js=(
reverse('admin:%s_%s_autosave_js' % info, args=[pk]) + get_params,
"autosave/js/autosave.js?v=3",
reverse(f'admin:{prefix}_autosave_js', args=[pk]) + get_params,
f'autosave/js/autosave.js?v={__version__}',
))

def set_autosave_flag(self, request, response):
Expand All @@ -194,12 +196,12 @@ def response_change(self, request, obj):

def render_change_form(self, request, context, add=False, obj=None, **kwargs):
if 'media' in context:
get_params = u''
get_params = ''
if 'is_retrieved_from_autosave' in request.POST:
get_params = u'?is_recovered=1'
get_params = '?is_recovered=1'
autosave_media = self.autosave_media(obj, get_params=get_params)
if isinstance(context['media'], six.string_types):
autosave_media = six.text_type(autosave_media)
if isinstance(context['media'], str):
autosave_media = str(autosave_media)
context['media'] += autosave_media
return super(AdminAutoSaveMixin, self).render_change_form(
request, context, add=add, obj=obj, **kwargs)
16 changes: 11 additions & 5 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
[bumpversion]
current_version = 1.0.0
commit = True
tag = True
[bdist_wheel]
universal = 1

[bumpversion:file:setup.py]
[flake8]
max-line-length = 100
ignore = E722, E128, E126

[tool:pytest]
python_files = tests.py test_*.py *_test.py
DJANGO_SETTINGS_MODULE = tests.settings
addopts = --tb=short --create-db
django_find_project = false
testpaths = tests
19 changes: 9 additions & 10 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,20 @@
from __future__ import absolute_import
from setuptools import setup, find_packages

import autosave

setup(
name="Django Autosave",
version="1.0.0",
author='Jason Goldstein',
author_email='jason@betheshoe.com',
version='2.0.0',
author='The Atlantic',
author_email='programmers@theatlantic.com',
url='https://github.com/theatlantic/django-autosave',
packages=['autosave'],
description='Generic autosave for the Django Admin.',
long_description=open('README.md').read(),
long_description_content_type='text/markdown',
install_requires=['Django>=1.11'],
python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4',
install_requires=['Django>=2.0'],
python_requires='>=3.7,<4',
classifiers=[
'Development Status :: 5 - Production',
'License :: OSI Approved :: BSD License',
Expand All @@ -23,14 +25,11 @@
'Operating System :: OS Independent',
'Programming Language :: Python',
'Framework :: Django',
'Framework :: Django :: 1.11',
'Framework :: Django :: 2.0',
'Framework :: Django :: 2.1',
'Programming Language :: Python :: 2',
'Programming Language :: Python :: 2.7',
'Framework :: Django :: 2.2',
'Framework :: Django :: 3.0',
'Programming Language :: Python :: 3',
'Programming Language :: Python :: 3.5',
'Programming Language :: Python :: 3.6',
'Programming Language :: Python :: 3.7',
],
include_package_data=True,
Expand Down
Empty file added tests/__init__.py
Empty file.
21 changes: 21 additions & 0 deletions tests/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from django.db import models
from django.contrib import admin
from django.conf.urls import url

from autosave.mixins import AdminAutoSaveMixin


class MyModel(models.Model):
name = models.CharField(max_length=50)
date_modified = models.DateTimeField(auto_now=True)


@admin.register(MyModel)
class MyAdmin(AdminAutoSaveMixin, admin.ModelAdmin):
autosave_last_modified_field = 'date_modified'


admin.autodiscover()
urlpatterns = [
url(r'^admin/', admin.site.urls),
]
58 changes: 58 additions & 0 deletions tests/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
from pathlib import Path
import sys

BASE_DIR = Path(__file__).parents[1]

ALLOWED_HOSTS = []
AUTH_PASSWORD_VALIDATORS = []
DEBUG = True
LANGUAGE_CODE = 'en-us'
MEDIA_URL = '/media/'
ROOT_URLCONF = 'tests.models'
SECRET_KEY = 'supersecretkey'
STATIC_URL = '/static/'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_L10N = True
USE_TZ = False

DATABASES = {'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': ':memory:' if 'pytest' in sys.argv else 'db.sqlite3',
}}

INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',

'autosave',
'tests',
]

MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

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',
],
},
}]
17 changes: 17 additions & 0 deletions tests/test_autosave.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import pytest
from .models import MyModel


@pytest.mark.django_db
def test_smoke(admin_client):
my = MyModel.objects.create(name='name')
rsp = admin_client.get(f'/admin/tests/mymodel/{my.id}/change/')
assert rsp.status_code == 200

html = rsp.content.decode()
assert f'src="/admin/tests/mymodel/{my.id}/autosave_variables.js' in html
assert f'src="/static/autosave/js/autosave.js' in html

rsp = admin_client.get(f'/admin/tests/mymodel/{my.id}/autosave_variables.js')
assert rsp.status_code == 200
assert b'var DjangoAutosave' in rsp.content
Loading

0 comments on commit f397b58

Please sign in to comment.