Repository: carpentries/amy

Issue:
Django 1.8 changes
Hi,

Django 1.8 is out and contains some backward-incompatible changes, for example new stuff for [templates](https://docs.djangoproject.com/en/1.8/ref/templates/upgrading/).

We should check what's working and what has stopped working in Amy with Django 1.8.

We should also decide if we keep Amy compatible with Django 1.7, or Django 1.8, or both.



Discussion:
On Fri, Apr 03, 2015 at 12:56:44PM -0700, Piotr Banaszkiewicz wrote:

> Django 1.8 is out and contains some backward-incompatible changes…

Previous Django 1.8 discussion in [1,2,3,4].

> We should check what's working and what has stopped working in Amy
> with Django 1.8.

We don't use all that much Django, so we should be able to handle this
by:
- Reading the 1.8 release notes [5](https://docs.djangoproject.com/en/1.8/releases/1.8/) and updating amy accordingly.
- Running the test suite after bumping Django.

> We should also decide if we keep Amy compatible with Django 1.7, or
> Django 1.8, or both.

I'm fine bumping to 1.8 and dropping 1.7 support, but we may want
input from our sysadmin first.  @drio (I think your still the amy
sysadmin ;), do you have any concerns about bumping the stack?  If
you've installed Django using pip I don't foresee any problems
shifting to 1.8.

Issues I found after running the test suite:
1. We need to run migrations (`makemigrations` and then `migrate`) because of https://docs.djangoproject.com/en/1.8/releases/1.8/#abstractuser-last-login-allows-null-values
2. We need to do something about our forms: #240 

After fixing 1. and running the test suite I only had some issues with the forms. I did not check them out so they may or may not be connected to #240.


Resolving patch:
diff --git a/amy/settings.py b/amy/settings.py
--- a/amy/settings.py
+++ b/amy/settings.py
@@ -12,8 +12,6 @@
 import os
 import json
 
-from django.conf import global_settings
-
 BASE_DIR = os.path.dirname(os.path.dirname(__file__))
 
 
@@ -33,7 +31,31 @@
 SECRET_KEY = os.environ.get('AMY_SECRET_KEY', SECRET_KEY)
 
 
-TEMPLATE_DEBUG = True
+# New template settings (for Django >= 1.8)
+TEMPLATES = [
+    {
+        'BACKEND': 'django.template.backends.django.DjangoTemplates',
+        'APP_DIRS': True,
+        'OPTIONS': {
+            'debug': DEBUG,
+
+            # default processors + a request processor
+            'context_processors': [
+                'django.contrib.auth.context_processors.auth',
+                'django.template.context_processors.debug',
+                'django.template.context_processors.i18n',
+                'django.template.context_processors.media',
+                'django.template.context_processors.static',
+                'django.template.context_processors.tz',
+                'django.contrib.messages.context_processors.messages',
+                'django.core.context_processors.request',
+            ],
+
+            # Warn viewers of invalid template strings
+            'string_if_invalid': 'XXX-unset-variable-XXX',
+        }
+    }
+]
 
 ALLOWED_HOSTS = [
     'software-carpentry.org',
@@ -70,12 +92,6 @@
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
 )
 
-# WARNING: in Django 1.8 processors get moved from ``django.core`` to
-#          ``django.template``.
-TEMPLATE_CONTEXT_PROCESSORS = global_settings.TEMPLATE_CONTEXT_PROCESSORS + (
-    "django.core.context_processors.request",
-)
-
 ROOT_URLCONF = 'amy.urls'
 
 WSGI_APPLICATION = 'amy.wsgi.application'
@@ -126,9 +142,6 @@
     os.path.join(BASE_DIR, 'bower_components'),
 )
 
-# Warn viewers of invalid template strings
-TEMPLATE_STRING_IF_INVALID = 'XXX-unset-variable-XXX'
-
 # if "next" (or "?next") variable is not set when logging in, redirect to
 # workshops
 LOGIN_REDIRECT_URL = '/workshops/'
diff --git a/workshops/migrations/0005_auto_20150404_0953.py b/workshops/migrations/0005_auto_20150404_0953.py
new file mode 100644
--- /dev/null
+++ b/workshops/migrations/0005_auto_20150404_0953.py
@@ -0,0 +1,24 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from django.db import models, migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('workshops', '0004_merge'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='person',
+            name='groups',
+            field=models.ManyToManyField(help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', blank=True, to='auth.Group', related_name='user_set', related_query_name='user', verbose_name='groups'),
+        ),
+        migrations.AlterField(
+            model_name='person',
+            name='last_login',
+            field=models.DateTimeField(blank=True, null=True, verbose_name='last login'),
+        ),
+    ]


Test patch:
diff --git a/workshops/test/base.py b/workshops/test/base.py
--- a/workshops/test/base.py
+++ b/workshops/test/base.py
@@ -178,12 +178,13 @@ def _parse(self, response, save_to=None):
                 writer.write(content)
 
         # Report unfilled tags.
-        if settings.TEMPLATE_STRING_IF_INVALID in content:
+        STRING_IF_INVALID = \
+            settings.TEMPLATES[0]['OPTIONS']['string_if_invalid']
+        if STRING_IF_INVALID in content:
             self._save_html(content)
             lines = content.split('\n')
-            hits = [x for x in enumerate(lines)
-                    if settings.TEMPLATE_STRING_IF_INVALID in x[1]]
-            msg = '"{0}" found in HTML page:\n'.format(settings.TEMPLATE_STRING_IF_INVALID)
+            hits = [x for x in enumerate(lines) if STRING_IF_INVALID in x[1]]
+            msg = '"{0}" found in HTML page:\n'.format(STRING_IF_INVALID)
             assert not hits, msg + '\n'.join(['{0}: "{1}"'.format(h[0], h[1].rstrip())
                                               for h in hits])
 
@@ -263,7 +264,7 @@ def _get_form_data(self, doc):
 
         inputs = dict([(i.attrib['name'], i.attrib.get('value', None))
                        for i in form.findall(".//input[@type='text']")])
-        
+
         hidden = dict([(i.attrib['name'], i.attrib.get('value', None))
                        for i in form.findall(".//input[@type='hidden']")])