Merge pull request #2 from EbookFoundation/dj111

update for Django 1.11/python 3.6
dj111py38
Eric Hellman 2020-03-18 16:56:22 -04:00 committed by GitHub
commit 6672fa575b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 322 additions and 191 deletions

12
.gitignore vendored
View File

@ -60,3 +60,15 @@ atlassian-ide-plugin.xml
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
# Python packaging
.eggs*
.env
.tox*
build*
dist*
*.egg-info/*
log/log.txt
*.log
db.sqlite3
.python-version

12
Pipfile Normal file
View File

@ -0,0 +1,12 @@
[[source]]
name = "pypi"
url = "https://pypi.org/simple"
verify_ssl = true
[dev-packages]
[packages]
fef-questionnaire = {editable = true,path = "."}
[requires]
python_version = "3.6"

81
Pipfile.lock generated Normal file
View File

@ -0,0 +1,81 @@
{
"_meta": {
"hash": {
"sha256": "3f3bbecd60d8aa8a4e96361e34be69113185857815516ad894e3d6bf44ee9bea"
},
"pipfile-spec": 6,
"requires": {
"python_version": "3.6"
},
"sources": [
{
"name": "pypi",
"url": "https://pypi.org/simple",
"verify_ssl": true
}
]
},
"default": {
"django": {
"hashes": [
"sha256:a3b01cdff845a43830d7ccacff55e0b8ff08305a4cbf894517a686e53ba3ad2d",
"sha256:b33ce35f47f745fea6b5aa3cf3f4241069803a3712d423ac748bd673a39741eb"
],
"version": "==1.11.28"
},
"django-compat": {
"hashes": [
"sha256:3ac9a3bedc56b9365d9eb241bc5157d0c193769bf995f9a78dc1bc24e7c2331b"
],
"version": "==1.0.15"
},
"django-transmeta-eh": {
"hashes": [
"sha256:cbe504f73e6c7cfed5c23d883db6c28efe2f2e0cdddf33a68c343d1fd862fa01"
],
"version": "==0.7.6"
},
"fef-questionnaire": {
"editable": true,
"path": "."
},
"pyparsing": {
"hashes": [
"sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
"sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
],
"version": "==2.4.6"
},
"pytz": {
"hashes": [
"sha256:1c557d7d0e871de1f5ccd5833f60fb2550652da6be2693c1e02300743d21500d",
"sha256:b02c06db6cf09c12dd25137e563b31700d3b80fcc4ad23abb7a315f2789819be"
],
"version": "==2019.3"
},
"pyyaml": {
"hashes": [
"sha256:059b2ee3194d718896c0ad077dd8c043e5e909d9180f387ce42012662a4946d6",
"sha256:1cf708e2ac57f3aabc87405f04b86354f66799c8e62c28c5fc5f88b5521b2dbf",
"sha256:24521fa2890642614558b492b473bee0ac1f8057a7263156b02e8b14c88ce6f5",
"sha256:4fee71aa5bc6ed9d5f116327c04273e25ae31a3020386916905767ec4fc5317e",
"sha256:70024e02197337533eef7b85b068212420f950319cc8c580261963aefc75f811",
"sha256:74782fbd4d4f87ff04159e986886931456a1894c61229be9eaf4de6f6e44b99e",
"sha256:940532b111b1952befd7db542c370887a8611660d2b9becff75d39355303d82d",
"sha256:cb1f2f5e426dc9f07a7681419fe39cee823bb74f723f36f70399123f439e9b20",
"sha256:dbbb2379c19ed6042e8f11f2a2c66d39cceb8aeace421bfc29d085d93eda3689",
"sha256:e3a057b7a64f1222b56e47bcff5e4b94c4f61faac04c7c4ecb1985e18caa3994",
"sha256:e9f45bd5b92c7974e59bcd2dcc8631a6b6cc380a904725fce7bc08872e691615"
],
"version": "==5.3"
},
"six": {
"hashes": [
"sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
"sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
],
"version": "==1.14.0"
}
},
"develop": {}
}

View File

@ -73,11 +73,11 @@ Install Django
Create your Django site
django-admin.py startproject mysite
django-admin.py startproject example
Create a place for the questionnare
cd mysite
cd example
mkdir apps
cd apps
@ -95,7 +95,7 @@ The next step is to install the questionnaire.
If you are working with ed-questionnaire from your own fork you may want to use `python setup.py develop` instead, which will save you from running `python setup.py install` every time the questionnaire changes.
Now let's configure your basic questionnaire OR copy the settings.py, urls.py, and models.py files from the "example" folder into `mysite/mysite`, then skip down to [initialize your database](#initialize-the-database).
Now let's configure your basic questionnaire OR copy the settings.py, urls.py, and models.py files from the "example" folder into `example/example`, then skip down to [initialize your database](#initialize-the-database).
Also add the locale and request cache middleware to MIDDLEWARE_CLASSES:
@ -104,7 +104,7 @@ Also add the locale and request cache middleware to MIDDLEWARE_CLASSES:
Add the questionnaire template directory as well as your own to TEMPLATES:
'DIRS': [os.path.join(BASE_DIR, 'mysite/templates/')],
'DIRS': [os.path.join(BASE_DIR, 'example/templates/')],
If you want to use multiple languages, add the i18n context processor to TEMPLATES
'context_processors': ['django.template.context_processors.i18n',]
@ -119,12 +119,12 @@ To finish the settings, add the fef-questionaire specific parameters. For our ex
QUESTIONNAIRE_PROGRESS = 'async'
QUESTIONNAIRE_USE_SESSION = False
QUESTIONNAIRE_ITEM_MODEL = 'mysite.Book'
QUESTIONNAIRE_ITEM_MODEL = 'example.Book'
QUESTIONNAIRE_SHOW_ITEM_RESULTS = True
Next up we want to edit the `urls.py` file of your project to link the questionnaire views to your site's url configuration. The example app shows you how.
Finally, we want to add a model to the mysite app for us to link our questionnaires to. It needs to have a back-relation named "items"
Finally, we want to add a model to the example app for us to link our questionnaires to. It needs to have a back-relation named "items"
class Book(models.Model):
title = models.CharField(max_length=1000, default="")
@ -135,13 +135,13 @@ Finally, we want to add a model to the mysite app for us to link our questionnai
### Initialize the database
Having done that we can initialize our database. (For this to work you must have set up your DATABASES in `settings.py`.). First, in your CLI navigate back to the `mysite` folder:
Having done that we can initialize our database. (For this to work you must have set up your DATABASES in `settings.py`.). First, in your CLI navigate back to the `example` folder:
cd ../..
The check that you are in the proper folder, type `ls`: if you can see `manage.py` in your list of files, you are good. Otherwise, find your way to the folder that contains that file. Then type:
python manage.py syncdb
python manage.py migrate
You will be asked to create a superuser.
@ -151,9 +151,9 @@ Congratulations, you have setup the basics of the questionnaire! At this point t
### Internationalizating the database
First, you want to setup the languages used in your questionnaire. Open up your `mysite` folder in your favorite text editor.
First, you want to setup the languages used in your questionnaire. Open up your `example` folder in your favorite text editor.
Open `mysite/mysite/settings.py` and add following lines, representing your languages of choice:
Open `example/example/settings.py` and add following lines, representing your languages of choice:
LANGUAGES = (
('en', 'English'),
@ -351,4 +351,13 @@ Version 4.0 has not been tested for compatibility with previous versions.
* documentation has been updated to reflect Django 1.8.
* email and subject functionality has not been tested
4.0.1
---------------
Updated for Django 1.11
5.0
---------------
Updated for Python 3.6

View File

@ -1,60 +1,60 @@
- fields: {title: "A Christmas Carol"}
model: mysite.book
model: example.book
pk: 4
- fields: {title: "A Little Princess"}
model: mysite.book
model: example.book
pk: 5
- fields: {title: "A Portrait of the Artist as a Young Man"}
model: mysite.book
model: example.book
pk: 6
- fields: {title: "A Room with a View"}
model: mysite.book
model: example.book
pk: 7
- fields: {title: "Agnes Grey"}
model: mysite.book
model: example.book
pk: 8
- fields: {title: "Anne of Green Gables"}
model: mysite.book
model: example.book
pk: 9
- fields: {title: "The Personal History, Adventures, Experience and Observation of David Copperfield the Younger of Blunderstone Rookery"}
model: mysite.book
model: example.book
pk: 10
- fields: {title: "Far From the Madding Crowd"}
model: mysite.book
model: example.book
pk: 11
- fields: {title: "Howards End"}
model: mysite.book
model: example.book
pk: 12
- fields: {title: "Jacob\'s Room"}
model: mysite.book
model: example.book
pk: 13
- fields: {title: "The Merry Adventures of Robin Hood"}
model: mysite.book
model: example.book
pk: 14
- fields: {title: "Sense and Sensibility"}
model: mysite.book
model: example.book
pk: 15
- fields: {title: "The Secret Garden"}
model: mysite.book
model: example.book
pk: 16
- fields: {title: "The Adventures of Tom Sawyer"}
model: mysite.book
model: example.book
pk: 17
- fields: {title: "The Invisible Man"}
model: mysite.book
model: example.book
pk: 18
- fields: {title: "The Last of the Mohicans"}
model: mysite.book
model: example.book
pk: 19
- fields: {title: "Oliver Twist, or the Parish Boy\'s Progress"}
model: mysite.book
model: example.book
pk: 20
- fields: {title: "Peter Pan in Kensington Gardens"}
model: mysite.book
model: example.book
pk: 21
- fields: {title: "Tales of the Jazz Age"}
model: mysite.book
model: example.book
pk: 22
- fields: {title: "Tess of the D'Urbervilles: A Pure Woman Faithfully Presented"}
model: mysite.book
model: example.book
pk: 23

View File

@ -6,5 +6,8 @@ from questionnaire.models import Landing
class Book(models.Model):
title = models.CharField(max_length=1000, default="")
landings = GenericRelation(Landing, related_query_name='items')
def __unicode__(self):
return self.title
__str__ = __unicode__

View File

@ -1,5 +1,5 @@
"""
Django settings for mysite project.
Django settings for example project.
Generated by 'django-admin startproject' using Django 1.8.18.
@ -40,7 +40,7 @@ INSTALLED_APPS = (
'transmeta',
'questionnaire',
'questionnaire.page',
'mysite',
'example',
)
MIDDLEWARE_CLASSES = (
@ -56,12 +56,12 @@ MIDDLEWARE_CLASSES = (
'django.middleware.security.SecurityMiddleware',
)
ROOT_URLCONF = 'mysite.urls'
ROOT_URLCONF = 'example.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR, 'mysite/templates/')],
'DIRS': [os.path.join(BASE_DIR, 'example/templates/')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@ -75,7 +75,7 @@ TEMPLATES = [
},
]
WSGI_APPLICATION = 'mysite.wsgi.application'
WSGI_APPLICATION = 'example.wsgi.application'
# Database
@ -145,7 +145,7 @@ QUESTIONNAIRE_USE_SESSION = False
# for item-linked questionnaires, defines the model used for the item-linked questionaires.
QUESTIONNAIRE_ITEM_MODEL = 'mysite.Book'
QUESTIONNAIRE_ITEM_MODEL = 'example.Book'
# for item-linked questionnaires, show the results to any logged in user. If the results are meant to be private, this should be false, and you should wrap the corresponding views with access control appropriate to your application.

View File

@ -1,16 +1,16 @@
from django.conf.urls import patterns, include, url
from django.conf.urls import include, url
from django.contrib import admin
import questionnaire
from questionnaire.page import views
admin.autodiscover()
urlpatterns = patterns('',
url(r'^$', 'questionnaire.page.views.page', {'page_to_render' : 'index'}),
urlpatterns = [
url(r'^$', views.page, {'page_to_render' : 'index'}),
url(r'q/', include('questionnaire.urls')),
# admin
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
url(r'^admin/', include(admin.site.urls)),
)
]

View File

@ -1,6 +1,6 @@
from django.utils.translation import ugettext as _
from django.contrib import admin
from django.core.urlresolvers import reverse
from django.urls import reverse
from .models import (Choice, Questionnaire, Question, QuestionSet, Subject,
RunInfo, RunInfoHistory, Answer, DBStylesheet, Landing)

View File

@ -3,17 +3,17 @@
Functions to send email reminders to users.
"""
import random, time, smtplib, rfc822
import random, time, smtplib
from datetime import datetime
from email.Header import Header
from email.Utils import formataddr, parseaddr
from email.header import Header
from email.utils import formataddr, parseaddr
from django.core.mail import get_connection, EmailMessage
from django.contrib.auth.decorators import login_required
from django.template import loader
from django.utils import translation
from django.conf import settings
from django.http import Http404, HttpResponse
from django.shortcuts import render_to_response, get_object_or_404
from django.shortcuts import get_object_or_404
from .models import Subject, QuestionSet, RunInfo, Questionnaire
try: from hashlib import md5
@ -37,7 +37,7 @@ def _new_random(subject):
Returns: subject_id + 'z' +
md5 hexdigest of subject's surname, nextrun date, and a random number
"""
return "%dz%s" % (subject.id, md5(subject.surname + str(subject.nextrun) + hex(random.randint(1,999999))).hexdigest()[:6])
return "%dz%s" % (subject.id, md5(bytes(subject.surname + str(subject.nextrun) + hex(random.randint(1,999999)), 'utf-8')).hexdigest()[:6])
def _new_runinfo(subject, questionset):
@ -148,7 +148,7 @@ def send_emails(request=None, qname=None):
outlog.append(u"[%s] %s, %s: OK" % (r.run.runid, r.subject.surname, r.subject.givenname))
else:
outlog.append(u"[%s] %s, %s: %s" % (r.run.runid, r.subject.surname, r.subject.givenname, r.lastemailerror))
except Exception, e:
except Exception as e:
outlog.append("Exception: [%s] %s: %s" % (r.run.runid, r.subject.surname, str(e)))
if request:
return HttpResponse("Sent Questionnaire Emails:\n "

View File

@ -38,7 +38,7 @@ def _load_template_source(template_name, template_dirs=None):
error_msg = "Tried %s" % tried
else:
error_msg = "Your TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory."
raise TemplateDoesNotExist, error_msg
raise TemplateDoesNotExist(error_msg)
def load_template_source(template_name, template_dirs=None):
"""Assuming the current language is German.

View File

@ -86,7 +86,7 @@ class TypeTest(TestCase):
response = c.post('/q/test:test/1/', ansdict)
self.assertEqual(response.status_code, 200)
errors = response.context[-1]['errors']
self.assertEqual(len(errors), 1) and errors.has_key('3')
self.assertEqual(len(errors), 1) and '3' in errors
def test050_missing_question(self):

View File

@ -1,3 +1,4 @@
from __future__ import print_function
from django.core.management.base import BaseCommand
from ...models import Landing
@ -10,5 +11,5 @@ class Command(BaseCommand):
how_many=int(how_many)
while how_many > 0:
landing = Landing.objects.create(label = label)
print landing.nonce
print(landing.nonce)
how_many -= 1

View File

@ -1,3 +1,4 @@
from __future__ import print_function
from django.core.management.base import NoArgsCommand
class Command(NoArgsCommand):
@ -5,4 +6,4 @@ class Command(NoArgsCommand):
from ...emails import send_emails
res = send_emails()
if res:
print res
print(res)

View File

@ -51,7 +51,7 @@ class Migration(migrations.Migration):
('nonce', models.CharField(max_length=32, null=True, blank=True)),
('object_id', models.PositiveIntegerField(null=True, blank=True)),
('label', models.CharField(max_length=64, blank=True)),
('content_type', models.ForeignKey(related_name='landings', blank=True, to='contenttypes.ContentType', null=True)),
('content_type', models.ForeignKey(on_delete=models.CASCADE, related_name='landings', blank=True, to='contenttypes.ContentType', null=True)),
],
),
migrations.CreateModel(
@ -91,7 +91,7 @@ class Migration(migrations.Migration):
('checks', models.CharField(help_text=b'Current options are \'femaleonly\' or \'maleonly\' and shownif="QuestionNumber,Answer" which takes the same format as <tt>requiredif</tt> for questions.', max_length=256, blank=True)),
('text_en', models.TextField(help_text=b'HTML or Text', null=True, verbose_name='Text', blank=True)),
('parse_html', models.BooleanField(default=False, verbose_name=b'Render html in heading?')),
('questionnaire', models.ForeignKey(to='questionnaire.Questionnaire')),
('questionnaire', models.ForeignKey(on_delete=models.CASCADE, to='questionnaire.Questionnaire')),
],
),
migrations.CreateModel(
@ -108,8 +108,8 @@ class Migration(migrations.Migration):
('cookies', models.TextField(null=True, blank=True)),
('tags', models.TextField(help_text='Tags active on this run, separated by commas', blank=True)),
('skipped', models.TextField(help_text='A comma sepearted list of questions to skip', blank=True)),
('landing', models.ForeignKey(blank=True, to='questionnaire.Landing', null=True)),
('questionset', models.ForeignKey(blank=True, to='questionnaire.QuestionSet', null=True)),
('landing', models.ForeignKey(on_delete=models.CASCADE, blank=True, to='questionnaire.Landing', null=True)),
('questionset', models.ForeignKey(on_delete=models.CASCADE, blank=True, to='questionnaire.QuestionSet', null=True)),
],
options={
'verbose_name_plural': 'Run Info',
@ -123,8 +123,8 @@ class Migration(migrations.Migration):
('completed', models.DateTimeField()),
('tags', models.TextField(help_text='Tags used on this run, separated by commas', blank=True)),
('skipped', models.TextField(help_text='A comma sepearted list of questions skipped by this run', blank=True)),
('landing', models.ForeignKey(blank=True, to='questionnaire.Landing', null=True)),
('questionnaire', models.ForeignKey(to='questionnaire.Questionnaire')),
('landing', models.ForeignKey(on_delete=models.CASCADE, blank=True, to='questionnaire.Landing', null=True)),
('questionnaire', models.ForeignKey(on_delete=models.CASCADE, to='questionnaire.Questionnaire')),
],
options={
'verbose_name_plural': 'Run Info History',
@ -153,37 +153,37 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='runinfohistory',
name='subject',
field=models.ForeignKey(to='questionnaire.Subject'),
field=models.ForeignKey(on_delete=models.CASCADE, to='questionnaire.Subject'),
),
migrations.AddField(
model_name='runinfo',
name='subject',
field=models.ForeignKey(to='questionnaire.Subject'),
field=models.ForeignKey(on_delete=models.CASCADE, to='questionnaire.Subject'),
),
migrations.AddField(
model_name='question',
name='questionset',
field=models.ForeignKey(to='questionnaire.QuestionSet'),
field=models.ForeignKey(on_delete=models.CASCADE, to='questionnaire.QuestionSet'),
),
migrations.AddField(
model_name='landing',
name='questionnaire',
field=models.ForeignKey(related_name='landings', blank=True, to='questionnaire.Questionnaire', null=True),
field=models.ForeignKey(on_delete=models.CASCADE, related_name='landings', blank=True, to='questionnaire.Questionnaire', null=True),
),
migrations.AddField(
model_name='choice',
name='question',
field=models.ForeignKey(to='questionnaire.Question'),
field=models.ForeignKey(on_delete=models.CASCADE, to='questionnaire.Question'),
),
migrations.AddField(
model_name='answer',
name='question',
field=models.ForeignKey(help_text='The question that this is an answer to', to='questionnaire.Question'),
field=models.ForeignKey(on_delete=models.CASCADE, help_text='The question that this is an answer to', to='questionnaire.Question'),
),
migrations.AddField(
model_name='answer',
name='subject',
field=models.ForeignKey(help_text='The user who supplied this answer', to='questionnaire.Subject'),
field=models.ForeignKey(on_delete=models.CASCADE, help_text='The user who supplied this answer', to='questionnaire.Subject'),
),
migrations.AlterIndexTogether(
name='runinfo',

View File

@ -21,16 +21,16 @@ class Migration(migrations.Migration):
migrations.AddField(
model_name='answer',
name='run',
field=models.ForeignKey(related_name='answers', to='questionnaire.Run', null=True),
field=models.ForeignKey(on_delete=models.CASCADE, related_name='answers', to='questionnaire.Run', null=True),
),
migrations.AddField(
model_name='runinfo',
name='run',
field=models.ForeignKey(related_name='run_infos', to='questionnaire.Run', null=True),
field=models.ForeignKey(on_delete=models.CASCADE, related_name='run_infos', to='questionnaire.Run', null=True),
),
migrations.AddField(
model_name='runinfohistory',
name='run',
field=models.ForeignKey(related_name='run_info_histories', to='questionnaire.Run', null=True),
field=models.ForeignKey(on_delete=models.CASCADE, related_name='run_info_histories', to='questionnaire.Run', null=True),
),
]

View File

@ -22,19 +22,19 @@ class Migration(migrations.Migration):
migrations.AlterField(
model_name='answer',
name='run',
field=models.ForeignKey(related_name='answers', default=1, to='questionnaire.Run'),
field=models.ForeignKey(on_delete=models.CASCADE, related_name='answers', default=1, to='questionnaire.Run'),
preserve_default=False,
),
migrations.AlterField(
model_name='runinfo',
name='run',
field=models.ForeignKey(related_name='run_infos', default=1, to='questionnaire.Run'),
field=models.ForeignKey(on_delete=models.CASCADE, related_name='run_infos', default=1, to='questionnaire.Run'),
preserve_default=False,
),
migrations.AlterField(
model_name='runinfohistory',
name='run',
field=models.ForeignKey(related_name='run_info_histories', to='questionnaire.Run'),
field=models.ForeignKey(on_delete=models.CASCADE, related_name='run_info_histories', to='questionnaire.Run'),
),
migrations.AlterIndexTogether(
name='answer',

View File

@ -3,6 +3,7 @@ import json
import re
import uuid
from datetime import datetime
from six import text_type as unicodestr
from transmeta import TransMeta
from django.conf import settings
@ -11,7 +12,7 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from django.core.urlresolvers import reverse
from django.urls import reverse
from . import QuestionChoices
from .utils import split_numal
@ -58,6 +59,8 @@ class Subject(models.Model):
else:
return u'%s, %s (%s)' % (self.surname, self.givenname, self.email)
__str__ = __unicode__
def next_runid(self):
"Return the string form of the runid for the upcoming run"
return str(self.nextrun.year)
@ -94,6 +97,8 @@ class Questionnaire(models.Model):
def __unicode__(self):
return self.name
__str__ = __unicode__
def questionsets(self):
if not hasattr(self, "__qscache"):
self.__qscache = \
@ -115,11 +120,11 @@ class Questionnaire(models.Model):
class Landing(models.Model):
# defines an entry point to a Feedback session
nonce = models.CharField(max_length=32, null=True,blank=True)
content_type = models.ForeignKey(ContentType, null=True,blank=True, related_name='landings')
content_type = models.ForeignKey(ContentType, on_delete=models.CASCADE, null=True,blank=True, related_name='landings')
object_id = models.PositiveIntegerField(null=True,blank=True)
content_object = GenericForeignKey('content_type', 'object_id')
label = models.CharField(max_length=64, blank=True)
questionnaire = models.ForeignKey(Questionnaire, null=True, blank=True, related_name='landings')
questionnaire = models.ForeignKey(Questionnaire, on_delete=models.CASCADE, null=True, blank=True, related_name='landings')
def _hash(self):
return uuid.uuid4().hex
@ -151,17 +156,18 @@ class DBStylesheet(models.Model):
def __unicode__(self):
return self.inclusion_tag
__str__ = __unicode__
class QuestionSet(models.Model):
__metaclass__ = TransMeta
class QuestionSet(models.Model, metaclass=TransMeta):
"Which questions to display on a question page"
questionnaire = models.ForeignKey(Questionnaire)
questionnaire = models.ForeignKey(Questionnaire, on_delete=models.CASCADE)
sortid = models.IntegerField() # used to decide which order to display in
heading = models.CharField(max_length=64)
checks = models.CharField(max_length=256, blank=True,
help_text = """Current options are 'femaleonly' or 'maleonly' and shownif="QuestionNumber,Answer" which takes the same format as <tt>requiredif</tt> for questions.""")
text = models.TextField(u'Text', help_text="HTML or Text")
text = models.TextField(u'Text', help_text="HTML or Text", default="",)
parse_html = models.BooleanField("Render html in heading?", null=False, default=False)
@ -191,6 +197,8 @@ class QuestionSet(models.Model):
retnext = True
return None
__next__ = next
def prev(self):
qs = self.questionnaire.questionsets()
last = None
@ -216,6 +224,8 @@ class QuestionSet(models.Model):
def __unicode__(self):
return u'%s: %s' % (self.questionnaire.name, self.heading)
__str__ = __unicode__
class Meta:
translate = ('text',)
index_together = [
@ -227,13 +237,13 @@ class Run(models.Model):
class RunInfo(models.Model):
"Store the active/waiting questionnaire runs here"
subject = models.ForeignKey(Subject)
subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
random = models.CharField(max_length=32) # probably a randomized md5sum
run = models.ForeignKey(Run, related_name='run_infos')
landing = models.ForeignKey(Landing, null=True, blank=True)
run = models.ForeignKey(Run, on_delete=models.CASCADE, related_name='run_infos')
landing = models.ForeignKey(Landing, on_delete=models.CASCADE, null=True, blank=True)
# questionset should be set to the first QuestionSet initially, and to null on completion
# ... although the RunInfo entry should be deleted then anyway.
questionset = models.ForeignKey(QuestionSet, blank=True, null=True) # or straight int?
questionset = models.ForeignKey(QuestionSet, on_delete=models.CASCADE, blank=True, null=True) # or straight int?
emailcount = models.IntegerField(default=0)
created = models.DateTimeField(auto_now_add=True)
@ -281,7 +291,7 @@ class RunInfo(models.Model):
"runinfo.set_cookie(key, value). If value is None, delete cookie"
key = key.lower().strip()
cookies = self.get_cookiedict()
if type(value) not in (int, float, str, unicode, type(None)):
if type(value) not in (int, float, unicodestr, type(None)):
raise Exception("Can only store cookies of type integer or string")
if value is None:
if key in cookies:
@ -311,6 +321,8 @@ class RunInfo(models.Model):
def __unicode__(self):
return "%s: %s, %s" % (self.run.runid, self.subject.surname, self.subject.givenname)
__str__ = __unicode__
class Meta:
verbose_name_plural = 'Run Info'
index_together = [
@ -318,10 +330,10 @@ class RunInfo(models.Model):
]
class RunInfoHistory(models.Model):
subject = models.ForeignKey(Subject)
run = models.ForeignKey(Run, related_name='run_info_histories')
subject = models.ForeignKey(Subject, on_delete=models.CASCADE)
run = models.ForeignKey(Run, on_delete=models.CASCADE, related_name='run_info_histories')
completed = models.DateTimeField()
landing = models.ForeignKey(Landing, null=True, blank=True)
landing = models.ForeignKey(Landing, on_delete=models.CASCADE, null=True, blank=True)
tags = models.TextField(
blank=True,
help_text=u"Tags used on this run, separated by commas"
@ -330,10 +342,12 @@ class RunInfoHistory(models.Model):
blank=True,
help_text=u"A comma sepearted list of questions skipped by this run"
)
questionnaire = models.ForeignKey(Questionnaire)
questionnaire = models.ForeignKey(Questionnaire, on_delete=models.CASCADE)
def __unicode__(self):
return "%s: %s on %s" % (self.run.runid, self.subject, self.completed)
return "%s: %s on %s" % (self.run.runid, self.subject, self.completed)
__str__ = __unicode__
def answers(self):
"Returns the query for the answers."
@ -343,15 +357,14 @@ class RunInfoHistory(models.Model):
verbose_name_plural = 'Run Info History'
class Question(models.Model):
__metaclass__ = TransMeta
class Question(models.Model, metaclass=TransMeta):
questionset = models.ForeignKey(QuestionSet)
questionset = models.ForeignKey(QuestionSet, on_delete=models.CASCADE)
number = models.CharField(max_length=8, help_text=
"eg. <tt>1</tt>, <tt>2a</tt>, <tt>2b</tt>, <tt>3c</tt><br /> "
"Number is also used for ordering questions.")
sort_id = models.IntegerField(null=True, blank=True, help_text="Questions within a questionset are sorted by sort order first, question number second")
text = models.TextField(blank=True, verbose_name=_("Text"))
text = models.TextField(blank=True, default="", verbose_name=_("Text"))
type = models.CharField(u"Type of question", max_length=32,
choices = QuestionChoices,
help_text = u"Determines the means of answering the question. " \
@ -373,7 +386,7 @@ class Question(models.Model):
"You may also combine tests appearing in <tt>requiredif</tt> "
"by joining them with the words <tt>and</tt> or <tt>or</tt>, "
'eg. <tt>requiredif="Q1,A or Q2,B"</tt>')
footer = models.TextField(u"Footer", help_text="Footer rendered below the question", blank=True)
footer = models.TextField(u"Footer", help_text="Footer rendered below the question", default="", blank=True)
parse_html = models.BooleanField("Render html in Footer?", null=False, default=False)
@ -392,7 +405,10 @@ class Question(models.Model):
return d
def __unicode__(self):
return u'{%s} (%s) %s' % (unicode(self.questionset), self.number, self.text)
return u'{%s} (%s) %s' % (unicodestr(self.questionset), self.number, self.text)
__str__ = __unicode__
def sameas(self):
if self.type == 'sameas':
@ -468,18 +484,20 @@ class Question(models.Model):
["number", "questionset"],
]
class Choice(models.Model):
__metaclass__ = TransMeta
class Choice(models.Model, metaclass=TransMeta):
question = models.ForeignKey(Question)
question = models.ForeignKey(Question, on_delete=models.CASCADE)
sortid = models.IntegerField()
value = models.CharField(u"Short Value", max_length=64)
text = models.CharField(u"Choice Text", max_length=200)
value = models.CharField(u"Short Value", max_length=64, default="")
text = models.CharField(u"Choice Text", max_length=200, default="")
tags = models.CharField(u"Tags", max_length=64, blank=True)
def __unicode__(self):
return u'(%s) %d. %s' % (self.question.number, self.sortid, self.text)
__str__ = __unicode__
class Meta:
translate = ('text',)
index_together = [
@ -487,14 +505,16 @@ class Choice(models.Model):
]
class Answer(models.Model):
subject = models.ForeignKey(Subject, help_text = u'The user who supplied this answer')
question = models.ForeignKey(Question, help_text = u"The question that this is an answer to")
run = models.ForeignKey(Run, related_name='answers')
subject = models.ForeignKey(Subject, on_delete=models.CASCADE, help_text = u'The user who supplied this answer')
question = models.ForeignKey(Question, on_delete=models.CASCADE, help_text = u"The question that this is an answer to")
run = models.ForeignKey(Run, on_delete=models.CASCADE, related_name='answers')
answer = models.TextField()
def __unicode__(self):
return "Answer(%s: %s, %s)" % (self.question.number, self.subject.surname, self.subject.givenname)
__str__ = __unicode__
def split_answer(self):
"""
Decode stored answer value and return as a list of choices.
@ -535,7 +555,7 @@ class Answer(models.Model):
runinfo.remove_tags(tags)
for split_answer in self.split_answer():
if unicode(split_answer) == choice.value:
if unicodestr(split_answer) == choice.value:
tags_to_add.extend(tags)
runinfo.add_tags(tags_to_add)

View File

@ -1,9 +1,8 @@
from django.db import models
from django.core.urlresolvers import reverse
from django.urls import reverse
from transmeta import TransMeta
class Page(models.Model):
__metaclass__ = TransMeta
class Page(models.Model, metaclass=TransMeta):
slug = models.SlugField(unique=True, primary_key=True)
title = models.CharField(u"Title", max_length=256)
@ -13,6 +12,8 @@ class Page(models.Model):
def __unicode__(self):
return u"Page[%s]" % self.slug
__str__ = __unicode__
def get_absolute_url(self):
return reverse('questionnaire.page.views.page', kwargs={'page_to_render':self.slug})

View File

@ -1,3 +1,4 @@
from __future__ import print_function
import hotshot
import os
import time
@ -51,7 +52,7 @@ def timethis(fn):
def wrapper(*args, **kwargs):
start = time.time()
result = fn(*args, **kwargs)
print fn.__name__, 'took', time.time() - start
print(fn.__name__, 'took', time.time() - start)
return result
return wrapper
@ -62,7 +63,7 @@ def sqlprint(fn):
def wrapper(*args, **kwargs):
connection.queries = list()
result = fn(*args, **kwargs)
print fn.__name__, 'issued'
print(fn.__name__, 'issued')
pprint(connection.queries)
return result
return wrapper

View File

@ -1,3 +1,4 @@
from __future__ import print_function
from json import dumps
import ast
from django.utils.translation import ugettext as _, ungettext
@ -84,8 +85,6 @@ def question_multiple(request, question):
prev_vals[choice_value] = str(prev_value)
possiblelist = pl
# print 'possible value is ', possibledbvalue, ', possiblelist is ', possiblelist
for choice in question.choices():
counter += 1
key = "question_%s_multiple_%d" % (question.number, choice.sortid)
@ -94,7 +93,7 @@ def question_multiple(request, question):
# so that the number box will be activated when item is checked
#try database first and only after that fall back to post choices
# print 'choice multiple checking for match for choice ', choice
# print('choice multiple checking for match for choice ', choice)
checked = ' checked'
prev_value = ''
qvalue = "%s_%s" % (question.number, choice.value)

View File

@ -84,7 +84,6 @@ def question_open(request, question):
@answer_proc('open', 'open-textfield', 'choice-yesno', 'choice-yesnocomment', 'choice-yesnodontknow','choice-yesno-optional', 'choice-yesnocomment-optional', 'choice-yesnodontknow-optional')
def process_simple(question, ansdict):
# print 'process_simple has question, ansdict ', question, ',', ansdict
checkdict = question.getcheckdict()
ans = ansdict['ANSWER'] or ''
qtype = question.get_type()
@ -109,7 +108,7 @@ def process_simple(question, ansdict):
answords = len(ans.split())
if answords > maxwords:
raise AnswerException(_(u'Answer is ' + str(answords) + ' words. Please shorten answer to ' + str(maxwords) + ' words or less'))
if ansdict.has_key('comment') and len(ansdict['comment']) > 0:
if 'comment' in ansdict and len(ansdict['comment']) > 0:
return dumps([ans, [ansdict['comment']]])
if ans:
return dumps([ans])

View File

@ -1,3 +1,5 @@
from six import text_type as unicodestr
from django.utils.translation import ugettext as _, ugettext_lazy
from .. import add_type, question_proc, answer_proc, AnswerException
@ -29,7 +31,7 @@ def question_timeperiod(request, question):
for x in units:
if x in perioddict:
timeperiods.append( (x, unicode(perioddict[x]), unitselected==x) )
timeperiods.append( (x, unicodestr(perioddict[x]), unitselected==x) )
return {
"required" : "required" in cd,
"timeperiods" : timeperiods,
@ -38,7 +40,7 @@ def question_timeperiod(request, question):
@answer_proc('timeperiod')
def process_timeperiod(question, answer):
if not answer['ANSWER'] or not answer.has_key('unit'):
if not answer['ANSWER'] or not 'unit' in answer:
raise AnswerException(_(u"Invalid time period"))
period = answer['ANSWER'].strip()
if period:

View File

@ -9,6 +9,13 @@ from functools import wraps
from threading import currentThread
from django.core.cache.backends.locmem import LocMemCache
try:
from django.utils.deprecation import MiddlewareMixin
except ImportError:
# djang0 < 1.10
class MiddlewareMixin(object):
pass
_request_cache = {}
_installed_middleware = False
@ -25,9 +32,10 @@ class RequestCache(LocMemCache):
params = dict()
super(RequestCache, self).__init__(name, params)
class RequestCacheMiddleware(object):
def __init__(self):
class RequestCacheMiddleware(MiddlewareMixin):
def __init__(self, get_response=None):
global _installed_middleware
self.get_response = get_response
_installed_middleware = True
def process_request(self, request):
@ -42,11 +50,11 @@ class request_cache(object):
@request_cache()
def cached(name):
print "My name is %s and I'm cached" % name
print("My name is %s and I'm cached" % name)
@request_cache(keyfn=lambda p: p['id'])
def cached(param):
print "My id is %s" % p['id']
print("My id is %s" % p['id'])
If no keyfn is provided the decorator expects the args to be hashable.

View File

@ -4,7 +4,7 @@ register = django.template.Library()
@register.simple_tag(takes_context=True)
def render_with_landing(context, text):
if not context.has_key('landing_object') and context.has_key('runinfo'):
if not 'landing_object' in context and 'runinfo' in context:
landing = context['runinfo'].landing
context['landing_object'] = landing.content_object if landing else ''
if text:

View File

@ -1,7 +1,7 @@
#!/usr/bin/python
from django import template
from django.core.urlresolvers import reverse
from django.urls import reverse
register = template.Library()

View File

@ -1,7 +1,8 @@
#!/usr/bin/python
import codecs
import cStringIO
from six import StringIO
import csv
from six import text_type as unicodestr
from django.conf import settings
try:
@ -9,6 +10,17 @@ try:
except AttributeError:
use_session = False
def cmp(x, y):
"""
Replacement for built-in function cmp that was removed in Python 3
Compare the two objects x and y and return an integer according to
the outcome. The return value is negative if x < y, zero if x == y
and strictly positive if x > y.
"""
return (x > y) - (x < y)
def split_numal(val):
"""Split, for example, '1a' into (1, 'a')
>>> split_numal("11a")
@ -60,33 +72,3 @@ if __name__ == "__main__":
import doctest
doctest.testmod()
class UnicodeWriter:
"""
COPIED from http://docs.python.org/library/csv.html example:
A CSV writer which will write rows to CSV file "f",
which is encoded in the given encoding.
"""
def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds):
# Redirect output to a queue
self.queue = cStringIO.StringIO()
self.writer = csv.writer(self.queue, dialect=dialect, **kwds)
self.stream = f
self.encoder = codecs.getincrementalencoder(encoding)()
def writerow(self, row):
self.writer.writerow([unicode(s).encode("utf-8") for s in row])
# Fetch UTF-8 output from the queue ...
data = self.queue.getvalue()
data = data.decode("utf-8")
# ... and reencode it into the target encoding
data = self.encoder.encode(data)
# write to the target stream
self.stream.write(data)
# empty queue
self.queue.truncate(0)
def writerows(self, rows):
for row in rows:
self.writerow(row)

View File

@ -2,15 +2,18 @@
# vim: set fileencoding=utf-8
import json
import logging
from six import text_type as unicodestr
import tempfile
import csv
from compat import commit_on_success, commit, rollback
from functools import cmp_to_key
from hashlib import md5
from uuid import uuid4
from django.apps import apps
from django.http import HttpResponse, HttpResponseRedirect
from django.core.urlresolvers import reverse
from django.urls import reverse
from django.core.cache import cache
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, get_object_or_404
@ -29,7 +32,7 @@ from .models import (
)
from .forms import NewLandingForm
from .parsers import BooleanParser
from .utils import numal_sort, split_numal, UnicodeWriter
from .utils import numal_sort, split_numal
from .run import (
add_answer, delete_answer, get_runinfo, get_question,
question_satisfies_checks, questionset_satisfies_checks,
@ -89,15 +92,15 @@ def redirect_to_qs(runinfo, request=None):
# skip questionsets that don't pass
if not questionset_satisfies_checks(runinfo.questionset, runinfo):
next = runinfo.questionset.next()
nxt = next(runinfo.questionset)
while next and not questionset_satisfies_checks(next, runinfo):
next = next.next()
while nxt and not questionset_satisfies_checks(nxt, runinfo):
nxt = next(nxt)
runinfo.questionset = next
runinfo.questionset = nxt
runinfo.save()
hasquestionset = bool(next)
hasquestionset = bool(nxt)
else:
hasquestionset = True
@ -166,7 +169,6 @@ def questionnaire(request, runcode=None, qs=None):
We only commit on success, to maintain consistency. We also specifically
rollback if there were errors processing the answers for this questionset.
"""
print translation.get_language()
if use_session:
session_runcode = request.session.get('runcode', None)
@ -261,7 +263,7 @@ def questionnaire(request, runcode=None, qs=None):
# to confirm that we have the correct answers
expected = questionset.questions()
items = request.POST.items()
items = list(request.POST.items())
extra = {} # question_object => { "ANSWER" : "123", ... }
# this will ensure that each question will be processed, even if we did not receive
@ -311,7 +313,7 @@ def questionnaire(request, runcode=None, qs=None):
add_answer(runinfo, question, ans)
if cd.get('store', False):
runinfo.set_cookie(question.number, ans['ANSWER'])
except AnswerException, e:
except AnswerException as e:
errors[question.number] = e
except Exception:
logging.exception("Unexpected Exception")
@ -325,15 +327,15 @@ def questionnaire(request, runcode=None, qs=None):
questionset_done.send(sender=None, runinfo=runinfo, questionset=questionset)
next = questionset.next()
while next and not questionset_satisfies_checks(next, runinfo):
next = next.next()
runinfo.questionset = next
nxt = next(questionset)
while nxt and not questionset_satisfies_checks(nxt, runinfo):
nxt = next(nxt)
runinfo.questionset = nxt
runinfo.save()
if use_session:
request.session['prev_runcode'] = runinfo.random
if next is None: # we are finished
if nxt is None: # we are finished
return finish_questionnaire(request, runinfo, questionnaire)
commit()
@ -543,19 +545,19 @@ def show_questionnaire(request, runinfo, errors={}):
return r
def set_language(request, runinfo=None, next=None):
def set_language(request, runinfo=None, nxt=None):
"""
Change the language, save it to runinfo if provided, and
redirect to the provided URL (or the last URL).
Can also be used by a url handler, w/o runinfo & next.
"""
if not next:
next = request.GET.get('next', request.POST.get('next', None))
if not next:
next = request.META.get('HTTP_REFERER', None)
if not next:
next = '/'
response = HttpResponseRedirect(next)
if not nxt:
nxt = request.GET.get('next', request.POST.get('next', None))
if not nxt:
nxt = request.META.get('HTTP_REFERER', None)
if not nxt:
nxt = '/'
response = HttpResponseRedirect(nxt)
response['Expires'] = "Thu, 24 Jan 1980 00:00:00 GMT"
if request.method == 'GET':
lang_code = request.GET.get('lang', None)
@ -604,7 +606,7 @@ def generate_run(request, questionnaire_id, subject_id=None, context={}):
# str_to_hash = "".join(map(lambda i: chr(random.randint(0, 255)), range(16)))
str_to_hash = str(uuid4())
str_to_hash += settings.SECRET_KEY
key = md5(str_to_hash).hexdigest()
key = md5(bytes(str_to_hash, 'utf-8')).hexdigest()
landing = context.get('landing', None)
r = Run.objects.create(runid=key)
run = RunInfo.objects.create(subject=su, random=key, run=r, questionset=qs, landing=landing)
@ -662,7 +664,6 @@ def new_questionnaire(request, item_id):
if form.is_valid():
if not item and form.item:
item = form.item
print "create landing"
landing = Landing.objects.create(label=form.cleaned_data['label'], questionnaire=form.cleaned_data['questionnaire'], content_object=item)
return HttpResponseRedirect(reverse('questionnaires'))
return render(request, "manage_questionnaire.html", {"item":item, "form":form})
@ -670,7 +671,6 @@ def new_questionnaire(request, item_id):
def questionnaires(request):
print "here"
if not request.user.is_authenticated() :
return render(request, "questionnaires.html")
items = item_model.objects.all()
@ -698,7 +698,7 @@ def _table_headers(questions):
columns.extend([qnum, qnum + "-freeform"])
elif q.type.startswith('choice-multiple'):
cl = [c.value for c in q.choice_set.all()]
cl.sort(numal_sort)
cl.sort(key=cmp_to_key(numal_sort))
columns.extend([qnum + '-' + value for value in cl])
if q.type == 'choice-multiple-freeform':
columns.append(qnum + '-freeform')
@ -733,22 +733,19 @@ def export_csv(request, qid,
if answer_filter is None and not request.user.has_perm("questionnaire.export"):
return HttpResponse('Sorry, you do not have export permissions', content_type="text/plain")
fd = tempfile.TemporaryFile()
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="answers-%s-%s.csv"' % (qid, filecode)
questionnaire = get_object_or_404(Questionnaire, pk=int(qid))
headings, answers = answer_export(questionnaire, answer_filter=answer_filter)
writer = UnicodeWriter(fd)
writer = csv.writer(response, dialect='excel')
writer.writerow(extra_headings + headings)
for subject, run, answer_row in answers:
row = extra_entries(subject, run) + [
a if a else '--' for a in answer_row]
writer.writerow(row)
fd.seek(0)
response = HttpResponse(fd, content_type="text/csv")
response['Content-Length'] = fd.tell()
response['Content-Disposition'] = 'attachment; filename="answers-%s-%s.csv"' % (qid, filecode)
return response
@ -855,7 +852,7 @@ def answer_export(questionnaire, answers=None, answer_filter=None):
choice = choice[0]
col = coldict.get(qnum + '-freeform', None)
if col is None: # look for enumerated choice column (multiple-choice)
col = coldict.get(qnum + '-' + unicode(choice), None)
col = coldict.get(qnum + '-' + unicodestr(choice), None)
if col is None: # single-choice items
if ((not qchoicedict[answer.question.id]) or
choice in qchoicedict[answer.question.id]):
@ -917,7 +914,7 @@ def answer_summary(questionnaire, answers=None, answer_filter=None):
else:
# be tolerant of improperly marked data
freeforms.append(choice)
freeforms.sort(numal_sort)
freeforms.sort(key=cmp_to_key(numal_sort))
summary.append((question.number, question.text, [
(n, t, choice_totals[n]) for (n, t) in choices], freeforms))
return summary

View File

@ -7,9 +7,10 @@ def read(fname):
setup(
name="fef-questionnaire",
version="4.0.1",
version="5.0",
description="A Django application for creating online questionnaires/surveys.",
long_description=read("README.md"),
long_description_content_type="text/markdown",
author="Eldest Daughter, LLC., Free Ebook Foundation",
author_email="gcaprio@eldestdaughter.com, eric@hellman.net",
license="BSD",
@ -24,15 +25,17 @@ setup(
"Operating System :: OS Independent",
"Programming Language :: Python",
'Programming Language :: Python :: 2.7',
'Programming Language :: Python :: 3.6',
"Framework :: Django",
],
zip_safe=False,
install_requires=[
'django',
'django-transmeta',
'django<2',
'django-transmeta-eh',
'django-compat',
'pyyaml',
'pyparsing'
'pyparsing',
'six'
],
setup_requires=[
'versiontools >= 1.6',