general renovations

dj111py38
eric 2016-07-26 17:08:23 -04:00
parent bad5c9d566
commit 72790b421d
62 changed files with 1078 additions and 1009 deletions

View File

@ -1,19 +1,23 @@
ED Questionnaire
FEF Questionnaire
=====================
Introduction
------------
ED Questionnaire is a Django questionnaire app which is easily customizable
FEF Questionnaire is a Django questionnaire app which is easily customizable
and includes advanced dependency support using boolean expressions.
It allows an administrator to create and edit questionnaires in the Django
admin interface, with support for multiple languages.
It can be run either as a survey where subjects are solicited by email, or as a web-based poll.
In either mode, an instance can be linked to an arbitrary object via the django content-types module.
History
-------
The questionnaire app was originally developed by [Seantis](https://github.com/seantis), itself derived from [rmt](https://github.com/rmt). We picked up the project because we had been using it and the Seantis version had entered a steady state of development. There are several feature changes we wanted and decided to head up the maintenance ourselves.
The questionnaire app was originally developed by [Seantis](https://github.com/seantis), itself derived from [rmt](https://github.com/rmt). Eldest Daughter picked up the project and named it [ED-questionnaire](git://github.com/eldest-daughter/ed-questionnaire) because they had been using it and the Seantis version had entered a steady state of development. There are several feature changes they wanted and decided to head up the maintenance themselves.
The old versions are tagged as follows:
@ -22,12 +26,14 @@ The old versions are tagged as follows:
* tag 2.0 - original updated trunk from Seantis version
* tag 2.5 - contains the original Seantis version and all PRs merged in as of 12/09/15. It's considered to be the backwards compatible version of the repository.
The new version is the current trunk and is dubbed v3.0. It should be considered a new project and thus will contain backwards incompatible changes. When possible, we'll try and backport fixes to the v2.x branches, but it will not be a priority.
The "ED-questionnaire" version was dubbed v3.0. It is not compatible with the v2.x branches.
The "FEF-questionnaire" was created to add the ability to link the questionnaire to individual books in a book database. We'll call this v4.0
About this Manual
-----------------
ED Questionnaire is not a very well documented app so far to say the least. This manual should give you a general idea of the layout and concepts of it, but it is not as comprehensive as it should be.
FEF Questionnaire is not a very well documented app so far to say the least. This manual should give you a general idea of the layout and concepts of it, but it is not as comprehensive as it should be.
What it does cover is the following:
@ -70,11 +76,11 @@ Create a place for the questionnare
Clone the questionnaire source
git clone git://github.com/eldest-daughter/ed-questionnaire.git
git clone git://github.com/EbookFoundation/fef-questionnaire.git
You should now have a ed-questionnaire folder in your apps folder
cd ed-questionnaire
cd fef-questionnaire
The next step is to install the questionnaire.
@ -102,7 +108,7 @@ We will use that below for the setup of the folders.
In the same file add the questionnaire static directory to your STATICFILES_DIRS:
STATICFILES_DIRS = (
os.path.abspath('./apps/ed-questionnaire/questionnaire/static/'),
os.path.abspath('./apps/fef-questionnaire/questionnaire/static/'),
)
Also add the locale and request cache middleware to MIDDLEWARE_CLASSES:
@ -116,7 +122,7 @@ otherwise you will get an error when trying to start the server.
Add the questionnaire template directory as well as your own to TEMPLATE_DIRS:
os.path.abspath('./apps/ed-questionnaire/questionnaire/templates'),
os.path.abspath('./apps/fef-questionnaire/questionnaire/templates'),
os.path.abspath('./templates'),
And finally, add `transmeta`, `questionnaire` to your INSTALLED_APPS:
@ -144,12 +150,6 @@ For an empty site with enabled admin interface you add:
# questionnaire urls
url(r'q/', include('questionnaire.urls')),
url(r'^take/(?P<questionnaire_id>[0-9]+)/$', 'questionnaire.views.generate_run'),
url(r'^$', 'questionnaire.page.views.page', {'page_to_render' : 'index'}),
url(r'^(?P<lang>..)/(?P<page_to_trans>.*)\.html$', 'questionnaire.page.views.langpage'),
url(r'^(?P<page_to_render>.*)\.html$', 'questionnaire.page.views.page'),
url(r'^setlang/$', 'questionnaire.views.set_language'),
)
Having done that we can initialize our database. (For this to work you must have setup your DATABASES in `settings.py`.). First, in your CLI navigate back to the `mysite` folder:
@ -161,19 +161,19 @@ The check that you are in the proper folder, type `ls`: if you can see `manage.p
python manage.py syncdb
python manage.py migrate
The questionnaire expects a `base.html` template to be there, with certain stylesheets and blocks inside. Have a look at `./apps/ed-questionnaire/example/templates/base.html`.
The questionnaire expects a `base.html` template to be there, with certain stylesheets and blocks inside. Have a look at `./apps/fef-questionnaire/example/templates/base.html`.
For now you might want to just copy the `base.html` to your own template folder.
mkdir templates
cd templates
cp ../apps/ed-questionnaire/example/templates/base.html .
cp ../apps/fef-questionnaire/example/templates/base.html .
Congratulations, you have setup the basics of the questionnaire! At this point this site doesn't really do anything, as there are no questionnaires defined.
To see an example questionnaire you can do the following (Note: this will only work if you have both English and German defined as Languages in `settings.py`):
python manage.py loaddata ./apps/ed-questionnaire/example/fixtures/initial_data.yaml
python manage.py loaddata ./apps/fef-questionnaire/example/fixtures/initial_data.yaml
You may then start your development server:
@ -194,6 +194,7 @@ The ED Questionnaire has the following tables, described in detail below.
* QuestionSet
* Questionnaire
* Answer
* Landing
### Subject
@ -289,6 +290,9 @@ Contains the answer to a question. The value of the answer is stored as JSON.
A questionnaire is a group of questionsets together.
### Landing
In Poll mode, the landing url links a Questionnaire to an Object and a User to a Subject.
Migration of 1.x to 2.0
-----------------------
@ -340,4 +344,15 @@ There are a few that do some simple testing, but more are needed. More tests wou
Django admin is a nice feature to have, but we either don't leverage it well enough, or it is not the right tool for the questionnaire. In any case, if you are expecting your customer to work with the questionnaire's structure you might have to write your own admin interface. The current one is not good enough.
4.0 Changes
--------------
Version 4.0 has not been tested for compatibility with previous versions.
* Broken back links have been fixed. The application works in session mode and non-session mode.
* We've updated to Bootstrap 3.3.6 and implemented label tags for accessibility
* "landings" have been added so that survey responses can be linked to arbitrary models in an application. template tags have been added that allow questions and answers to refer to those models.
* question types have been added so that choices can be offered without making the question required.
* styling of required questions has been spiffed up

View File

@ -7,13 +7,13 @@ Create flexible questionnaires.
Author: Robert Thomson <git AT corporatism.org>
"""
from django.conf import settings
from django.dispatch import Signal
import imp
__all__ = ['question_proc', 'answer_proc', 'add_type', 'AnswerException',
'questionset_done', 'questionnaire_done', ]
default_app_config = '{}.apps.QuestionnaireConfig'.format(__name__)
QuestionChoices = []
QuestionProcessors = {} # supply additional information to the templates
Processors = {} # for processing answers
@ -27,8 +27,7 @@ questionnaire_done = Signal(providing_args=["runinfo", "questionnaire"])
class AnswerException(Exception):
"""Thrown from an answer processor to generate an error message"""
pass
def question_proc(*names):
"""
Decorator to create a question processor for one or more
@ -82,19 +81,3 @@ def add_type(id, name):
QuestionChoices.append((id, name))
import questionnaire.qprocessors # make sure ours are imported first
add_type('sameas', 'Same as Another Question (put sameas=question.number in checks or sameasid=question.id)')
for app in settings.INSTALLED_APPS:
try:
app_path = __import__(app, {}, {}, [app.split('.')[-1]]).__path__
except AttributeError:
continue
try:
imp.find_module('qprocessors', app_path)
except ImportError:
continue
__import__("%s.qprocessors" % app)

View File

@ -1,6 +1,8 @@
from django.utils.translation import ugettext as _
from django.contrib import admin
from models import *
from django.core.urlresolvers import reverse
from .models import (Choice, Questionnaire, Question, QuestionSet, Subject,
RunInfo, RunInfoHistory, Answer, DBStylesheet, Landing)
adminsite = admin.site
@ -51,7 +53,8 @@ class QuestionnaireAdmin(admin.ModelAdmin):
readonly_fields = ('export',)
def export(self, obj):
return '<a href="/q/csv/%s">%s</a>' % (obj.id, _("Download data"))
csv_url= reverse("export_csv", args=[obj.id,])
return '<a href="%s">%s</a>' % (csv_url, _("Download data"))
export.allow_tags = True
export.short_description = _('Export to CSV')
@ -71,6 +74,13 @@ class AnswerAdmin(admin.ModelAdmin):
list_display = ['id', 'runid', 'subject', 'question']
list_filter = ['subject', 'runid']
ordering = [ 'id', 'subject', 'runid', 'question', ]
from django.contrib import admin
# new in dj1.7
# @admin.register(Landing)
class LandingAdmin(admin.ModelAdmin):
pass
adminsite.register(Questionnaire, QuestionnaireAdmin)
adminsite.register(Question, QuestionAdmin)
@ -79,4 +89,5 @@ adminsite.register(Subject, SubjectAdmin)
adminsite.register(RunInfo, RunInfoAdmin)
adminsite.register(RunInfoHistory, RunInfoHistoryAdmin)
adminsite.register(Answer, AnswerAdmin)
adminsite.register(Landing, LandingAdmin)
adminsite.register(DBStylesheet)

29
questionnaire/apps.py Normal file
View File

@ -0,0 +1,29 @@
# questionnaire/apps.py
import imp
from django.conf import settings
from . import qprocessors, add_type # make sure ours are imported first # noqa
from . import __name__ as app_name
from django.apps import AppConfig
class QuestionnaireConfig(AppConfig):
name = app_name
verbose_name = "FEF Questionnaire"
label = 'questionnaire'
def ready(self):
add_type('sameas', 'Same as Another Question (put sameas=question.number in checks or sameasid=question.id)')
for app in settings.INSTALLED_APPS:
try:
app_path = __import__(app, {}, {}, [app.split('.')[-1]]).__path__
except AttributeError:
continue
try:
imp.find_module('qprocessors', app_path)
except ImportError:
continue
__import__("%s.qprocessors" % app)

View File

@ -1,4 +1,4 @@
from questionnaire.models import Question, Answer
from .models import Question, Answer
import logging

View File

@ -3,18 +3,19 @@
Functions to send email reminders to users.
"""
import random, time, smtplib, rfc822
from datetime import datetime
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 Context, loader
from django.utils import translation
from django.conf import settings
from django.http import Http404, HttpResponse
from models import Subject, QuestionSet, RunInfo, Questionnaire
from datetime import datetime
from django.shortcuts import render_to_response, get_object_or_404
import random, time, smtplib, rfc822
from email.Header import Header
from email.Utils import formataddr, parseaddr
from .models import Subject, QuestionSet, RunInfo, Questionnaire
try: from hashlib import md5
except: from md5 import md5

View File

@ -0,0 +1,512 @@
[
{
"fields": {
"anonymous": false,
"email": "test@example.com",
"formtype": "email",
"gender": "male",
"givenname": "TestGivenname",
"ip_address": null,
"language": "de",
"nextrun": "2009-05-15",
"state": "active",
"surname": "TestSurname"
},
"model": "questionnaire.subject",
"pk": 1
},
{
"fields": {
"admin_access_only": false,
"html": "survey html here",
"name": "MappingSurvey",
"parse_html": false,
"redirect_url": ""
},
"model": "questionnaire.questionnaire",
"pk": 3
},
{
"fields": {
"content_type": 22,
"label": "Open Book Publishers",
"nonce": "xxxxxx",
"object_id": 81834,
"questionnaire": 3
},
"model": "questionnaire.landing",
"pk": 1234
},
{
"fields": {
"checks": "",
"heading": "Open Access Ebooks (Part 1)",
"parse_html": true,
"questionnaire": 3,
"sortid": 1,
"text_en": " <h1> Introduction </h1> \r\n <p> \r\nWelcome, reader of <i>{{ landing_object.title }}</i>! And thanks for visiting Unglue.it to help us out with this \u2026\r\n </p> \r\n <p> \r\nAs Open Access publishers, {{ landing_object.claim.all.0.rights_holder }} are truly committed to making academic research broadly accessible - so we want to understand how people like you are actually accessing and using our Open Access titles. \r\n </p> \r\n <p> \r\nWe have a bunch of questions for you (well - only 9 actually) about how you found this book and what you\u2019re going to do with it. Please tell us the things you think are interesting or relevant. We really want to know!\r\n </p> \r\n <p> \r\n[Privacy policy: There are no marketing traps, we\u2019re not going to surreptitiously drop cookies on you to carry around for us, or swamp you with emails afterwards, or tell our \u201cfriends\u201d about you - we\u2019re just going to store your answers to create a database of usage examples that can be used to understand what Open Access publishing enables."
},
"model": "questionnaire.questionset",
"pk": 5
},
{
"fields": {
"checks": "",
"heading": "Now About You...",
"parse_html": true,
"questionnaire": 3,
"sortid": 2,
"text_en": " <p> And now, three questions about you as well ... </p> "
},
"model": "questionnaire.questionset",
"pk": 6
},
{
"fields": {
"checks": "",
"heading": "Follow-up",
"parse_html": true,
"questionnaire": 3,
"sortid": 3,
"text_en": " <p> We would really like to be able to follow up with some of the respondents to this questionnaire to ask them a few more questions - particularly if you\u2019ve told us something really interesting in a comment (for example). [There will also be a little reward (a free book no less!) for those of you we do contact in this way.] </p> \r\n\r\n <p> Thanks so much for your time and efforts answering these questions for us - we love you for it! </p> \r\n\r\n <p> We hope you enjoy <i>{{ landing_object.title }}</i>. </p> \r\n\r\n <p> {{ landing_object.claim.all.0.rights_holder }} and Unglue.it </p> \r\n"
},
"model": "questionnaire.questionset",
"pk": 7
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "1",
"parse_html": true,
"questionset": 5,
"sort_id": 1,
"text_en": "How did you find out about this book in the first place? <br /> <br /> \r\n\r\nFor example: Was it from a Google search? Following a wikipedia link? A tweet? Referenced in another book? A late night session with a friend (we don\u2019t need to know much more about that!)? - or in some other way?\r\n",
"type": "open"
},
"model": "questionnaire.question",
"pk": 16
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "2",
"parse_html": false,
"questionset": 5,
"sort_id": 2,
"text_en": "How did you get hold of this particular copy? \r\n\r\nFor example: Did you download it from the publisher's website? Amazon or another retailer? Find it on academia.edu? Or somewhere like aaaaarg? Get it from a friend? ",
"type": "open"
},
"model": "questionnaire.question",
"pk": 17
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "3",
"parse_html": true,
"questionset": 5,
"sort_id": 3,
"text_en": "Why are you interested in this book?",
"type": "choice-multiple-freeform"
},
"model": "questionnaire.question",
"pk": 18
},
{
"fields": {
"checks": "",
"extra_en": "If Yes - why are you using this edition and not one of the other ones?",
"footer_en": "",
"number": "4",
"parse_html": false,
"questionset": 5,
"sort_id": 4,
"text_en": "Are you aware that this title is available in multiple different digital and printed formats?",
"type": "choice-yesnocomment"
},
"model": "questionnaire.question",
"pk": 19
},
{
"fields": {
"checks": "",
"extra_en": "Please tell us in more detail:",
"footer_en": "\r\n\r\n\r\n\r\n\r\n\r\n\r\n",
"number": "5",
"parse_html": false,
"questionset": 5,
"sort_id": 5,
"text_en": "What are you going to do with it now you have it?",
"type": "choice-multiple-freeform"
},
"model": "questionnaire.question",
"pk": 20
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "1",
"parse_html": false,
"questionset": 6,
"sort_id": null,
"text_en": "Where do you live?",
"type": "choice-freeform"
},
"model": "questionnaire.question",
"pk": 21
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "2",
"parse_html": false,
"questionset": 6,
"sort_id": null,
"text_en": "What do you do for a living?",
"type": "open"
},
"model": "questionnaire.question",
"pk": 22
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "\r\n\r\n \r\n\r\n",
"number": "3",
"parse_html": false,
"questionset": 6,
"sort_id": null,
"text_en": "When did you finish your formal education?",
"type": "choice-freeform"
},
"model": "questionnaire.question",
"pk": 23
},
{
"fields": {
"checks": "required-no",
"extra_en": "",
"footer_en": "",
"number": "4",
"parse_html": false,
"questionset": 6,
"sort_id": null,
"text_en": "Is there anything else you would like to tell us, or think we should know?",
"type": "open-textfield"
},
"model": "questionnaire.question",
"pk": 24
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "1",
"parse_html": false,
"questionset": 7,
"sort_id": null,
"text_en": "If you\u2019re willing, then please leave us an email address where we could make contact with you (information which we won\u2019t share or make public).\r\n",
"type": "open"
},
"model": "questionnaire.question",
"pk": 25
},
{
"fields": {
"question": 18,
"sortid": 1,
"tags": "",
"text_en": "For personal use - I\u2019m interested in the topic ",
"value": "personal"
},
"model": "questionnaire.choice",
"pk": 17
},
{
"fields": {
"question": 18,
"sortid": 2,
"tags": "",
"text_en": "For my job - it relates to what I do ",
"value": "job"
},
"model": "questionnaire.choice",
"pk": 18
},
{
"fields": {
"question": 18,
"sortid": 3,
"tags": "",
"text_en": "I need to read it for a course",
"value": "course"
},
"model": "questionnaire.choice",
"pk": 19
},
{
"fields": {
"question": 20,
"sortid": 1,
"tags": "",
"text_en": "Save it, in case I need to use it in the future",
"value": "save"
},
"model": "questionnaire.choice",
"pk": 20
},
{
"fields": {
"question": 20,
"sortid": 2,
"tags": "",
"text_en": "Skim through it and see if it\u2019s at all interesting",
"value": "skim"
},
"model": "questionnaire.choice",
"pk": 21
},
{
"fields": {
"question": 20,
"sortid": 3,
"tags": "",
"text_en": "There\u2019s only really a section/chapter I\u2019m interested in - I\u2019ll probably just read that",
"value": "section"
},
"model": "questionnaire.choice",
"pk": 22
},
{
"fields": {
"question": 20,
"sortid": 4,
"tags": "",
"text_en": "The whole book looks fascinating - I\u2019m going to read it all!",
"value": "whole"
},
"model": "questionnaire.choice",
"pk": 23
},
{
"fields": {
"question": 20,
"sortid": 5,
"tags": "",
"text_en": "I\u2019m going to adapt it and use it (or, at least, parts of it) for another purpose (eg a student coursepack, lecture/briefing notes \u2026)",
"value": "adapt"
},
"model": "questionnaire.choice",
"pk": 24
},
{
"fields": {
"question": 20,
"sortid": 6,
"tags": "",
"text_en": "Share it with my friends ",
"value": "share"
},
"model": "questionnaire.choice",
"pk": 25
},
{
"fields": {
"question": 20,
"sortid": 7,
"tags": "",
"text_en": "Print it out and ceremoniously burn it!",
"value": "print"
},
"model": "questionnaire.choice",
"pk": 26
},
{
"fields": {
"question": 20,
"sortid": 8,
"tags": "",
"text_en": "I\u2019m creating/collating a (online) library",
"value": "catalog"
},
"model": "questionnaire.choice",
"pk": 27
},
{
"fields": {
"question": 20,
"sortid": 9,
"tags": "",
"text_en": "Something else entirely \u2026. ",
"value": "else"
},
"model": "questionnaire.choice",
"pk": 28
},
{
"fields": {
"question": 21,
"sortid": 1,
"tags": "",
"text_en": "USA/Canada",
"value": "us"
},
"model": "questionnaire.choice",
"pk": 29
},
{
"fields": {
"question": 21,
"sortid": 2,
"tags": "",
"text_en": "Europe",
"value": "eu"
},
"model": "questionnaire.choice",
"pk": 30
},
{
"fields": {
"question": 21,
"sortid": 3,
"tags": "",
"text_en": "South America",
"value": "sa"
},
"model": "questionnaire.choice",
"pk": 31
},
{
"fields": {
"question": 21,
"sortid": 4,
"tags": "",
"text_en": "Central America/ Caribbean",
"value": "ca"
},
"model": "questionnaire.choice",
"pk": 32
},
{
"fields": {
"question": 21,
"sortid": 5,
"tags": "",
"text_en": "Asia",
"value": "as"
},
"model": "questionnaire.choice",
"pk": 33
},
{
"fields": {
"question": 21,
"sortid": 6,
"tags": "",
"text_en": "Africa",
"value": "af"
},
"model": "questionnaire.choice",
"pk": 34
},
{
"fields": {
"question": 21,
"sortid": 7,
"tags": "",
"text_en": "Middle East",
"value": "me"
},
"model": "questionnaire.choice",
"pk": 35
},
{
"fields": {
"question": 21,
"sortid": 8,
"tags": "",
"text_en": "Another Planet",
"value": "ap"
},
"model": "questionnaire.choice",
"pk": 36
},
{
"fields": {
"question": 23,
"sortid": 1,
"tags": "",
"text_en": "I haven\u2019t - I\u2019m still a student",
"value": "x"
},
"model": "questionnaire.choice",
"pk": 37
},
{
"fields": {
"question": 23,
"sortid": 2,
"tags": "",
"text_en": "At primary school",
"value": "8"
},
"model": "questionnaire.choice",
"pk": 38
},
{
"fields": {
"question": 23,
"sortid": 3,
"tags": "",
"text_en": "At high school",
"value": "h"
},
"model": "questionnaire.choice",
"pk": 39
},
{
"fields": {
"question": 23,
"sortid": 4,
"tags": "",
"text_en": "After trade qualifications",
"value": "t"
},
"model": "questionnaire.choice",
"pk": 40
},
{
"fields": {
"question": 23,
"sortid": 5,
"tags": "",
"text_en": "At College/Undergraduate Degree ",
"value": "c"
},
"model": "questionnaire.choice",
"pk": 41
},
{
"fields": {
"question": 23,
"sortid": 6,
"tags": "",
"text_en": "At Grad School/post-graduate university",
"value": "g"
},
"model": "questionnaire.choice",
"pk": 42
}
]

View File

@ -2,7 +2,7 @@
import questionnaire
from django.conf.urls.defaults import *
from views import *
from .views import *
urlpatterns = patterns('',
url(r'^q/(?P<runcode>[^/]+)/(?P<qs>\d+)/$',

View File

@ -7,7 +7,7 @@ answers submitted to the DB.
"""
from django.test import TestCase
from django.test.client import Client
from questionnaire.models import *
from .models import *
from datetime import datetime
import os

View File

@ -0,0 +1,14 @@
from django.core.management.base import BaseCommand
from ...models import Landing
class Command(BaseCommand):
help = "make survey nonces with the specified label"
args = "<how_many> <label>"
def handle(self, how_many=1, label="no label yet", **options):
how_many=int(how_many)
while how_many > 0:
landing = Landing.objects.create(label = label)
print landing.nonce
how_many -= 1

View File

@ -2,7 +2,7 @@ from django.core.management.base import NoArgsCommand
class Command(NoArgsCommand):
def handle_noargs(self, **options):
from questionnaire.emails import send_emails
from ...emails import send_emails
res = send_emails()
if res:
print res

View File

@ -1,12 +1,13 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
@ -17,9 +18,6 @@ class Migration(migrations.Migration):
('runid', models.CharField(help_text='The RunID (ie. year)', max_length=32, verbose_name='RunID')),
('answer', models.TextField()),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Choice',
@ -27,39 +25,62 @@ class Migration(migrations.Migration):
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('sortid', models.IntegerField()),
('value', models.CharField(max_length=64, verbose_name='Short Value')),
('text_en', models.CharField(max_length=200, verbose_name='Choice Text')),
('text_en', models.CharField(max_length=200, null=True, verbose_name='Choice Text', blank=True)),
('tags', models.CharField(max_length=64, verbose_name='Tags', blank=True)),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='DBStylesheet',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('inclusion_tag', models.CharField(max_length=128)),
('content', models.TextField()),
],
),
migrations.CreateModel(
name='GlobalStyles',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('content', models.TextField()),
],
),
migrations.CreateModel(
name='Landing',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('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)),
],
),
migrations.CreateModel(
name='Question',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('number', models.CharField(help_text=b'eg. <tt>1</tt>, <tt>2a</tt>, <tt>2b</tt>, <tt>3c</tt><br /> Number is also used for ordering questions.', max_length=8)),
('text_en', models.TextField(verbose_name='Text', blank=True)),
('type', models.CharField(help_text="Determines the means of answering the question. An open question gives the user a single-line textfield, multiple-choice gives the user a number of choices he/she can choose from. If a question is multiple-choice, enter the choices this user can choose from below'.", max_length=32, verbose_name='Type of question', choices=[(b'open', b'Open Answer, single line [input]'), (b'open-textfield', b'Open Answer, multi-line [textarea]'), (b'choice-yesno', b'Yes/No Choice [radio]'), (b'choice-yesnocomment', b'Yes/No Choice with optional comment [radio, input]'), (b'choice-yesnodontknow', b"Yes/No/Don't know Choice [radio]"), (b'comment', b'Comment Only'), (b'choice', b'Choice [radio]'), (b'choice-freeform', b'Choice with a freeform option [radio]'), (b'dropdown', b'Dropdown choice [select]'), (b'choice-multiple', b'Multiple-Choice, Multiple-Answers [checkbox]'), (b'choice-multiple-freeform', b'Multiple-Choice, Multiple-Answers, plus freeform [checkbox, input]'), (b'range', b'Range of numbers [select]'), (b'number', b'Number [input]'), (b'timeperiod', b'Time Period [input, select]'), (b'custom', b'Custom field'), (b'sameas', b'Same as Another Question (put sameas=question.number in checks or sameasid=question.id)')])),
('sort_id', models.IntegerField(help_text=b'Questions within a questionset are sorted by sort order first, question number second', null=True, blank=True)),
('text_en', models.TextField(null=True, verbose_name='Text', blank=True)),
('type', models.CharField(help_text="Determines the means of answering the question. An open question gives the user a single-line textfield, multiple-choice gives the user a number of choices he/she can choose from. If a question is multiple-choice, enter the choices this user can choose from below'.", max_length=32, verbose_name='Type of question', choices=[(b'open', b'Open Answer, single line [input]'), (b'open-textfield', b'Open Answer, multi-line [textarea]'), (b'choice-yesno', b'Yes/No Choice [radio]'), (b'choice-yesnocomment', b'Yes/No Choice with optional comment [radio, input]'), (b'choice-yesnodontknow', b"Yes/No/Don't know Choice [radio]"), (b'choice-yesno-optional', b'Optional Yes/No Choice [radio]'), (b'choice-yesnocomment-optional', b'Optional Yes/No Choice with optional comment [radio, input]'), (b'choice-yesnodontknow-optional', b"Optional Yes/No/Don't know Choice [radio]"), (b'comment', b'Comment Only'), (b'choice', b'Choice [radio]'), (b'choice-freeform', b'Choice with a freeform option [radio]'), (b'choice-optional', b'Optional choice [radio]'), (b'choice-freeform-optional', b'Optional choice with a freeform option [radio]'), (b'dropdown', b'Dropdown choice [select]'), (b'choice-multiple', b'Multiple-Choice, Multiple-Answers [checkbox]'), (b'choice-multiple-freeform', b'Multiple-Choice, Multiple-Answers, plus freeform [checkbox, input]'), (b'choice-multiple-values', b'Multiple-Choice, Multiple-Answers [checkboxes], plus value box [input] for each selected answer'), (b'range', b'Range of numbers [select]'), (b'number', b'Number [input]'), (b'timeperiod', b'Time Period [input, select]'), (b'custom', b'Custom field'), (b'sameas', b'Same as Another Question (put sameas=question.number in checks or sameasid=question.id)')])),
('extra_en', models.CharField(help_text='Extra information (use on question type)', max_length=512, null=True, verbose_name='Extra information', blank=True)),
('checks', models.CharField(help_text=b'Additional checks to be performed for this value (space separated) <br /><br />For text fields, <tt>required</tt> is a valid check.<br />For yes/no choice, <tt>required</tt>, <tt>required-yes</tt>, and <tt>required-no</tt> are valid.<br /><br />If this question is required only if another question\'s answer is something specific, use <tt>requiredif="QuestionNumber,Value"</tt> or <tt>requiredif="QuestionNumber,!Value"</tt> for anything but a specific value. 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>', max_length=512, null=True, verbose_name='Additional checks', blank=True)),
('footer_en', models.TextField(help_text=b'Footer rendered below the question interpreted as textile', verbose_name='Footer', blank=True)),
('footer_en', models.TextField(help_text=b'Footer rendered below the question', null=True, verbose_name='Footer', blank=True)),
('parse_html', models.BooleanField(default=False, verbose_name=b'Render html in Footer?')),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Questionnaire',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('name', models.CharField(max_length=128)),
('redirect_url', models.CharField(default=b'/static/complete.html', help_text=b'URL to redirect to when Questionnaire is complete. Macros: $SUBJECTID, $RUNID, $LANG', max_length=128)),
('redirect_url', models.CharField(default=b'', help_text=b"URL to redirect to when Questionnaire is complete. Macros: $SUBJECTID, $RUNID, $LANG. Leave blank to render the 'complete.$LANG.html' template.", max_length=128, blank=True)),
('html', models.TextField(verbose_name='Html', blank=True)),
('parse_html', models.BooleanField(default=False, verbose_name=b'Render html instead of name for survey?')),
('admin_access_only', models.BooleanField(default=False, verbose_name=b'Only allow access to logged in users? (This allows entering paper surveys without allowing new external submissions)')),
],
options={
'permissions': (('export', 'Can export questionnaire answers'), ('management', 'Management Tools')),
},
bases=(models.Model,),
),
migrations.CreateModel(
name='QuestionSet',
@ -68,12 +89,10 @@ class Migration(migrations.Migration):
('sortid', models.IntegerField()),
('heading', models.CharField(max_length=64)),
('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"This is interpreted as Textile: <a href='http://en.wikipedia.org/wiki/Textile_%28markup_language%29' target='_blank'>http://en.wikipedia.org/wiki/Textile_(markup_language)</a>", verbose_name='Text')),
('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')),
],
options={
},
bases=(models.Model,),
),
migrations.CreateModel(
name='RunInfo',
@ -89,79 +108,101 @@ 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)),
],
options={
'verbose_name_plural': 'Run Info',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='RunInfoHistory',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('runid', models.CharField(max_length=32)),
('completed', models.DateField()),
('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')),
],
options={
'verbose_name_plural': 'Run Info History',
},
bases=(models.Model,),
),
migrations.CreateModel(
name='Subject',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('state', models.CharField(default=b'inactive', max_length=16, verbose_name='State', choices=[(b'active', 'Active'), (b'inactive', 'Inactive')])),
('anonymous', models.BooleanField(default=False)),
('ip_address', models.GenericIPAddressField(null=True, blank=True)),
('surname', models.CharField(max_length=64, null=True, verbose_name='Surname', blank=True)),
('givenname', models.CharField(max_length=64, null=True, verbose_name='Given name', blank=True)),
('email', models.EmailField(max_length=75, null=True, verbose_name='Email', blank=True)),
('email', models.EmailField(max_length=254, null=True, verbose_name='Email', blank=True)),
('gender', models.CharField(default=b'unset', max_length=8, verbose_name='Gender', blank=True, choices=[(b'unset', 'Unset'), (b'male', 'Male'), (b'female', 'Female')])),
('nextrun', models.DateField(null=True, verbose_name='Next Run', blank=True)),
('formtype', models.CharField(default=b'email', max_length=16, verbose_name='Form Type', choices=[(b'email', 'Subject receives emails'), (b'paperform', 'Subject is sent paper form')])),
('language', models.CharField(default=b'en', max_length=2, verbose_name='Language', choices=[(b'en', b'English')])),
('language', models.CharField(default=b'en-us', max_length=5, verbose_name='Language', choices=[(b'en', b'English')])),
],
options={
},
bases=(models.Model,),
),
migrations.AlterIndexTogether(
name='subject',
index_together=set([('givenname', 'surname')]),
),
migrations.AddField(
model_name='runinfohistory',
name='subject',
field=models.ForeignKey(to='questionnaire.Subject'),
preserve_default=True,
),
migrations.AddField(
model_name='runinfo',
name='subject',
field=models.ForeignKey(to='questionnaire.Subject'),
preserve_default=True,
),
migrations.AddField(
model_name='question',
name='questionset',
field=models.ForeignKey(to='questionnaire.QuestionSet'),
preserve_default=True,
),
migrations.AddField(
model_name='landing',
name='questionnaire',
field=models.ForeignKey(related_name='landings', blank=True, to='questionnaire.Questionnaire', null=True),
),
migrations.AddField(
model_name='choice',
name='question',
field=models.ForeignKey(to='questionnaire.Question'),
preserve_default=True,
),
migrations.AddField(
model_name='answer',
name='question',
field=models.ForeignKey(help_text='The question that this is an answer to', to='questionnaire.Question'),
preserve_default=True,
),
migrations.AddField(
model_name='answer',
name='subject',
field=models.ForeignKey(help_text='The user who supplied this answer', to='questionnaire.Subject'),
preserve_default=True,
),
migrations.AlterIndexTogether(
name='runinfo',
index_together=set([('random',)]),
),
migrations.AlterIndexTogether(
name='questionset',
index_together=set([('questionnaire', 'sortid'), ('sortid',)]),
),
migrations.AlterIndexTogether(
name='question',
index_together=set([('number', 'questionset')]),
),
migrations.AlterIndexTogether(
name='choice',
index_together=set([('value',)]),
),
migrations.AlterIndexTogether(
name='answer',
index_together=set([('subject', 'runid', 'id'), ('subject', 'runid')]),
),
]

View File

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import models, migrations
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='question',
name='sort_id',
field=models.IntegerField(help_text=b'Questions within a questionset are sorted by sort order first, question number second', null=True, blank=True),
preserve_default=True,
),
]

View File

@ -1,44 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0002_question_sort_id'),
]
operations = [
migrations.AlterField(
model_name='choice',
name='text_en',
field=models.CharField(max_length=200, null=True, verbose_name='Choice Text', blank=True),
),
migrations.AlterField(
model_name='question',
name='footer_en',
field=models.TextField(help_text=b'Footer rendered below the question interpreted as textile', null=True, verbose_name='Footer', blank=True),
),
migrations.AlterField(
model_name='question',
name='text_en',
field=models.TextField(null=True, verbose_name='Text', blank=True),
),
migrations.AlterField(
model_name='questionset',
name='text_en',
field=models.TextField(help_text=b"This is interpreted as Textile: <a href='http://en.wikipedia.org/wiki/Textile_%28markup_language%29' target='_blank'>http://en.wikipedia.org/wiki/Textile_(markup_language)</a>", null=True, verbose_name='Text', blank=True),
),
migrations.AlterField(
model_name='subject',
name='email',
field=models.EmailField(max_length=254, null=True, verbose_name='Email', blank=True),
),
migrations.AlterField(
model_name='subject',
name='language',
field=models.CharField(default=b'en', max_length=2, verbose_name='Language', choices=[(b'en', b'English')]),
),
]

View File

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0003_auto_20151129_0727'),
]
operations = [
migrations.AddField(
model_name='question',
name='parse_html',
field=models.BooleanField(default=False, verbose_name=b'parse question text and footer as html?'),
),
migrations.AlterField(
model_name='question',
name='type',
field=models.CharField(help_text="Determines the means of answering the question. An open question gives the user a single-line textfield, multiple-choice gives the user a number of choices he/she can choose from. If a question is multiple-choice, enter the choices this user can choose from below'.", max_length=32, verbose_name='Type of question'),
),
]

View File

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0004_auto_20151202_0230'),
]
operations = [
migrations.AddField(
model_name='questionset',
name='parse_html',
field=models.BooleanField(default=False, verbose_name=b'parse questionset heading and text as html?'),
),
]

View File

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0005_questionset_parse_html'),
]
operations = [
migrations.AddField(
model_name='questionnaire',
name='parse_html',
field=models.BooleanField(default=False, verbose_name=b'parse questionnaire name as html?'),
),
]

View File

@ -1,24 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0006_questionnaire_parse_html'),
]
operations = [
migrations.AddField(
model_name='questionnaire',
name='html',
field=models.TextField(verbose_name='Html', blank=True),
),
migrations.AlterField(
model_name='questionnaire',
name='parse_html',
field=models.BooleanField(default=False, verbose_name=b'Render html instead of name for survey?'),
),
]

View File

@ -1,21 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0007_auto_20151207_1045'),
]
operations = [
migrations.CreateModel(
name='DBStylesheet',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('content', models.TextField()),
],
),
]

View File

@ -1,20 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0008_dbstylesheet'),
]
operations = [
migrations.AddField(
model_name='dbstylesheet',
name='inclusion_tag',
field=models.CharField(default='', max_length=128),
preserve_default=False,
),
]

View File

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0009_dbstylesheet_inclusion_tag'),
]
operations = [
migrations.AlterIndexTogether(
name='choice',
index_together=set([('value',)]),
),
]

View File

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0010_index_choice_value'),
]
operations = [
migrations.AlterIndexTogether(
name='question',
index_together=set([('number', 'questionset')]),
),
]

View File

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0011_index_on_question_mod_number_questionset'),
]
operations = [
migrations.AlterIndexTogether(
name='questionset',
index_together=set([('questionnaire', 'sortid')]),
),
]

View File

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0012_questionset_questionnaire_and_sortid_index'),
]
operations = [
migrations.AlterIndexTogether(
name='answer',
index_together=set([('subject', 'runid', 'id'), ('subject', 'runid')]),
),
]

View File

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0013__answer_index_subject_run_id'),
]
operations = [
migrations.AlterIndexTogether(
name='runinfo',
index_together=set([('random',)]),
),
]

View File

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0014__runinfo_index_random'),
]
operations = [
migrations.AlterIndexTogether(
name='questionset',
index_together=set([('questionnaire', 'sortid'), ('sortid',)]),
),
]

View File

@ -1,18 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0015_questionnaire_index_sortid'),
]
operations = [
migrations.AlterIndexTogether(
name='subject',
index_together=set([('givenname', 'surname')]),
),
]

View File

@ -1,31 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0016_subject_given_sur_name_index'),
]
operations = [
migrations.CreateModel(
name='GlobalStyles',
fields=[
('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
('content', models.TextField()),
],
),
migrations.AddField(
model_name='questionnaire',
name='admin_access_only',
field=models.BooleanField(default=False, verbose_name=b'Only allow access to logged in users? (This allows entering paper surveys without allowing new external submissions)'),
),
migrations.AlterField(
model_name='subject',
name='language',
field=models.CharField(default=b'en-us', max_length=2, verbose_name='Language', choices=[(b'en', b'English')]),
),
]

View File

@ -1,19 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('questionnaire', '0017_auto_20160209_0228'),
]
operations = [
migrations.AlterField(
model_name='runinfohistory',
name='completed',
field=models.DateTimeField(),
),
]

View File

@ -1,16 +1,23 @@
from django.db import models
from transmeta import TransMeta
from django.utils.translation import ugettext_lazy as _
from questionnaire import QuestionChoices
import re
from utils import split_numal
import hashlib
import json
from parsers import parse_checks, ParseException
import re
import uuid
from datetime import datetime
from transmeta import TransMeta
from django.conf import settings
from django.contrib.contenttypes.generic import GenericForeignKey
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 . import QuestionChoices
from .utils import split_numal
from .parsers import parse_checks, ParseException
_numre = re.compile("(\d+)([a-z]+)", re.I)
class Subject(models.Model):
STATE_CHOICES = [
("active", _("Active")),
@ -20,6 +27,8 @@ class Subject(models.Model):
]
state = models.CharField(max_length=16, default="inactive",
choices = STATE_CHOICES, verbose_name=_('State'))
anonymous = models.BooleanField(default=False)
ip_address = models.GenericIPAddressField(null=True, blank=True)
surname = models.CharField(max_length=64, blank=True, null=True,
verbose_name=_('Surname'))
givenname = models.CharField(max_length=64, blank=True, null=True,
@ -39,11 +48,14 @@ class Subject(models.Model):
("email", _("Subject receives emails")),
("paperform", _("Subject is sent paper form"),))
)
language = models.CharField(max_length=2, default=settings.LANGUAGE_CODE,
language = models.CharField(max_length=5, default=settings.LANGUAGE_CODE,
verbose_name = _('Language'), choices = settings.LANGUAGES)
def __unicode__(self):
return u'%s, %s (%s)' % (self.surname, self.givenname, self.email)
if self.anonymous:
return self.ip_address
else:
return u'%s, %s (%s)' % (self.surname, self.givenname, self.email)
def next_runid(self):
"Return the string form of the runid for the upcoming run"
@ -67,13 +79,13 @@ class Subject(models.Model):
index_together = [
["givenname", "surname"],
]
class GlobalStyles(models.Model):
content = models.TextField()
class Questionnaire(models.Model):
name = models.CharField(max_length=128)
redirect_url = models.CharField(max_length=128, help_text="URL to redirect to when Questionnaire is complete. Macros: $SUBJECTID, $RUNID, $LANG", default="/static/complete.html")
redirect_url = models.CharField(max_length=128, help_text="URL to redirect to when Questionnaire is complete. Macros: $SUBJECTID, $RUNID, $LANG. Leave blank to render the 'complete.$LANG.html' template.", default="", blank=True)
html = models.TextField(u'Html', blank=True)
parse_html = models.BooleanField("Render html instead of name for survey?", null=False, default=False)
admin_access_only = models.BooleanField("Only allow access to logged in users? (This allows entering paper surveys without allowing new external submissions)", null=False, default=False)
@ -99,6 +111,29 @@ class Questionnaire(models.Model):
("management", "Management Tools")
)
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')
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')
def _hash(self):
return uuid.uuid4().hex
def __str__(self):
return self.label
def url(self):
return settings.BASE_URL_SECURE + reverse('landing', args=[self.nonce])
def config_landing(sender, instance, created, **kwargs):
if created:
instance.nonce=instance._hash()
instance.save()
post_save.connect(config_landing,sender=Landing)
class DBStylesheet(models.Model):
#Questionnaire max length of name is 128; Questionset max length of heading
@ -120,9 +155,9 @@ class QuestionSet(models.Model):
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="This is interpreted as Textile: <a href='http://en.wikipedia.org/wiki/Textile_%28markup_language%29' target='_blank'>http://en.wikipedia.org/wiki/Textile_(markup_language)</a>")
text = models.TextField(u'Text', help_text="HTML or Text")
parse_html = models.BooleanField("parse questionset heading and text as html?", null=False, default=False)
parse_html = models.BooleanField("Render html in heading?", null=False, default=False)
def questions(self):
if not hasattr(self, "__qcache"):
@ -182,12 +217,12 @@ class QuestionSet(models.Model):
["sortid",]
]
class RunInfo(models.Model):
"Store the active/waiting questionnaire runs here"
subject = models.ForeignKey(Subject)
random = models.CharField(max_length=32) # probably a randomized md5sum
runid = models.CharField(max_length=32)
landing = models.ForeignKey(Landing, 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?
@ -274,11 +309,11 @@ class RunInfo(models.Model):
["random"],
]
class RunInfoHistory(models.Model):
subject = models.ForeignKey(Subject)
runid = models.CharField(max_length=32)
completed = models.DateTimeField()
landing = models.ForeignKey(Landing, null=True, blank=True)
tags = models.TextField(
blank=True,
help_text=u"Tags used on this run, separated by commas"
@ -330,9 +365,9 @@ 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 interpreted as textile", blank=True)
footer = models.TextField(u"Footer", help_text="Footer rendered below the question", blank=True)
parse_html = models.BooleanField("parse question text and footer as html?", null=False, default=False)
parse_html = models.BooleanField("Render html in Footer?", null=False, default=False)
def questionnaire(self):
return self.questionset.questionnaire
@ -412,11 +447,12 @@ class Question(models.Model):
def is_comment(self):
return self.type == 'comment'
# def __cmp__(a, b):
# anum, astr = split_numal(a.number)
# bnum, bstr = split_numal(b.number)
# cmpnum = cmp(anum, bnum)
# return cmpnum or cmp(astr, bstr)
def get_value_for_run_question(self, runid):
runanswer = Answer.objects.filter(runid=runid,question=self)
if len(runanswer) > 0:
return runanswer[0].answer
else:
return None
class Meta:
translate = ('text', 'extra', 'footer')
@ -424,7 +460,6 @@ class Question(models.Model):
["number", "questionset"],
]
class Choice(models.Model):
__metaclass__ = TransMeta
@ -442,7 +477,6 @@ class Choice(models.Model):
index_together = [
['value'],
]
class Answer(models.Model):
subject = models.ForeignKey(Subject, help_text = u'The user who supplied this answer')

View File

@ -1,13 +1,4 @@
#!/usr/bin/python
from questionnaire.models import Answer
def get_value_for_run_question(runid, questionid):
runanswer = Answer.objects.filter(runid=runid,question__id=questionid)
if len(runanswer) > 0:
return runanswer[0].answer
else:
return None
#!/usr/bin/python
if __name__ == "__main__":
import doctest

View File

@ -1,6 +1,6 @@
from django.contrib import admin
from models import Page
from .models import Page
class PageAdmin(admin.ModelAdmin):
list_display = ('slug', 'title',)

View File

@ -4,13 +4,16 @@ from django.conf import settings
from django.template import RequestContext
from django import http
from django.utils import translation
from models import Page
from .models import Page
def page(request, page_to_render):
try:
p = Page.objects.get(slug=page_to_render, public=True)
except Page.DoesNotExist:
raise http.Http404('%s page requested but not found' % page_to_render)
return render_to_response("pages/{}.html".format(page_to_render),
{ "request" : request,},
context_instance = RequestContext(request)
)
return render_to_response("page.html",
{ "request" : request, "page" : p, },
@ -22,7 +25,7 @@ def langpage(request, lang, page_to_trans):
return page(request, page_to_trans)
def set_language(request):
next = request.REQUEST.get('next', None)
next = request.POST.get('next', request.GET.get('next', None))
if not next:
next = request.META.get('HTTP_REFERER', None)
if not next:

View File

@ -1,11 +1,10 @@
from django.conf import settings
from questionnaire import *
from django.utils.translation import ugettext as _
# add all the question types to QuestionChoices before anything else
from .. import add_type
import simple # store value as returned
import choice # multiple choice, do checks
import range_or_number # range of numbers
import timeperiod # time periods
import custom # backwards compatibility support
from . import simple # store value as returned
from . import choice # multiple choice, do checks
from . import range_or_number # range of numbers
from . import timeperiod # time periods
from . import custom # backwards compatibility support
add_type('custom', 'Custom field')

View File

@ -1,12 +1,12 @@
from questionnaire import *
from django.utils.translation import ugettext as _, ungettext
from json import dumps
import ast
from questionnaire.utils import get_runid_from_request
from questionnaire.modelutils import get_value_for_run_question
from django.utils.translation import ugettext as _, ungettext
from .. import add_type, question_proc, answer_proc, AnswerException
from ..utils import get_runid_from_request
@question_proc('choice', 'choice-freeform', 'dropdown')
@question_proc('choice', 'choice-freeform', 'dropdown', 'choice-optional', 'choice-freeform-optional')
def question_choice(request, question):
choices = []
jstriggers = []
@ -15,7 +15,7 @@ def question_choice(request, question):
key = "question_%s" % question.number
key2 = "question_%s_comment" % question.number
val = None
possibledbvalue = get_value_for_run_question(get_runid_from_request(request), question.id)
possibledbvalue = question.get_value_for_run_question(get_runid_from_request(request))
if key in request.POST:
val = request.POST[key]
elif not possibledbvalue == None:
@ -27,22 +27,25 @@ def question_choice(request, question):
for choice in question.choices():
choices.append( ( choice.value == val, choice, ) )
if question.type == 'choice-freeform':
if question.type in ( 'choice-freeform','choice-freeform-optional'):
jstriggers.append('%s_comment' % question.number)
template = question.type[:-9] if question.type.endswith('-optional') else question.type
return {
'choices' : choices,
'sel_entry' : val == '_entry_',
'qvalue' : val or '',
'required' : True,
"template" : "questionnaire/{}.html".format(template),
'required' : not question.type in ( 'choice-optional', 'choice-freeform-optional'),
'comment' : request.POST.get(key2, ""),
'jstriggers': jstriggers,
}
@answer_proc('choice', 'choice-freeform', 'dropdown')
@answer_proc('choice', 'choice-freeform', 'dropdown', 'choice-optional', 'choice-freeform-optional')
def process_choice(question, answer):
opt = answer['ANSWER'] or ''
if not opt:
if not opt and not question.type.endswith( '-optional'):
raise AnswerException(_(u'You must select an option'))
if opt == '_entry_' and question.type == 'choice-freeform':
opt = answer.get('comment','')
@ -51,11 +54,13 @@ def process_choice(question, answer):
return dumps([[opt]])
else:
valid = [c.value for c in question.choices()]
if opt not in valid:
if opt and opt not in valid:
raise AnswerException(_(u'Invalid option!'))
return dumps([opt])
add_type('choice', 'Choice [radio]')
add_type('choice-freeform', 'Choice with a freeform option [radio]')
add_type('choice-optional', 'Optional choice [radio]')
add_type('choice-freeform-optional', 'Optional choice with a freeform option [radio]')
add_type('dropdown', 'Dropdown choice [select]')
@question_proc('choice-multiple', 'choice-multiple-freeform', 'choice-multiple-values')
@ -67,7 +72,7 @@ def question_multiple(request, question):
qvalues = []
cd = question.getcheckdict()
defaults = cd.get('default','').split(',')
possibledbvalue = get_value_for_run_question(get_runid_from_request(request), question.id)
possibledbvalue = question.get_value_for_run_question(get_runid_from_request(request))
possiblelist = []
if not possibledbvalue == None:
possiblelist = ast.literal_eval(possibledbvalue)

View File

@ -3,9 +3,9 @@
# exist in the drop down list of the management interface.
#
from questionnaire import *
from questionnaire import Processors, QuestionProcessors
from django.utils.translation import ugettext as _
from .. import add_type, question_proc, answer_proc, AnswerException
from .. import Processors, QuestionProcessors
@question_proc('custom')
def question_custom(request, question):

View File

@ -1,10 +1,9 @@
from questionnaire import *
import ast
from json import dumps
from django.conf import settings
from django.utils.translation import ugettext as _
from json import dumps
import ast
from questionnaire.utils import get_runid_from_request
from questionnaire.modelutils import get_value_for_run_question
from ..utils import get_runid_from_request
from .. import add_type, question_proc, answer_proc, AnswerException
@question_proc('range', 'number')
def question_range_or_number(request, question):
@ -15,7 +14,7 @@ def question_range_or_number(request, question):
runit = cd.get('unit', '')
#try loading current from database before just setting to min
possibledbvalue = get_value_for_run_question(get_runid_from_request(request), question.id)
possibledbvalue = question.get_value_for_run_question(get_runid_from_request(request))
#you can't eval none nor can you eval empty
if not possibledbvalue == None and len(possibledbvalue) > 0:

View File

@ -1,15 +1,14 @@
from questionnaire import *
from questionnaire.utils import get_runid_from_request
from questionnaire.modelutils import get_value_for_run_question
from django.utils.translation import ugettext as _
from json import dumps
import ast
from json import dumps
from django.utils.translation import ugettext as _
from .. import add_type, question_proc, answer_proc, AnswerException
from ..utils import get_runid_from_request
#true if either 'required' or if 'requiredif' is satisfied
#def is_required
@question_proc('choice-yesno', 'choice-yesnocomment', 'choice-yesnodontknow')
@question_proc('choice-yesno', 'choice-yesnocomment', 'choice-yesnodontknow','choice-yesno-optional', 'choice-yesnocomment-optional', 'choice-yesnodontknow-optional')
def question_yesno(request, question):
key = "question_%s" % question.number
key2 = "question_%s_comment" % question.number
@ -19,17 +18,17 @@ def question_yesno(request, question):
cd = question.getcheckdict()
jstriggers = []
if qtype == 'choice-yesnocomment':
if qtype.startswith('choice-yesnocomment'):
hascomment = True
else:
hascomment = False
if qtype == 'choice-yesnodontknow' or 'dontknow' in cd:
if qtype.startswith( 'choice-yesnodontknow') or 'dontknow' in cd:
hasdontknow = True
else:
hasdontknow = False
#try the database before reverting to default
possiblevalue = get_value_for_run_question(get_runid_from_request(request), question.id)
possiblevalue = question.get_value_for_run_question(get_runid_from_request(request))
if not possiblevalue == None:
#save process always listifies the answer so we unlistify it to put it back in the field
valueaslist = ast.literal_eval(possiblevalue)
@ -51,7 +50,7 @@ def question_yesno(request, question):
checks = ' checks="dep_check(\'%s,dontknow\')"' % question.number
return {
'required': True,
'required': not qtype.endswith("-optional"),
'checks': checks,
'value': val,
'qvalue': val,
@ -71,7 +70,7 @@ def question_open(request, question):
value = request.POST[key]
else:
#also try to get it from the database so we can handle back/forward in which post has been cleared
possiblevalue = get_value_for_run_question(get_runid_from_request(request), question.id)
possiblevalue = question.get_value_for_run_question(get_runid_from_request(request))
if not possiblevalue == None:
#save process always listifies the answer so we unlistify it to put it back in the field
valueaslist = ast.literal_eval(possiblevalue)
@ -83,14 +82,14 @@ def question_open(request, question):
}
@answer_proc('open', 'open-textfield', 'choice-yesno', 'choice-yesnocomment', 'choice-yesnodontknow')
@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()
if qtype.startswith('choice-yesno'):
if ans not in ('yes', 'no', 'dontknow'):
if ans not in ('yes', 'no', 'dontknow') and not qtype.endswith('-optional'):
raise AnswerException(_(u'You must select an option'))
if qtype == 'choice-yesnocomment' \
and len(ansdict.get('comment', '').strip()) == 0:
@ -122,6 +121,9 @@ add_type('open-textfield', 'Open Answer, multi-line [textarea]')
add_type('choice-yesno', 'Yes/No Choice [radio]')
add_type('choice-yesnocomment', 'Yes/No Choice with optional comment [radio, input]')
add_type('choice-yesnodontknow', 'Yes/No/Don\'t know Choice [radio]')
add_type('choice-yesno-optional', 'Optional Yes/No Choice [radio]')
add_type('choice-yesnocomment-optional', 'Optional Yes/No Choice with optional comment [radio, input]')
add_type('choice-yesnodontknow-optional', 'Optional Yes/No/Don\'t know Choice [radio]')
@answer_proc('comment')

View File

@ -1,5 +1,5 @@
from questionnaire import *
from django.utils.translation import ugettext as _, ugettext_lazy
from .. import add_type, question_proc, answer_proc, AnswerException
perioddict = {
"second" : ugettext_lazy("second(s)"),

File diff suppressed because one or more lines are too long

View File

@ -1,8 +1,17 @@
/* question numbers are always bold, the whole question if it is required */
.required, .qnumber {
/* question numbers are bold */
.qnumber {
font-weight: bold;
}
.required::after {
content: " * ";
color: red;
}
.required:hover::after {
content: " * required";
color: red;
}
.questionset-title {
/* margin right is there to make space for the progressbar */
margin: 0 220px 25px 0;
@ -12,9 +21,15 @@
margin-bottom: 25px;
}
.questionset-text p{
font-size: 1.2em;
line-height: 1.2em;
}
.question-text {
font-size: 1.3em;
line-height: 1.3em;
font-size: 1.2em;
line-height: 1.2em;
}
div.input {
@ -65,9 +80,6 @@ div.error {
margin: 15px 0px 0px 0px;
}
html, body {
background-color: #cdcdcd;
}
.content {
background-color: #fff;

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}Questionnaire{% endblock title %}</title>
<link rel="stylesheet" href="/static/bootstrap/bootstrap.min.css" type="text/css" />
<link rel="stylesheet" href="/static/questionnaire.css" />
<style type="text/css">
{% block styleextra %}
{% endblock %}
</style>
{% block headextra %}
{% endblock %}
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<div class="content">
<div id="languages">
{% block language %}
{% for lang in LANGUAGES %}
{% if not forloop.first %} | {% endif %}
<a href="/setlang/?lang={{ lang.0 }}&next={{ request.path }}">{{ lang.1 }}</a>
{% endfor %}
{% endblock language %}
</div>
<div class="page-header">
<h1>Sample Django Questionnaire</h1>
</div>
<div class="row">
<div class="span1">&nbsp;</div>
<div class="span14">
{% block content %}
{% block questionnaire %}
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud execitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
{% endblock %}
{% endblock %}
</div>
<div class="span1">&nbsp;</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -1,10 +1,10 @@
{% extends "base.html" %}
{% extends "base-questionnaire.html" %}
{% block title %}
{{ block.super }} - {{ page.title }}
{% endblock %}
{% block content %}
{% block questionnaire %}
{{ page.body }}
{% if user.is_authenticated %}
<a href="/admin/page/page/{{ page.slug }}/">(edit)</a>

View File

@ -0,0 +1,8 @@
{% extends "base-questionnaire.html" %}
{% block questionnaire %}
<h2>
Thanks for completing the survey!
</h2>
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends 'basedocumentation.html' %}
{% block title %}Tell us Stuff!{% endblock %}
{% block doccontent %}
<h2>Tell us stuff!</h2>
<p>We are excited that this is Open Access book and really hope that it will be shared around, read by lots of people all over the world, and used in lots of exciting new ways. We would love to hear about you and how you are using it - please would you share your story with us using this link: (it will only take about 3 minutes - and you could just get a free book as well ….)</p>
<p>But comeback again, our survey for {{landing.label}} isn't ready yet.</p>
{% endblock %}

View File

@ -1,7 +1,7 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<ul class="inputs-list">
<ul class="inputs-list list-unstyled">
{% for sel, choice in qdict.choices %}
<li>
<label>
@ -14,10 +14,12 @@
</div>
<div class="input">
<input onClick="valchanged('{{ question.number }}', '_entry_');" type="radio" id="{{ question.number }}_entry" name="question_{{ question.number }}" value="_entry_" {% if qdict.sel_entry %} checked {% endif %}>
<input id="{{ question.number }}_comment" checks="dep_check('{{ question.number }},_entry_')" type="input" name="question_{{ question.number }}_comment" value="{{ qdict.comment }}">
{% if question.extra %}
<span class="help-block">{{ question.extra }}</span>
<label for="{{ question.number }}_entry"><span class="extra-block">{{ question.extra }}</span></label>
{% else %}
<label for="{{ question.number }}_entry">{% trans "Other..." %}</label>
{% endif %}
<input id="{{ question.number }}_comment" checks="dep_check('{{ question.number }},_entry_')" type="text" name="question_{{ question.number }}_comment" id="{{ question.number }}_comment" value="{{ qdict.comment }}">
</div>
</div>

View File

@ -2,10 +2,10 @@
<div class="clearfix">
<div class="input">
<ul class="inputs-list">
<ul class="inputs-list list-unstyled">
{% for choice, key, checked, prev_value in qdict.choices %}
<li>
<!-- <label> -->
<label>
<span class="{{ qdict.type }}-text">
<input onClick="valchanged('{{ question.number }}_{{ choice.value }}', this.checked);" type="checkbox" id="{{ key }}" name="{{ key }}" value="{{ choice.value }}" {{ checked }}>
{{ choice.text }}
@ -18,7 +18,7 @@
&#37; <!-- percentage sign: all choice-multiple-values currently represent percentages and must add up to 100% -->
</span>
{% endif %}
<!-- </label> -->
</label>
</li>
{% endfor %}
{% if qdict.type == 'choice-multiple-values' %}
@ -27,22 +27,26 @@
</script>
{% endif %}
{% if question.extra %}
<li>
<label for="{{ question.number }}extra"><span class="extra-block">{{ question.extra }}</span></label>
</li>
{% else %}
<li>
<label for="{{ question.number }}extra">{% trans "Other..." %}</label>
</li>
{% endif %}
{% if qdict.extras %}
{% for key, value in qdict.extras %}
<li>
{% if not forloop.last or not forloop.first %}
<b>{{ forloop.counter }}.</b>
{% endif %}
<input type="text" name="{{ key }}" size="50" value="{{ value }}">
<input type="text" id="{{ question.number }}extra{% if not forloop.first %}{{ forloop.counter }}{% endif %}" name="{{ key }}" size="50" value="{{ value }}">
</li>
{% endfor %}
{% endif %}
{% if question.extra %}
<li>
<span class="help-block">{{ question.extra }}</span>
</li>
{% endif %}
</ul>
</div>
</div>

View File

@ -1,7 +1,7 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<ul class="inputs-list">
<ul class="inputs-list list-unstyled">
<!-- yes -->
<li>
@ -31,12 +31,12 @@
<!-- comment -->
{% if qdict.hascomment %}
<li>
<input type="text" id="{{ question.number }}_comment" name="question_{{ question.number }}_comment" value="{{ qdict.comment }}" size="50" {{ qdict.checks|safe }} placeholder="{% trans 'comment' %}">
<li><label>
{% if question.extra %}
<span class="help-block">{{ question.extra }}</span>
<span class="extra-block">{{ question.extra }}</span><br />
{% endif %}
</li>
<input type="text" id="{{ question.number }}_comment" name="question_{{ question.number }}_comment" value="{{ qdict.comment }}" size="50" {{ qdict.checks|safe }} placeholder="{% trans 'comment' %}">
</label></li>
{% endif %}
</ul>
</div>

View File

@ -1,7 +1,7 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<ul class="inputs-list">
<ul class="inputs-list list-unstyled">
{% for sel, choice in qdict.choices %}
<li>
<label>

View File

@ -1,5 +1,5 @@
{% extends "base.html" %}
{% block content %}
{% extends "base-questionnaire.html" %}
{% block questionnaire %}
<h2>
Merci vielmals! Die Umfragung ist fertig! Sie bekommen eine neue Einladung nächstes Jahr.
</h2>

View File

@ -0,0 +1,18 @@
{% extends "base-questionnaire.html" %}
{% block questionnaire %}
<h2>
Thanks for completing the survey!
</h2>
<div class="question-text">
{{ landing_object.claim.all.0.rights_holder }}
{% if request.COOKIES.next %}
<p>redirecting in 5 seconds...</p>
<script type="text/JavaScript">
setTimeout(function(){location.replace('/next/');}, 5000);
</script>
{% endif %}
</div>
{% endblock %}

View File

@ -1,10 +0,0 @@
{% extends "base.html" %}
{% block content %}
<h2>
Thanks for completing the survey! You will be contacted again next year.
</h2>
<h4>
If you change your email address in the meantime, contact us please.
</h4>
{% endblock %}

View File

@ -1,7 +1,7 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<textarea class="span8" name="question_{{ question.number }}" cols="60" rows="10">{{ qdict.value }}</textarea>
<textarea class="span8" name="question_{{ question.number }}" cols="60" rows="10" id={{ question.number }}>{{ qdict.value }}</textarea>
{% if question.extra %}
<span class="help-block">{{ question.extra }}</span>
{% endif %}

View File

@ -1,17 +1,33 @@
{% extends "base.html" %}
{% extends "base-questionnaire.html" %}
{% load questionnaire i18n %}
{% load static %}
{% load dynamicStyleTags %}
{% load landings %}
{% block title %}
Survey: {{ questionset.heading }}
{% endblock %}
{% block headextra %}
<script type="text/javascript" src="{% static 'jquery-1.7.1.min.js' %}"></script>
<script type="text/javascript" src="{% static 'questionset.js' %}"></script>
<!-- <link rel="stylesheet" href="{% static 'progressbar.css' %}"/>-->
<link rel="stylesheet" href="{% static 'progressbar.css' %}" />
{% if questionsetstylesheet|getAssociatedStylesheets %}
<style type="text/css">
{{ questionsetstylesheet|getAssociatedStylesheets|safe }}
</style>
{% endif %}
{% if progress %}
{% if questionset.questionnaire.name|add:"Progress"|getAssociatedStylesheets %}
<style type="text/css">
{{ questionset.questionnaire.name|add:"Progress"|getAssociatedStylesheets|safe }}
</style>
{% else %}
<style type="text/css">
{{ "CommonProgressStyles"|getAssociatedStylesheets|safe }}
</style>
{% endif %}
{% endif %}
{% for x in jsinclude %}
<script type="text/javascript" src="{{ x }}"></script>
{% endfor %}
@ -34,18 +50,9 @@
{% endfor %}
{% endblock %}
{% block content %}
{% block questionnaire %}
{% if progress %}
{% if questionset.questionnaire.name|add:"Progress"|getAssociatedStylesheets %}
<style type="text/css">
{{ questionset.questionnaire.name|add:"Progress"|getAssociatedStylesheets|safe }}
</style>
{% else %}
<style type="text/css">
{{ "CommonProgressStyles"|getAssociatedStylesheets|safe }}
</style>
{% endif %}
<div id="progress_bar" class="ui-progress-bar ui-container">
<div class="ui-progress" style="width: {{progress}}%;">
<span class="ui-label"><b class="value">{{progress}}%</b></span>
@ -57,7 +64,7 @@
<h2 class="questionset-title">
{% if questionset.heading %}
{% if questionset.parse_html %}
{{ questionset.heading|safe }}
{% render_with_landing questionset.heading|safe %}
{% else %}
{{ questionset.heading }}
{% endif %}
@ -67,7 +74,7 @@
<div class="questionset-text">
{% if questionset.text %}
{% if questionset.parse_html %}
{{ questionset.text|safe }}
{% render_with_landing questionset.text|safe %}
{% else %}
{{ questionset.text }}
{% endif %}
@ -107,12 +114,14 @@
{% include qdict.template %}
{% else %}
<div class="question-text {% if qdict.required %}required{% endif %}">
<span class="qnumber">{% if not question.parse_html %}{{ question.display_number|safe }}.{% endif %}</span>
<label for="{{ question.number }}">
<span class="qnumber">{{ question.display_number|safe }}.</span>
{% if question.parse_html %}
{{ question.text|safe }}
{% else %}
{{ question.text }}
{% endif %}
</label>
</div>
<div class="answer">
{% if error %}
@ -138,7 +147,7 @@
<div style="text-align: center;" class="well questionset-submit">
<input class="btn large primary" name="submit" type="submit" value="{% trans "Continue" %}">
<input class="btn large primary" name="submit" type="submit" value="{% if questionset.next %}{% trans 'Continue' %}{% else %}{% trans 'Finish' %}{% endif %}" />
</div>

View File

@ -0,0 +1,15 @@
import django.template
from django.template import Template, Context
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'):
context['landing_object'] = context['runinfo'].landing.content_object
if text:
template = Template(text)
return template.render(context)
else:
return ''

View File

@ -1,19 +1,15 @@
#!/usr/bin/python
from django import template
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
from django.core.urlresolvers import reverse
register = template.Library()
@register.filter(name="dictget")
def dictget(thedict, key):
"{{ dictionary|dictget:variableholdingkey }}"
return thedict.get(key, None)
@register.filter(name="spanclass")
def spanclass(string):
l = 2 + len(string.strip()) // 6
@ -29,6 +25,5 @@ def spanclass(string):
def qtesturl(question):
qset = question.questionset
return reverse("questionset",
args=("test:%s" % qset.questionnaire.id,
qset.sortid))
args=("test:%s" % qset.questionnaire.id,
qset.sortid))

View File

@ -1,6 +1,6 @@
from django.test import TestCase
from dependency_checker import check_actual_answers_against_expression, explode_answer_into_list
from .dependency_checker import check_actual_answers_against_expression, explode_answer_into_list
from .models import Question

View File

@ -1,8 +1,8 @@
# vim: set fileencoding=utf-8
from django.conf.urls import *
from views import *
from .views import *
from .page.views import page, langpage
urlpatterns = patterns(
'',
@ -12,13 +12,21 @@ urlpatterns = patterns(
export_csv, name='export_csv'),
url(r'^(?P<runcode>[^/]+)/progress/$',
get_async_progress, name='progress'),
url(r'^take/(?P<questionnaire_id>[0-9]+)/$', generate_run),
url(r'^$', page, {'page_to_render' : 'index'}),
url(r'^(?P<page_to_render>.*)\.html$', page),
url(r'^(?P<lang>..)/(?P<page_to_trans>.*)\.html$', langpage),
url(r'^setlang/$', set_language),
url(r'^landing/(?P<nonce>\w+)/$', SurveyView.as_view(), name="landing"),
url(r'^(?P<runcode>[^/]+)/(?P<qs>[-]{0,1}\d+)/$',
questionnaire, name='questionset'),
url(r'^q/manage/csv/(\d+)/',
export_csv, name="export_csv"),
)
if not use_session:
urlpatterns += patterns(
'',
url(r'^(?P<runcode>[^/]+)/(?P<qs>[-]{0,1}\d+)/$',
questionnaire, name='questionset'),
url(r'^(?P<runcode>[^/]+)/$',
questionnaire, name='questionnaire'),
url(r'^(?P<runcode>[^/]+)/(?P<qs>[-]{0,1}\d+)/prev/$',

View File

@ -1,4 +1,9 @@
#!/usr/bin/python
from django.conf import settings
try:
use_session = settings.QUESTIONNAIRE_USE_SESSION
except AttributeError:
use_session = False
def split_numal(val):
"""Split, for example, '1a' into (1, 'a')
@ -42,18 +47,10 @@ def numal0_sort(a, b):
return numal_sort(a[0], b[0])
def get_runid_from_request(request):
request_string = str(request)
string_chunks = request_string.split('/')
return string_chunks[2]
def get_sortid_from_request(request):
request_string = str(request)
string_chunks = request_string.split('/')
if len(string_chunks) > 3:
return string_chunks[3]
#not enough string chunks to get a sortid
return None
if use_session:
return request.session.get('runcode', None)
else:
return request.runinfo.runid
if __name__ == "__main__":
import doctest

View File

@ -1,6 +1,6 @@
#!/usr/bin/python
from questionnaire.models import RunInfoHistory, Answer
from .models import RunInfoHistory, Answer
def get_completed_answers_for_questions(questionnaire_id, question_list):
completed_questionnaire_runs = RunInfoHistory.objects.filter(questionnaire__id=questionnaire_id)

View File

@ -6,23 +6,24 @@ from django.core.urlresolvers import reverse
from django.core.cache import cache
from django.contrib.auth.decorators import permission_required
from django.shortcuts import render_to_response, get_object_or_404
from django.views.generic.base import TemplateView
from django.db import transaction
from django.conf import settings
from datetime import datetime
from django.utils import translation
from django.utils.translation import ugettext_lazy as _
from questionnaire import QuestionProcessors
from questionnaire import questionnaire_start, questionset_start, questionset_done, questionnaire_done
from questionnaire import AnswerException
from questionnaire import Processors
from questionnaire.models import *
from questionnaire.parsers import *
from questionnaire.parsers import BoolNot, BoolAnd, BoolOr, Checker
from questionnaire.emails import _send_email, send_emails
from questionnaire.utils import numal_sort, split_numal, get_sortid_from_request
from questionnaire.request_cache import request_cache
from questionnaire.dependency_checker import dep_check
from questionnaire import profiler
from . import QuestionProcessors
from . import questionnaire_start, questionset_start, questionset_done, questionnaire_done
from . import AnswerException
from . import Processors
from . import profiler
from .models import *
from .parsers import *
from .parsers import BoolNot, BoolAnd, BoolOr, Checker
from .emails import _send_email, send_emails
from .utils import numal_sort, split_numal
from .request_cache import request_cache
from .dependency_checker import dep_check
from compat import commit_on_success, commit, rollback
import logging
import random
@ -54,9 +55,9 @@ def get_runinfo(random):
return res and res[0] or None
def get_question(number, questionnaire):
"Return the specified Question (by number) from the specified Questionnaire"
res = Question.objects.filter(number=number, questionset__questionnaire=questionnaire)
def get_question(number, questionset):
"Return the specified Question (by number) from the specified Questionset"
res = Question.objects.filter(number=number, questionset=questionset)
return res and res[0] or None
@ -299,7 +300,7 @@ def redirect_to_prev_questionnaire(request, runcode=None, qs=None):
"""
Takes the questionnaire set in the session and redirects to the
previous questionnaire if any. Used for linking to previous pages
both when using sessions or not. (Has not been tested with sessions.)
both when using sessions or not.
"""
if use_session:
runcode = request.session.get('runcode', None)
@ -448,7 +449,7 @@ def questionnaire(request, runcode=None, qs=None):
key, value = item[0], item[1]
if key.startswith('question_'):
answer = key.split("_", 2)
question = get_question(answer[1], questionnaire)
question = get_question(answer[1], questionset)
if not question:
logging.warn("Unknown question when processing: %s" % answer[1])
continue
@ -522,13 +523,14 @@ def finish_questionnaire(request, runinfo, questionnaire):
hist.questionnaire = questionnaire
hist.tags = runinfo.tags
hist.skipped = runinfo.skipped
hist.landing = runinfo.landing
hist.save()
questionnaire_done.send(sender=None, runinfo=runinfo,
questionnaire=questionnaire)
lang=translation.get_language()
redirect_url = questionnaire.redirect_url
for x, y in (('$LANG', translation.get_language()),
for x, y in (('$LANG', lang),
('$SUBJECTID', runinfo.subject.id),
('$RUNID', runinfo.runid),):
redirect_url = redirect_url.replace(x, str(y))
@ -542,7 +544,7 @@ def finish_questionnaire(request, runinfo, questionnaire):
commit()
if redirect_url:
return HttpResponseRedirect(redirect_url)
return r2r("questionnaire/complete.$LANG.html", request)
return r2r("questionnaire/complete.{}.html".format(lang), request, landing_object=hist.landing.content_object)
def recursivly_build_partially_evaluated_javascript_expression_for_shownif_check(treenode, runinfo, question):
@ -711,7 +713,6 @@ def show_questionnaire(request, runinfo, errors={}):
else:
qvalues[s[1]] = v
if use_session:
prev_url = reverse('redirect_to_prev_questionnaire')
else:
@ -780,7 +781,7 @@ def set_language(request, runinfo=None, next=None):
Can also be used by a url handler, w/o runinfo & next.
"""
if not next:
next = request.REQUEST.get('next', None)
next = request.GET.get('next', request.POST.get('next', None))
if not next:
next = request.META.get('HTTP_REFERER', None)
if not next:
@ -811,9 +812,9 @@ def _table_headers(questions):
ql.sort(lambda x, y: numal_sort(x.number, y.number))
columns = []
for q in ql:
if q.type == 'choice-yesnocomment':
if q.type.startswith('choice-yesnocomment'):
columns.extend([q.number, q.number + "-freeform"])
elif q.type == 'choice-freeform':
elif q.type.startswith('choice-freeform'):
columns.extend([q.number, q.number + "-freeform"])
elif q.type.startswith('choice-multiple'):
cl = [c.value for c in q.choice_set.all()]
@ -1026,7 +1027,7 @@ def send_email(request, runinfo_id):
return r2r("emailsent.html", request, runinfo=runinfo, successful=successful)
def generate_run(request, questionnaire_id, subject_id=None):
def generate_run(request, questionnaire_id, subject_id=None, context={}):
"""
A view that can generate a RunID instance anonymously,
and then redirect to the questionnaire itself.
@ -1044,19 +1045,16 @@ def generate_run(request, questionnaire_id, subject_id=None):
if subject_id is not None:
su = get_object_or_404(Subject, pk=subject_id)
else:
su = Subject.objects.filter(givenname='Anonymous', surname='User')[0:1]
if su:
su = su[0]
else:
su = Subject(givenname='Anonymous', surname='User')
su.save()
su = Subject(anonymous=True, ip_address=request.META['REMOTE_ADDR'])
su.save()
# 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()
landing = context.get('landing', None)
run = RunInfo(subject=su, random=key, runid=key, questionset=qs)
run = RunInfo(subject=su, random=key, runid=key, questionset=qs, landing=landing)
run.save()
if not use_session:
kwargs = {'runcode': key}
@ -1065,7 +1063,29 @@ def generate_run(request, questionnaire_id, subject_id=None):
request.session['runcode'] = key
questionnaire_start.send(sender=None, runinfo=run, questionnaire=qu)
return HttpResponseRedirect(reverse('questionnaire', kwargs=kwargs))
response = HttpResponseRedirect(reverse('questionnaire', kwargs=kwargs))
response.set_cookie('next', context.get('next',''))
return response
def generate_error(request):
return 400/0
class SurveyView(TemplateView):
template_name = "pages/generic.html"
def get_context_data(self, **kwargs):
context = super(SurveyView, self).get_context_data(**kwargs)
nonce = self.kwargs['nonce']
landing = get_object_or_404(Landing, nonce=nonce)
context["landing"] = landing
context["next"] = self.request.GET.get('next', '')
return context
def get(self, request, *args, **kwargs):
context = self.get_context_data(**kwargs)
if context['landing'].questionnaire:
return generate_run(request, context['landing'].questionnaire.id, context=context)
return self.render_to_response(context)

View File

@ -6,14 +6,14 @@ def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setup(
name="ed-questionnaire",
version="2.0.1",
name="fef-questionnaire",
version="4.0.0",
description="A Django application for creating online questionnaires/surveys.",
long_description=read("README.md"),
author="Eldest Daughter, LLC.",
author_email="gcaprio@eldestdaughter.com",
author="Eldest Daughter, LLC.","Free Ebook Foundation"
author_email="gcaprio@eldestdaughter.com", "eric@hellman.net"
license="BSD",
url="https://github.com/eldest-daughter/ed-questionnaire",
url="https://github.com/EbookFoundation/fef-questionnaire",
packages=find_packages(exclude=["example"]),
include_package_data=True,
classifiers=[
@ -23,8 +23,6 @@ setup(
"License :: OSI Approved :: BSD License",
"Operating System :: OS Independent",
"Programming Language :: Python",
'Programming Language :: Python :: 2.5',
'Programming Language :: Python :: 2.6',
'Programming Language :: Python :: 2.7',
"Framework :: Django",
],