commit
a3dfce7e90
|
@ -7,6 +7,7 @@ local_settings.py
|
|||
*egg-info
|
||||
*.egg
|
||||
build
|
||||
dist
|
||||
.project
|
||||
.pydevproject
|
||||
.settings
|
||||
|
|
210
README.md
210
README.md
|
@ -1,8 +1,6 @@
|
|||
FEF Questionnaire
|
||||
=====================
|
||||
# FEF Questionnaire
|
||||
|
||||
Introduction
|
||||
------------
|
||||
## Introduction
|
||||
|
||||
FEF Questionnaire is a Django questionnaire app which is easily customizable
|
||||
and includes advanced dependency support using boolean expressions.
|
||||
|
@ -16,8 +14,7 @@ In either mode, an instance can be linked to an arbitrary object via the django
|
|||
|
||||
Try out the questionaire on the Unglue.it page for "Open Access Ebooks" https://unglue.it/work/82028/
|
||||
|
||||
History
|
||||
-------
|
||||
## History
|
||||
|
||||
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.
|
||||
|
||||
|
@ -30,12 +27,11 @@ The old versions are tagged as follows:
|
|||
|
||||
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
|
||||
The "FEF-questionnaire" version was created to add the ability to link the questionnaire to individual books in a book database. We'll call this v4.0. The app was extensively renovated and updated. This work was funded by the Mellon Foundation as part of the [Mapping the Free Ebook Supply Chain Project](https://www.publishing.umich.edu/projects/mapping-the-free-ebook/).
|
||||
|
||||
About this Manual
|
||||
-----------------
|
||||
## About this Manual
|
||||
|
||||
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.
|
||||
Questionnaire was 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 please help us improve it.
|
||||
|
||||
What it does cover is the following:
|
||||
|
||||
|
@ -44,8 +40,17 @@ What it does cover is the following:
|
|||
* **Migration** explains how a questionnaire defined with 1.0 can be used in 2.0.
|
||||
* **2.0 Postmortem** talks about some experiences made during the development of 2.0.
|
||||
|
||||
Integration
|
||||
-----------
|
||||
|
||||
|
||||
## Integration
|
||||
|
||||
### Install
|
||||
|
||||
If you just want to install, start with
|
||||
|
||||
pip install fef-Questionnaire
|
||||
|
||||
### Example Setup
|
||||
|
||||
This part of the docs will take you through the steps needed to create a questionnaire app from scratch. It should also be quite handy for the task of integrating the questionnaire into an existing site.
|
||||
|
||||
|
@ -56,7 +61,7 @@ First, create a folder for your new site:
|
|||
|
||||
Create a virtual environment so your python packages don't influence your system
|
||||
|
||||
virtualenv --no-site-packages -p python2.5 .
|
||||
virtualenv --no-site-packages -p python2.7 .
|
||||
|
||||
Activate your virtual environment
|
||||
|
||||
|
@ -64,7 +69,7 @@ Activate your virtual environment
|
|||
|
||||
Install Django
|
||||
|
||||
pip install django
|
||||
pip install django==1.8.18
|
||||
|
||||
Create your Django site
|
||||
|
||||
|
@ -80,7 +85,7 @@ Clone the questionnaire source
|
|||
|
||||
git clone git://github.com/EbookFoundation/fef-questionnaire.git
|
||||
|
||||
You should now have a ed-questionnaire folder in your apps folder
|
||||
You should now have a fef-questionnaire folder in your apps folder
|
||||
|
||||
cd fef-questionnaire
|
||||
|
||||
|
@ -90,7 +95,61 @@ 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.
|
||||
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).
|
||||
|
||||
|
||||
Also add the locale and request cache middleware to MIDDLEWARE_CLASSES:
|
||||
|
||||
'questionnaire.request_cache.RequestCacheMiddleware'
|
||||
|
||||
Add the questionnaire template directory as well as your own to TEMPLATES:
|
||||
|
||||
'DIRS': [os.path.join(BASE_DIR, 'mysite/templates/')],
|
||||
|
||||
If you want to use multiple languages, add the i18n context processor to TEMPLATES
|
||||
'context_processors': ['django.template.context_processors.i18n',]
|
||||
|
||||
Now add `transmeta`, `questionnaire` to your INSTALLED_APPS:
|
||||
|
||||
'transmeta',
|
||||
'questionnaire',
|
||||
'questionnaire.page',
|
||||
|
||||
To finish the settings, add the fef-questionaire specific parameters. For our example, we'll use:
|
||||
|
||||
QUESTIONNAIRE_PROGRESS = 'async'
|
||||
QUESTIONNAIRE_USE_SESSION = False
|
||||
QUESTIONNAIRE_ITEM_MODEL = 'mysite.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"
|
||||
|
||||
class Book(models.Model):
|
||||
title = models.CharField(max_length=1000, default="")
|
||||
landings = GenericRelation(Landing, related_query_name='items')
|
||||
def __unicode__(self):
|
||||
return self.title
|
||||
|
||||
|
||||
### 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:
|
||||
|
||||
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
|
||||
|
||||
You will be asked to create a superuser.
|
||||
|
||||
The questionnaire expects a `base-questionnaire.html` template to be there, with certain stylesheets and blocks inside. Have a look at `./apps/fef-questionnaire/example/templates/base-questionnaire.html`. if you're adding the app to an existing project.
|
||||
|
||||
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.
|
||||
|
||||
### Internationalizating the database
|
||||
|
||||
First, you want to setup the languages used in your questionnaire. Open up your `mysite` folder in your favorite text editor.
|
||||
|
||||
|
@ -101,90 +160,37 @@ Open `mysite/mysite/settings.py` and add following lines, representing your lang
|
|||
('de', 'Deutsch')
|
||||
)
|
||||
|
||||
At the top of `settings.py` you should at this point add:
|
||||
Now, you'll need to
|
||||
|
||||
import os.path
|
||||
|
||||
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/fef-questionnaire/questionnaire/static/'),
|
||||
)
|
||||
|
||||
Also add the locale and request cache middleware to MIDDLEWARE_CLASSES:
|
||||
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'questionnaire.request_cache.RequestCacheMiddleware',
|
||||
|
||||
If you are using Django 1.7 you will need to comment out the following line, like so:
|
||||
# 'django.middleware.security.SecurityMiddleware',
|
||||
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/fef-questionnaire/questionnaire/templates'),
|
||||
os.path.abspath('./templates'),
|
||||
|
||||
And finally, add `transmeta`, `questionnaire` to your INSTALLED_APPS:
|
||||
|
||||
'django.contrib.sites',
|
||||
'transmeta',
|
||||
'questionnaire',
|
||||
'questionnaire.page',
|
||||
|
||||
To get the "sites" framework working you also need to add the following setting:
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
Next up we want to edit the `urls.py` file of your project to link the questionnaire views to your site's url configuration.
|
||||
|
||||
For an empty site with enabled admin interface you add:
|
||||
|
||||
from django.conf.urls import patterns, include, url
|
||||
|
||||
from django.contrib import admin
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
|
||||
# questionnaire urls
|
||||
url(r'q/', include('questionnaire.urls')),
|
||||
)
|
||||
|
||||
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:
|
||||
|
||||
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 makemigrations
|
||||
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/fef-questionnaire/example/templates/base.html`.
|
||||
If you want to use multiple languages, add the i18n context processor to TEMPLATES
|
||||
'context_processors': ['django.template.context_processors.i18n',]
|
||||
|
||||
and set up middleware as described in the [Django translation docs](https://docs.djangoproject.com/en/1.8/topics/i18n/translation/)
|
||||
|
||||
For now you might want to just copy the `base.html` to your own template folder.
|
||||
To see example questionnaires you can do the following (Note: this will only work if you have both English and German defined as Languages in `settings.py`):
|
||||
|
||||
mkdir templates
|
||||
cd templates
|
||||
cp ../apps/fef-questionnaire/example/templates/base.html .
|
||||
python manage.py loaddata ./apps/fef-questionnaire/example/fixtures/example.yaml
|
||||
python manage.py loaddata ./apps/fef-questionnaire/example/fixtures/books.yaml
|
||||
|
||||
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`):
|
||||
### Start the server!
|
||||
|
||||
python manage.py loaddata ./apps/fef-questionnaire/example/fixtures/initial_data.yaml
|
||||
|
||||
You may then start your development server:
|
||||
Start your development server:
|
||||
|
||||
python manage.py runserver
|
||||
|
||||
And navigate to [localhost:8000](http://localhost:8000/).
|
||||
|
||||
Concepts
|
||||
--------
|
||||
First, go to the admin console and log yourself in. Otherwise, there won't be items for you to link questionnaires to.
|
||||
|
||||
Take a questionnaire. the "Example" has English and German translations. the "MappingSurvey" is English only.
|
||||
|
||||
|
||||
|
||||
## Concepts
|
||||
|
||||
The ED Questionnaire has the following tables, described in detail below.
|
||||
|
||||
|
@ -294,28 +300,12 @@ 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.
|
||||
In Poll mode, the landing url links a Questionnaire to an Object and a User to a Subject. This is useful if you have a database of things you want to ask questions about.
|
||||
|
||||
Migration of 1.x to 2.0
|
||||
-----------------------
|
||||
|
||||
2.0 added new fields to the questionnaire, but it did so in a backwards compatible way. None of the new fields are mandatory and no changes should be necessary to your existing questionnaire. Since we do not have any relevant testing data however, you might find yourself on your own if it doesn't work. Please file an issue if you think we did something wrong, so we can fix it and help you.
|
||||
|
||||
As Django per default does not provide a way to migrate database schemas, we pretty much make use of the bulldozer way of migrating, by exporting the data from one database and import it into a newly created one.
|
||||
|
||||
From you existing 1.x site do:
|
||||
|
||||
python manage.py dumpdata >> export.yaml
|
||||
|
||||
Copy your file to your new site and in your new site, create your empty database:
|
||||
|
||||
python manage.py syncdb
|
||||
|
||||
You may then import your data from your old site, which should probably work :)
|
||||
|
||||
python manage.py loaddata export.yaml
|
||||
|
||||
This of course covers only the data migration. How to migrate your custom tailored site to use questionnaire 2.0 is unfortunately something we cannot really document.
|
||||
Version 4.0 does not support migration of 1.X data files.
|
||||
|
||||
2.0 Postmortem
|
||||
--------------
|
||||
|
@ -324,7 +314,7 @@ This of course covers only the data migration. How to migrate your custom tailor
|
|||
|
||||
Here's what we think we learned:
|
||||
|
||||
### ED.questionnaire is a Framework
|
||||
### Questionnaire is a Framework
|
||||
|
||||
More than anything else ed.questionnaire should be thought of as a framework. Your site has to provide and do certain things for the questionnaire to work. If your site is a customized questionnaire for a company with other needs on the same site you will end up integrating code which will call questionnaire to setup runs and you will probably work through the answer records to provide some sort of summary.
|
||||
|
||||
|
@ -354,7 +344,11 @@ Version 4.0 has not been tested for compatibility with previous versions.
|
|||
* 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
|
||||
|
||||
* styling of required questions has been spiffed up.
|
||||
* export of response data has been fixed.
|
||||
* compatibility with Django 1.8. Compatibility with other versions of Django has not been tested.
|
||||
* refactoring of views
|
||||
* documentation has been updated to reflect Django 1.8.
|
||||
* email and subject functionality has not been tested
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
- fields: {title: "A Christmas Carol"}
|
||||
model: mysite.book
|
||||
pk: 4
|
||||
- fields: {title: "A Little Princess"}
|
||||
model: mysite.book
|
||||
pk: 5
|
||||
- fields: {title: "A Portrait of the Artist as a Young Man"}
|
||||
model: mysite.book
|
||||
pk: 6
|
||||
- fields: {title: "A Room with a View"}
|
||||
model: mysite.book
|
||||
pk: 7
|
||||
- fields: {title: "Agnes Grey"}
|
||||
model: mysite.book
|
||||
pk: 8
|
||||
- fields: {title: "Anne of Green Gables"}
|
||||
model: mysite.book
|
||||
pk: 9
|
||||
- fields: {title: "The Personal History, Adventures, Experience and Observation of David Copperfield the Younger of Blunderstone Rookery"}
|
||||
model: mysite.book
|
||||
pk: 10
|
||||
- fields: {title: "Far From the Madding Crowd"}
|
||||
model: mysite.book
|
||||
pk: 11
|
||||
- fields: {title: "Howards End"}
|
||||
model: mysite.book
|
||||
pk: 12
|
||||
- fields: {title: "Jacob\'s Room"}
|
||||
model: mysite.book
|
||||
pk: 13
|
||||
- fields: {title: "The Merry Adventures of Robin Hood"}
|
||||
model: mysite.book
|
||||
pk: 14
|
||||
- fields: {title: "Sense and Sensibility"}
|
||||
model: mysite.book
|
||||
pk: 15
|
||||
- fields: {title: "The Secret Garden"}
|
||||
model: mysite.book
|
||||
pk: 16
|
||||
- fields: {title: "The Adventures of Tom Sawyer"}
|
||||
model: mysite.book
|
||||
pk: 17
|
||||
- fields: {title: "The Invisible Man"}
|
||||
model: mysite.book
|
||||
pk: 18
|
||||
- fields: {title: "The Last of the Mohicans"}
|
||||
model: mysite.book
|
||||
pk: 19
|
||||
- fields: {title: "Oliver Twist, or the Parish Boy\'s Progress"}
|
||||
model: mysite.book
|
||||
pk: 20
|
||||
- fields: {title: "Peter Pan in Kensington Gardens"}
|
||||
model: mysite.book
|
||||
pk: 21
|
||||
- fields: {title: "Tales of the Jazz Age"}
|
||||
model: mysite.book
|
||||
pk: 22
|
||||
- fields: {title: "Tess of the D'Urbervilles: A Pure Woman Faithfully Presented"}
|
||||
model: mysite.book
|
||||
pk: 23
|
|
@ -0,0 +1,337 @@
|
|||
- fields: {admin_access_only: false, html: '', name: 'Example Questionnaire', parse_html: false, redirect_url: /}
|
||||
model: questionnaire.questionnaire
|
||||
pk: 1
|
||||
- fields: {checks: '', heading: Page 1, parse_html: true, questionnaire: 1, sortid: 1,
|
||||
text_de: <h2> Biervorlieben</h2>, text_en: <h2>Beer Preferences</h2>}
|
||||
model: questionnaire.questionset
|
||||
pk: 1
|
||||
- fields: {checks: '', heading: Page 2, parse_html: false, questionnaire: 1, sortid: 2,
|
||||
text_de: h1. Python Web Frameworks, text_en: h1. Python Web Frameworks}
|
||||
model: questionnaire.questionset
|
||||
pk: 2
|
||||
- fields: {checks: '', heading: Finished!, parse_html: true, questionnaire: 1, sortid: 99,
|
||||
text_de: "<h2> Vielen Dank </h2>\r\n \r\n Wir hoffen, dass Sie uns in Zukunft
|
||||
wieder besuchen!", text_en: "<h2>Thank you! </h2>\r\n \r\n We hope that you
|
||||
visit us again in the future!"}
|
||||
model: questionnaire.questionset
|
||||
pk: 3
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: null, footer_en: '',
|
||||
number: '1', parse_html: false, questionset: 1, sort_id: null, text_de: 'Trinken
|
||||
Sie Bier?', text_en: 'Do you drink beer?', type: choice-yesno}
|
||||
model: questionnaire.question
|
||||
pk: 1
|
||||
- fields: {checks: 'requiredif="1,yes"', extra_de: '', extra_en: '', footer_de: null,
|
||||
footer_en: '', number: '2', parse_html: false, questionset: 1, sort_id: null,
|
||||
text_de: 'Was Art von Bier trinken Sie am meisten?', text_en: 'What type of beer
|
||||
do you drink predominantly?', type: choice-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 2
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: null, footer_en: '',
|
||||
number: '3', parse_html: false, questionset: 1, sort_id: null, text_de: 'Welche
|
||||
von dieser Bieren haben Sie probiert?', text_en: 'Which of these brands of beer
|
||||
have you tried?', type: choice-multiple}
|
||||
model: questionnaire.question
|
||||
pk: 3
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: null, footer_en: '',
|
||||
number: '4', parse_html: false, questionset: 2, sort_id: null, text_de: 'Which
|
||||
Python Web Frameworks have you tried?', text_en: 'Which Python Web Frameworks
|
||||
have you tried?', type: choice-multiple-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 4
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: null, footer_en: '',
|
||||
number: '5', parse_html: false, questionset: 2, sort_id: null, text_de: "Welche
|
||||
m\xF6gen Sie am liebsten?", text_en: 'Which do you like the most?', type: choice-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 5
|
||||
- fields: {question: 2, sortid: 1, tags: '', text_de: Pils, text_en: Pils/Bitter,
|
||||
value: pils}
|
||||
model: questionnaire.choice
|
||||
pk: 1
|
||||
- fields: {question: 2, sortid: 2, tags: '', text_de: Helles, text_en: Lager, value: helles}
|
||||
model: questionnaire.choice
|
||||
pk: 2
|
||||
- fields: {question: 2, sortid: 3, tags: '', text_de: Weizen, text_en: Wheat-beer,
|
||||
value: weizen}
|
||||
model: questionnaire.choice
|
||||
pk: 3
|
||||
- fields: {question: 3, sortid: 1, tags: '', text_de: Heineken, text_en: Heineken,
|
||||
value: heineken}
|
||||
model: questionnaire.choice
|
||||
pk: 4
|
||||
- fields: {question: 3, sortid: 2, tags: '', text_de: Becks, text_en: Becks, value: becks}
|
||||
model: questionnaire.choice
|
||||
pk: 5
|
||||
- fields: {question: 3, sortid: 3, tags: '', text_de: "L\xF6wenbr\xE4u", text_en: "L\xF6wenbr\xE4u",
|
||||
value: lowenbrau}
|
||||
model: questionnaire.choice
|
||||
pk: 6
|
||||
- fields: {question: 2, sortid: 4, tags: '', text_de: "Altbier/D\xFCssel", text_en: Altbier,
|
||||
value: altbier}
|
||||
model: questionnaire.choice
|
||||
pk: 7
|
||||
- fields: {question: 2, sortid: 5, tags: '', text_de: "K\xF6lsch", text_en: "K\xF6lsch",
|
||||
value: koelsch}
|
||||
model: questionnaire.choice
|
||||
pk: 8
|
||||
- fields: {question: 4, sortid: 1, tags: '', text_de: Django, text_en: Django, value: django}
|
||||
model: questionnaire.choice
|
||||
pk: 9
|
||||
- fields: {question: 4, sortid: 2, tags: '', text_de: Pylons, text_en: Pylons, value: pylons}
|
||||
model: questionnaire.choice
|
||||
pk: 10
|
||||
- fields: {question: 4, sortid: 3, tags: '', text_de: Turbogears, text_en: Turbogears,
|
||||
value: turbogears}
|
||||
model: questionnaire.choice
|
||||
pk: 11
|
||||
- fields: {question: 4, sortid: 4, tags: '', text_de: CherryPy, text_en: CherryPy,
|
||||
value: cherrypy}
|
||||
model: questionnaire.choice
|
||||
pk: 12
|
||||
- fields: {question: 5, sortid: 1, tags: '', text_de: Django, text_en: Django, value: django}
|
||||
model: questionnaire.choice
|
||||
pk: 13
|
||||
- fields: {question: 5, sortid: 2, tags: '', text_de: Pylons, text_en: Pylons, value: pylons}
|
||||
model: questionnaire.choice
|
||||
pk: 14
|
||||
- fields: {question: 5, sortid: 3, tags: '', text_de: Turbogears, text_en: Turbogears,
|
||||
value: turbogears}
|
||||
model: questionnaire.choice
|
||||
pk: 15
|
||||
- fields: {question: 5, sortid: 4, tags: '', text_de: CherryPy, text_en: CherryPy,
|
||||
value: cherrypy}
|
||||
model: questionnaire.choice
|
||||
pk: 16
|
||||
- fields: {body_de: "Wilkommen zu der Fragebogenbeispielseite!\r\n\r\nWenn Sie einen
|
||||
einfachen Fragebogen sehen wollen, <a href=\"/q/take/1\"> \"tun Sie's!</a>\r\n\r\nSie
|
||||
k\xF6nnen das <a href=\"/admin/\">Admin Interface</a> benutzen, um den Fragebogen
|
||||
zu \xE4ndern.", body_en: "Welcome to the Example Questionnaire website!\r\n\r\nIf you wish to
|
||||
take a sample questionnaire, <a href=\"q/take/1\">go for it!</a>. Or <a href=\"q/items/\">link
|
||||
a questionnaire to an item</a>\r\n\r\nUse the <a href=\"/admin/\">Admin Interface</a> to
|
||||
change the questionnaire.", public: true,
|
||||
title_de: '', title_en: Welcome}
|
||||
model: page.page
|
||||
pk: index
|
||||
- fields: {admin_access_only: false, html: questionnaire html here, name: MappingSurvey,
|
||||
parse_html: false, redirect_url: ''}
|
||||
model: questionnaire.questionnaire
|
||||
pk: 3
|
||||
- fields: {checks: '', heading: Open Access Ebooks (Part 1), parse_html: true, questionnaire: 3,
|
||||
sortid: 1, text_de: '', text_en: " <h1> Introduction </h1> \r\n <p> \r\nWelcome, reader of
|
||||
<i>{{ landing_object.title }}</i>! And thanks for visiting Unglue.it to complete
|
||||
this survey, part of a research project to understand how open access ebooks
|
||||
are discovered and how readers use them. For more information, please see <a
|
||||
href=\"http://www.publishing.umich.edu/projects/mapping-the-free-ebook/\">the
|
||||
project description</a>.\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 10 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 spy on you 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. A <a href=\"http://www.publishing.umich.edu/projects/mapping-the-free-ebook/\">report
|
||||
of the results</a> will be made available in July 2017 ]\r\n</p>"}
|
||||
model: questionnaire.questionset
|
||||
pk: 5
|
||||
- fields: {checks: '', heading: Now About You..., parse_html: true, questionnaire: 3,
|
||||
sortid: 2, text_de: '', text_en: ' <p> And now, four 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_de: '', extra_en: '', footer_de: '', footer_en: '', number: '1', parse_html: true,
|
||||
questionset: 5, sort_id: 1, text_de: '', 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? - or in some other way?\r\n", type: open}
|
||||
model: questionnaire.question
|
||||
pk: 16
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '2', parse_html: false,
|
||||
questionset: 5, sort_id: 2, text_de: '', 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? Your library?", type: open}
|
||||
model: questionnaire.question
|
||||
pk: 17
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '3', parse_html: true,
|
||||
questionset: 5, sort_id: 3, text_de: '', text_en: 'Why are you interested in this book?', type: choice-multiple-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 18
|
||||
- fields: {checks: '', extra_de: '', extra_en: 'If Yes - is there any particular reason why you
|
||||
are using this version rather than one of the others?', footer_de: '', footer_en: '', number: '4',
|
||||
parse_html: false, questionset: 5, sort_id: 4, text_de: '', text_en: 'Are you aware that this
|
||||
title is available in multiple different digital and printed formats?', type: choice-yesnocomment-optional}
|
||||
model: questionnaire.question
|
||||
pk: 19
|
||||
- fields: {checks: '', extra_de: '', extra_en: 'Please tell us in more detail:', footer_de: '', 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_de: '', 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_de: '', extra_en: '', footer_de: '', footer_en: '', number: '1', parse_html: false,
|
||||
questionset: 6, sort_id: null, text_de: '', text_en: 'Where do you live?', type: choice-freeform-optional}
|
||||
model: questionnaire.question
|
||||
pk: 21
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '2', parse_html: false,
|
||||
questionset: 6, sort_id: null, text_de: '', text_en: 'What do you do for a living?', type: open}
|
||||
model: questionnaire.question
|
||||
pk: 22
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: "\r\n\r\n \r\n\r\n", number: '4',
|
||||
parse_html: false, questionset: 6, sort_id: null, text_de: '', text_en: 'When did you finish
|
||||
your formal education?', type: choice-freeform-optional}
|
||||
model: questionnaire.question
|
||||
pk: 23
|
||||
- fields: {checks: required-no, extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '5', parse_html: false,
|
||||
questionset: 6, sort_id: null, text_de: '', text_en: ' Is there anything else you would like
|
||||
to tell us or think we should know about how you found or are using the ebook?
|
||||
or about yourself?', type: open-textfield}
|
||||
model: questionnaire.question
|
||||
pk: 24
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '1', parse_html: false,
|
||||
questionset: 7, sort_id: null, text_de: '', 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: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '3', parse_html: false,
|
||||
questionset: 6, sort_id: null, text_de: '', text_en: 'How old are you?', type: choice-optional}
|
||||
model: questionnaire.question
|
||||
pk: 26
|
||||
- fields: {question: 18, sortid: 1, tags: '', text_de: '', 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_de: '', 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_de: '', text_en: I need to read it for a course,
|
||||
value: course}
|
||||
model: questionnaire.choice
|
||||
pk: 19
|
||||
- fields: {question: 20, sortid: 1, tags: '', text_de: '', 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_de: '', 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_de: '', 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_de: '', 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_de: '', 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_de: '', text_en: 'Share it with my friends ',
|
||||
value: share}
|
||||
model: questionnaire.choice
|
||||
pk: 25
|
||||
- fields: {question: 20, sortid: 7, tags: '', text_de: '', text_en: Print it out, value: print}
|
||||
model: questionnaire.choice
|
||||
pk: 26
|
||||
- fields: {question: 20, sortid: 8, tags: '', text_de: '', text_en: "I\u2019m creating/collating
|
||||
a (online) library", value: catalog}
|
||||
model: questionnaire.choice
|
||||
pk: 27
|
||||
- fields: {question: 20, sortid: 9, tags: '', text_de: '', text_en: "Something else entirely \u2026. ",
|
||||
value: else}
|
||||
model: questionnaire.choice
|
||||
pk: 28
|
||||
- fields: {question: 21, sortid: 1, tags: '', text_de: '', text_en: Canada/USA, value: us}
|
||||
model: questionnaire.choice
|
||||
pk: 29
|
||||
- fields: {question: 21, sortid: 4, tags: '', text_de: '', text_en: Europe, value: eu}
|
||||
model: questionnaire.choice
|
||||
pk: 30
|
||||
- fields: {question: 21, sortid: 3, tags: '', text_de: '', text_en: South America, value: sa}
|
||||
model: questionnaire.choice
|
||||
pk: 31
|
||||
- fields: {question: 21, sortid: 2, tags: '', text_de: '', text_en: Central America/ Caribbean,
|
||||
value: ca}
|
||||
model: questionnaire.choice
|
||||
pk: 32
|
||||
- fields: {question: 21, sortid: 9, tags: '', text_de: '', text_en: Other Asia, value: as}
|
||||
model: questionnaire.choice
|
||||
pk: 33
|
||||
- fields: {question: 21, sortid: 6, tags: '', text_de: '', text_en: Africa, value: af}
|
||||
model: questionnaire.choice
|
||||
pk: 34
|
||||
- fields: {question: 21, sortid: 5, tags: '', text_de: '', text_en: Middle East, value: me}
|
||||
model: questionnaire.choice
|
||||
pk: 35
|
||||
- fields: {question: 21, sortid: 11, tags: '', text_de: '', text_en: Another Planet, value: ap}
|
||||
model: questionnaire.choice
|
||||
pk: 36
|
||||
- fields: {question: 23, sortid: 1, tags: '', text_de: '', text_en: "I haven\u2019t - I\u2019m
|
||||
still a student", value: x}
|
||||
model: questionnaire.choice
|
||||
pk: 37
|
||||
- fields: {question: 23, sortid: 2, tags: '', text_de: '', text_en: At primary/elementary school,
|
||||
value: '8'}
|
||||
model: questionnaire.choice
|
||||
pk: 38
|
||||
- fields: {question: 23, sortid: 3, tags: '', text_de: '', text_en: At high school/secondary school,
|
||||
value: h}
|
||||
model: questionnaire.choice
|
||||
pk: 39
|
||||
- fields: {question: 23, sortid: 4, tags: '', text_de: '', text_en: After trade qualifications,
|
||||
value: t}
|
||||
model: questionnaire.choice
|
||||
pk: 40
|
||||
- fields: {question: 23, sortid: 5, tags: '', text_de: '', text_en: 'At College/Undergraduate Degree ',
|
||||
value: c}
|
||||
model: questionnaire.choice
|
||||
pk: 41
|
||||
- fields: {question: 23, sortid: 6, tags: '', text_de: '', text_en: At Grad School/post-graduate
|
||||
university, value: g}
|
||||
model: questionnaire.choice
|
||||
pk: 42
|
||||
- fields: {question: 18, sortid: 4, tags: '', text_de: '', text_en: 'If other, tell us more...',
|
||||
value: other}
|
||||
model: questionnaire.choice
|
||||
pk: 43
|
||||
- fields: {question: 26, sortid: 1, tags: '', text_de: '', text_en: under 18, value: teen}
|
||||
model: questionnaire.choice
|
||||
pk: 46
|
||||
- fields: {question: 26, sortid: 2, tags: '', text_de: '', text_en: 18-30, value: young}
|
||||
model: questionnaire.choice
|
||||
pk: 47
|
||||
- fields: {question: 26, sortid: 3, tags: '', text_de: '', text_en: 31-60, value: mid}
|
||||
model: questionnaire.choice
|
||||
pk: 48
|
||||
- fields: {question: 26, sortid: 4, tags: '', text_de: '', text_en: over 60, value: old}
|
||||
model: questionnaire.choice
|
||||
pk: 49
|
||||
- fields: {question: 26, sortid: 5, tags: '', text_de: '', text_en: decline to say, value: x}
|
||||
model: questionnaire.choice
|
||||
pk: 50
|
||||
- fields: {question: 21, sortid: 10, tags: '', text_de: '', text_en: Oceania, value: oc}
|
||||
model: questionnaire.choice
|
||||
pk: 51
|
||||
- fields: {question: 21, sortid: 7, tags: '', text_de: '', text_en: India, value: in}
|
||||
model: questionnaire.choice
|
||||
pk: 52
|
||||
- fields: {question: 21, sortid: 8, tags: '', text_de: '', text_en: China, value: zh}
|
||||
model: questionnaire.choice
|
||||
pk: 53
|
|
@ -1,107 +0,0 @@
|
|||
- fields: {domain: example.com, name: example.com}
|
||||
model: sites.site
|
||||
pk: 1
|
||||
- fields: {email: test@example.com, formtype: email, gender: unset, givenname: Test,
|
||||
language: de, nextrun: 2011-05-16, state: active, surname: Test}
|
||||
model: questionnaire.subject
|
||||
pk: 1
|
||||
- fields: {email: null, formtype: email, gender: unset, givenname: Anonymous, language: en,
|
||||
nextrun: null, state: inactive, surname: User}
|
||||
model: questionnaire.subject
|
||||
pk: 2
|
||||
- fields: {name: example, redirect_url: /}
|
||||
model: questionnaire.questionnaire
|
||||
pk: 1
|
||||
- fields: {checks: '', heading: Page 1, questionnaire: 1, sortid: 1, text_de: h1. Biervorlieben,
|
||||
text_en: h1. Beer Preferences}
|
||||
model: questionnaire.questionset
|
||||
pk: 1
|
||||
- fields: {checks: '', heading: Page 2, questionnaire: 1, sortid: 2, text_de: h1. Python
|
||||
Web Frameworks, text_en: h1. Python Web Frameworks}
|
||||
model: questionnaire.questionset
|
||||
pk: 2
|
||||
- fields: {checks: '', heading: Thankyou, questionnaire: 1, sortid: 99, text_de: "h1. Vielen Dank \n \n Wir hoffen, dass Sie uns in Zukunft wieder besuchen!", text_en: "h1. Thank you! \n \n We hope that you visit us again in the future!"}
|
||||
model: questionnaire.questionset
|
||||
pk: 3
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', number: '1', questionset: 1, text_de: 'Trinken
|
||||
Sie Bier?', text_en: 'Do you drink beer?', type: choice-yesno}
|
||||
model: questionnaire.question
|
||||
pk: 1
|
||||
- fields: {checks: 'requiredif="1,yes"', extra_de: '', extra_en: '', number: '2',
|
||||
questionset: 1, text_de: 'Was Art von Bier trinken Sie am meisten?', text_en: 'What
|
||||
type of beer do you drink predominantly?', type: choice-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 2
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', number: '3', questionset: 1, text_de: 'Welche
|
||||
von dieser Bieren haben Sie probiert?', text_en: 'Which of these brands of beer
|
||||
have you tried?', type: choice-multiple}
|
||||
model: questionnaire.question
|
||||
pk: 3
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', number: '4', questionset: 2, text_de: 'Which
|
||||
Python Web Frameworks have you tried?', text_en: 'Which Python Web Frameworks
|
||||
have you tried?', type: choice-multiple-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 4
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', number: '5', questionset: 2, text_de: "Welche\
|
||||
\ m\xF6gen Sie am liebsten?", text_en: 'Which do you like the most?', type: choice-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 5
|
||||
- fields: {question: 2, sortid: 1, text_de: Pils, text_en: Pils/Bitter, value: pils}
|
||||
model: questionnaire.choice
|
||||
pk: 1
|
||||
- fields: {question: 2, sortid: 2, text_de: Helles, text_en: Lager, value: helles}
|
||||
model: questionnaire.choice
|
||||
pk: 2
|
||||
- fields: {question: 2, sortid: 3, text_de: Weizen, text_en: Wheat-beer, value: weizen}
|
||||
model: questionnaire.choice
|
||||
pk: 3
|
||||
- fields: {question: 3, sortid: 1, text_de: Heineken, text_en: Heineken, value: heineken}
|
||||
model: questionnaire.choice
|
||||
pk: 4
|
||||
- fields: {question: 3, sortid: 2, text_de: Becks, text_en: Becks, value: becks}
|
||||
model: questionnaire.choice
|
||||
pk: 5
|
||||
- fields: {question: 3, sortid: 3, text_de: "L\xF6wenbr\xE4u", text_en: "L\xF6wenbr\xE4\
|
||||
u", value: lowenbrau}
|
||||
model: questionnaire.choice
|
||||
pk: 6
|
||||
- fields: {question: 2, sortid: 4, text_de: "Altbier/D\xFCssel", text_en: Altbier,
|
||||
value: altbier}
|
||||
model: questionnaire.choice
|
||||
pk: 7
|
||||
- fields: {question: 2, sortid: 5, text_de: "K\xF6lsch", text_en: "K\xF6lsch", value: koelsch}
|
||||
model: questionnaire.choice
|
||||
pk: 8
|
||||
- fields: {question: 4, sortid: 1, text_de: Django, text_en: Django, value: django}
|
||||
model: questionnaire.choice
|
||||
pk: 9
|
||||
- fields: {question: 4, sortid: 2, text_de: Pylons, text_en: Pylons, value: pylons}
|
||||
model: questionnaire.choice
|
||||
pk: 10
|
||||
- fields: {question: 4, sortid: 3, text_de: Turbogears, text_en: Turbogears, value: turbogears}
|
||||
model: questionnaire.choice
|
||||
pk: 11
|
||||
- fields: {question: 4, sortid: 4, text_de: CherryPy, text_en: CherryPy, value: cherrypy}
|
||||
model: questionnaire.choice
|
||||
pk: 12
|
||||
- fields: {question: 5, sortid: 1, text_de: Django, text_en: Django, value: django}
|
||||
model: questionnaire.choice
|
||||
pk: 13
|
||||
- fields: {question: 5, sortid: 2, text_de: Pylons, text_en: Pylons, value: pylons}
|
||||
model: questionnaire.choice
|
||||
pk: 14
|
||||
- fields: {question: 5, sortid: 3, text_de: Turbogears, text_en: Turbogears, value: turbogears}
|
||||
model: questionnaire.choice
|
||||
pk: 15
|
||||
- fields: {question: 5, sortid: 4, text_de: CherryPy, text_en: CherryPy, value: cherrypy}
|
||||
model: questionnaire.choice
|
||||
pk: 16
|
||||
- fields: {body_de: "Wilkommen zu der Fragebogenbeispielseite!\r\n\r\nWenn Sie einen\
|
||||
\ einfachen Fragebogen sehen wollen, \"tun Sie's!\":/take/1\r\n\r\nSie k\xF6\
|
||||
nnen das \"Admin Interface\":/admin/ benutzen, um den Fragebogen zu \xE4ndern.",
|
||||
body_en: "Welcome to the example Questionnaire website!\r\n\r\nIf you wish to\
|
||||
\ take a sample questionnaire, \"go for it!\":/take/1\r\n\r\nUse the \"Admin\
|
||||
\ Interface\":/admin/, to change the questionnaire.", public: true, title_de: '',
|
||||
title_en: Welcome}
|
||||
model: page.page
|
||||
pk: index
|
|
@ -1,9 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import os
|
||||
import sys
|
||||
|
||||
if __name__ == '__main__':
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings')
|
||||
|
||||
from django.core.management import execute_from_command_line
|
||||
execute_from_command_line(sys.argv)
|
|
@ -0,0 +1,10 @@
|
|||
from django.contrib.contenttypes.fields import GenericRelation
|
||||
from django.db import models
|
||||
|
||||
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
|
|
@ -1,129 +1,119 @@
|
|||
# Django settings for example project.
|
||||
import os.path
|
||||
"""
|
||||
Django settings for mysite project.
|
||||
|
||||
Generated by 'django-admin startproject' using Django 1.8.18.
|
||||
|
||||
For more information on this file, see
|
||||
https://docs.djangoproject.com/en/1.8/topics/settings/
|
||||
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/1.8/ref/settings/
|
||||
"""
|
||||
|
||||
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
|
||||
import os
|
||||
|
||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
|
||||
# Quick-start development settings - unsuitable for production
|
||||
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
|
||||
|
||||
# SECURITY WARNING: keep the secret key used in production secret!
|
||||
SECRET_KEY = '3b@kp9-x$dxp@)aqct^$vf^*n95^@k%jd)&kx_%*(kj#0s+sty'
|
||||
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
TEMPLATE_DEBUG = DEBUG
|
||||
|
||||
ADMINS = (
|
||||
# ('Your Name', 'your_email@domain.com'),
|
||||
)
|
||||
|
||||
MANAGERS = ADMINS
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3', # Add 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
|
||||
'NAME': 'example.sqlite', # Or path to database file if using sqlite3.
|
||||
'USER': '', # Not used with sqlite3.
|
||||
'PASSWORD': '', # Not used with sqlite3.
|
||||
'HOST': '', # Set to empty string for localhost. Not used with sqlite3.
|
||||
'PORT': '', # Set to empty string for default. Not used with sqlite3.
|
||||
}
|
||||
}
|
||||
|
||||
# Local time zone for this installation. Choices can be found here:
|
||||
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
|
||||
# although not all choices may be available on all operating systems.
|
||||
# If running in a Windows environment this must be set to the same as your
|
||||
# system time zone.
|
||||
TIME_ZONE = 'Europe/Berlin'
|
||||
|
||||
# Language code for this installation. All choices can be found here:
|
||||
# http://www.i18nguy.com/unicode/language-identifiers.html
|
||||
LANGUAGE_CODE = 'en'
|
||||
|
||||
SITE_ID = 1
|
||||
|
||||
# If you set this to False, Django will make some optimizations so as not
|
||||
# to load the internationalization machinery.
|
||||
USE_I18N = True
|
||||
|
||||
# Absolute path to the directory that holds media.
|
||||
# Example: "/home/media/media.lawrence.com/"
|
||||
MEDIA_ROOT = ''
|
||||
|
||||
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
|
||||
# trailing slash if there is a path component (optional in other cases).
|
||||
# Examples: "http://media.lawrence.com", "http://example.com/media/"
|
||||
MEDIA_URL = '/media/'
|
||||
|
||||
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
|
||||
# trailing slash.
|
||||
# Examples: "http://foo.com/media/", "/media/".
|
||||
ADMIN_MEDIA_PREFIX = '/static/admin/'
|
||||
|
||||
# Make this unique, and don't share it with anybody.
|
||||
SECRET_KEY = 'j69g6-&t0l43f06iq=+u!ni)9n)g!ygy4dk-dgdbrbdx7%9l*6'
|
||||
|
||||
# Absolute path to the directory static files should be collected to.
|
||||
# Don't put anything in this directory yourself; store your static files
|
||||
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
|
||||
# Example: "/home/media/media.lawrence.com/static/"
|
||||
STATIC_ROOT = os.path.abspath('./static_root')
|
||||
|
||||
# URL prefix for static files.
|
||||
# Example: "http://media.lawrence.com/static/"
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# Additional locations of static files
|
||||
STATICFILES_DIRS = (
|
||||
os.path.abspath('./static'),
|
||||
os.path.abspath('../questionnaire/static/')
|
||||
)
|
||||
|
||||
# List of finder classes that know how to find static files in
|
||||
# various locations.
|
||||
STATICFILES_FINDERS = (
|
||||
'django.contrib.staticfiles.finders.FileSystemFinder',
|
||||
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
|
||||
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
|
||||
)
|
||||
ALLOWED_HOSTS = []
|
||||
|
||||
|
||||
# List of callables that know how to import templates from various sources.
|
||||
TEMPLATE_LOADERS = (
|
||||
'django.template.loaders.filesystem.Loader',
|
||||
'django.template.loaders.app_directories.Loader',
|
||||
)
|
||||
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'questionnaire.request_cache.RequestCacheMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'example.urls'
|
||||
|
||||
TEMPLATE_DIRS = (
|
||||
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
|
||||
# Always use forward slashes, even on Windows.
|
||||
# Don't forget to use absolute paths, not relative paths.
|
||||
os.path.abspath("../questionnaire/templates/"),
|
||||
os.path.abspath("./templates/"),
|
||||
)
|
||||
# Application definition
|
||||
|
||||
INSTALLED_APPS = (
|
||||
'django.contrib.admin',
|
||||
'django.contrib.auth',
|
||||
'django.contrib.contenttypes',
|
||||
'django.contrib.messages',
|
||||
'django.contrib.sessions',
|
||||
'django.contrib.sites',
|
||||
'django.contrib.admin',
|
||||
'django.contrib.admindocs',
|
||||
'django.contrib.staticfiles',
|
||||
'transmeta',
|
||||
'questionnaire',
|
||||
'questionnaire.page',
|
||||
'mysite',
|
||||
)
|
||||
|
||||
LANGUAGES = (
|
||||
('en', 'English'),
|
||||
('de', 'Deutsch'),
|
||||
MIDDLEWARE_CLASSES = (
|
||||
'django.contrib.sessions.middleware.SessionMiddleware',
|
||||
'django.middleware.locale.LocaleMiddleware',
|
||||
'django.middleware.common.CommonMiddleware',
|
||||
'django.middleware.csrf.CsrfViewMiddleware',
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
|
||||
'questionnaire.request_cache.RequestCacheMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'django.middleware.security.SecurityMiddleware',
|
||||
)
|
||||
|
||||
ROOT_URLCONF = 'mysite.urls'
|
||||
|
||||
TEMPLATES = [
|
||||
{
|
||||
'BACKEND': 'django.template.backends.django.DjangoTemplates',
|
||||
'DIRS': [os.path.join(BASE_DIR, 'mysite/templates/')],
|
||||
'APP_DIRS': True,
|
||||
'OPTIONS': {
|
||||
'context_processors': [
|
||||
'django.template.context_processors.debug',
|
||||
'django.template.context_processors.request',
|
||||
'django.template.context_processors.i18n',
|
||||
'django.contrib.auth.context_processors.auth',
|
||||
'django.contrib.messages.context_processors.messages',
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
WSGI_APPLICATION = 'mysite.wsgi.application'
|
||||
|
||||
|
||||
# Database
|
||||
# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
|
||||
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.sqlite3',
|
||||
'NAME': os.path.join(BASE_DIR, 'db.sqlite3'),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# Internationalization
|
||||
# https://docs.djangoproject.com/en/1.8/topics/i18n/
|
||||
|
||||
LANGUAGE_CODE = 'en'
|
||||
|
||||
TIME_ZONE = 'UTC'
|
||||
|
||||
USE_I18N = True
|
||||
|
||||
USE_L10N = True
|
||||
|
||||
USE_TZ = True
|
||||
|
||||
|
||||
# Static files (CSS, JavaScript, Images)
|
||||
# https://docs.djangoproject.com/en/1.8/howto/static-files/
|
||||
|
||||
STATIC_URL = '/static/'
|
||||
|
||||
# sets the available languages in the questionnaire
|
||||
LANGUAGES = (
|
||||
('en', 'English'),
|
||||
('de', 'Deutsch')
|
||||
)
|
||||
|
||||
# Defines the progressbar behavior in the questionnaire
|
||||
# the possible options are 'default', 'async' and 'none'
|
||||
#
|
||||
|
@ -143,6 +133,7 @@ LANGUAGES = (
|
|||
# 'none'
|
||||
# Completely omits the progressbar. Good if you don't want one or if the
|
||||
# questionnaire is so huge that even the ajax request takes too long.
|
||||
|
||||
QUESTIONNAIRE_PROGRESS = 'async'
|
||||
|
||||
# Defines how the questionnaire and questionset id are passed around.
|
||||
|
@ -152,5 +143,10 @@ QUESTIONNAIRE_PROGRESS = 'async'
|
|||
# user goes through the steps of the question set.
|
||||
QUESTIONNAIRE_USE_SESSION = False
|
||||
|
||||
try: from local_settings import *
|
||||
except: pass
|
||||
# for item-linked questionnaires, defines the model used for the item-linked questionaires.
|
||||
|
||||
QUESTIONNAIRE_ITEM_MODEL = 'mysite.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.
|
||||
|
||||
QUESTIONNAIRE_SHOW_ITEM_RESULTS = True
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
<!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"></script>
|
||||
|
||||
<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"> </div>
|
||||
<div class="span14">
|
||||
{% block content %}
|
||||
<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 %}
|
||||
</div>
|
||||
<div class="span1"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,18 +1,16 @@
|
|||
from django.conf.urls import patterns, include, url
|
||||
from django.contrib import admin
|
||||
|
||||
import questionnaire
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
|
||||
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'),
|
||||
url(r'q/', include('questionnaire.urls')),
|
||||
|
||||
# admin
|
||||
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"admin_access_only": false,
|
||||
"html": "survey html here",
|
||||
"html": "questionnaire html here",
|
||||
"name": "MappingSurvey",
|
||||
"parse_html": false,
|
||||
"redirect_url": ""
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
from django import forms
|
||||
from .models import Questionnaire
|
||||
|
||||
class NewLandingForm(forms.Form):
|
||||
label = forms.CharField(max_length=64, required=True)
|
||||
questionnaire = forms.ModelChoiceField(
|
||||
Questionnaire.objects.all(),
|
||||
widget=forms.widgets.RadioSelect(),
|
||||
empty_label=None,
|
||||
required=True,
|
||||
)
|
||||
|
|
@ -3,7 +3,7 @@ from ...models import Landing
|
|||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "make survey nonces with the specified label"
|
||||
help = "make landing nonces with the specified label"
|
||||
args = "<how_many> <label>"
|
||||
|
||||
def handle(self, how_many=1, label="no label yet", **options):
|
|
@ -75,8 +75,8 @@ class Migration(migrations.Migration):
|
|||
('name', models.CharField(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)')),
|
||||
('parse_html', models.BooleanField(default=False, verbose_name=b'Render html instead of name for questionnaire?')),
|
||||
('admin_access_only', models.BooleanField(default=False, verbose_name=b'Only allow access to logged in users? (This allows entering paper questionnaires without allowing new external submissions)')),
|
||||
],
|
||||
options={
|
||||
'permissions': (('export', 'Can export questionnaire answers'), ('management', 'Management Tools')),
|
||||
|
|
|
@ -4,6 +4,7 @@ import re
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
from transmeta import TransMeta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
@ -87,8 +88,8 @@ 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. 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)
|
||||
parse_html = models.BooleanField("Render html instead of name for questionnaire?", null=False, default=False)
|
||||
admin_access_only = models.BooleanField("Only allow access to logged in users? (This allows entering paper questionnaires without allowing new external submissions)", null=False, default=False)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
@ -126,7 +127,12 @@ class Landing(models.Model):
|
|||
return self.label
|
||||
|
||||
def url(self):
|
||||
return settings.BASE_URL_SECURE + reverse('landing', args=[self.nonce])
|
||||
try:
|
||||
return settings.BASE_URL_SECURE + reverse('landing', args=[self.nonce])
|
||||
except AttributeError:
|
||||
# not using sites
|
||||
return reverse('landing', args=[self.nonce])
|
||||
|
||||
|
||||
def config_landing(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Create your views here.
|
||||
from django.shortcuts import render, render_to_response
|
||||
from django.shortcuts import render
|
||||
from django.conf import settings
|
||||
from django.template import RequestContext
|
||||
from django import http
|
||||
from django.utils import translation
|
||||
from .models import Page
|
||||
|
@ -10,12 +9,11 @@ def page(request, page_to_render):
|
|||
try:
|
||||
p = Page.objects.get(slug=page_to_render, public=True)
|
||||
except Page.DoesNotExist:
|
||||
return render(request, "pages/{}.html".format(page_to_render),
|
||||
{ "request" : request,},
|
||||
return render(request, "pages/{}.html".format(page_to_render),
|
||||
{ "request" : request,},
|
||||
)
|
||||
|
||||
return render(request, "page.html",
|
||||
{ "request" : request, "page" : p, },
|
||||
return render(request, "page.html",
|
||||
{ "request" : request, "page" : p, },
|
||||
)
|
||||
|
||||
def langpage(request, lang, page_to_trans):
|
||||
|
@ -33,7 +31,7 @@ def set_language(request):
|
|||
lang_code = request.GET.get('language', None)
|
||||
if lang_code and translation.check_for_language(lang_code):
|
||||
if hasattr(request, 'session'):
|
||||
request.session['django_language'] = lang_code
|
||||
request.session[translation.LANGUAGE_SESSION_KEY] = lang_code
|
||||
else:
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code)
|
||||
return response
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
import re
|
||||
from . import Processors, AnswerException
|
||||
from .dependency_checker import dep_check
|
||||
from .models import Answer, Question, RunInfo
|
||||
from .parsers import BooleanParser, parse_checks
|
||||
from .parsers import BoolNot, BoolAnd, BoolOr, Checker
|
||||
from .request_cache import request_cache
|
||||
|
||||
def get_runinfo(random):
|
||||
"Return the RunInfo entry with the provided random key"
|
||||
res = RunInfo.objects.filter(random=random.lower())
|
||||
return res and res[0] or None
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def delete_answer(question, subject, run):
|
||||
"Delete the specified question/subject/run combination from the Answer table"
|
||||
Answer.objects.filter(subject=subject, run=run, question=question).delete()
|
||||
|
||||
|
||||
def add_answer(runinfo, question, answer_dict):
|
||||
"""
|
||||
Add an Answer to a Question for RunInfo, given the relevant form input
|
||||
|
||||
answer_dict contains the POST'd elements for this question, minus the
|
||||
question_{number} prefix. The question_{number} form value is accessible
|
||||
with the ANSWER key.
|
||||
"""
|
||||
answer = Answer()
|
||||
answer.question = question
|
||||
answer.subject = runinfo.subject
|
||||
answer.run = runinfo.run
|
||||
|
||||
type = question.get_type()
|
||||
|
||||
if "ANSWER" not in answer_dict:
|
||||
answer_dict['ANSWER'] = None
|
||||
|
||||
if type in Processors:
|
||||
answer.answer = Processors[type](question, answer_dict) or ''
|
||||
else:
|
||||
raise AnswerException("No Processor defined for question type %s" % type)
|
||||
|
||||
# first, delete all existing answers to this question for this particular user+run
|
||||
delete_answer(question, runinfo.subject, runinfo.run)
|
||||
|
||||
# then save the new answer to the database
|
||||
answer.save(runinfo)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def has_tag(tag, runinfo):
|
||||
""" Returns true if the given runinfo contains the given tag. """
|
||||
return tag in (t.strip() for t in runinfo.tags.split(','))
|
||||
|
||||
|
||||
def check_parser(runinfo, exclude=[]):
|
||||
depparser = BooleanParser(dep_check, runinfo, {})
|
||||
tagparser = BooleanParser(has_tag, runinfo)
|
||||
|
||||
fnmap = {
|
||||
"maleonly": lambda v: runinfo.subject.gender == 'male',
|
||||
"femaleonly": lambda v: runinfo.subject.gender == 'female',
|
||||
"shownif": lambda v: v and depparser.parse(v),
|
||||
"iftag": lambda v: v and tagparser.parse(v)
|
||||
}
|
||||
|
||||
for ex in exclude:
|
||||
del fnmap[ex]
|
||||
|
||||
@request_cache()
|
||||
def satisfies_checks(checks):
|
||||
if not checks:
|
||||
return True
|
||||
|
||||
checks = parse_checks(checks)
|
||||
|
||||
for check, value in checks.items():
|
||||
if check in fnmap:
|
||||
value = value and value.strip()
|
||||
if not fnmap[check](value):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return satisfies_checks
|
||||
|
||||
|
||||
@request_cache()
|
||||
def skipped_questions(runinfo):
|
||||
if not runinfo.skipped:
|
||||
return []
|
||||
|
||||
return [s.strip() for s in runinfo.skipped.split(',')]
|
||||
|
||||
|
||||
@request_cache()
|
||||
def question_satisfies_checks(question, runinfo, checkfn=None):
|
||||
if question.number in skipped_questions(runinfo):
|
||||
return False
|
||||
|
||||
checkfn = checkfn or check_parser(runinfo)
|
||||
return checkfn(question.checks)
|
||||
|
||||
@request_cache(keyfn=lambda *args: args[0].id)
|
||||
def questionset_satisfies_checks(questionset, runinfo, checks=None):
|
||||
"""Return True if the runinfo passes the checks specified in the QuestionSet
|
||||
|
||||
Checks is an optional dictionary with the keys being questionset.pk and the
|
||||
values being the checks of the contained questions.
|
||||
|
||||
This, in conjunction with fetch_checks allows for fewer
|
||||
db roundtrips and greater performance.
|
||||
|
||||
Sadly, checks cannot be hashed and therefore the request cache is useless
|
||||
here. Thankfully the benefits outweigh the costs in my tests.
|
||||
"""
|
||||
|
||||
passes = check_parser(runinfo)
|
||||
|
||||
if not passes(questionset.checks):
|
||||
return False
|
||||
|
||||
if not checks:
|
||||
checks = dict()
|
||||
checks[questionset.id] = []
|
||||
|
||||
for q in questionset.questions():
|
||||
checks[questionset.id].append((q.checks, q.number))
|
||||
|
||||
# questionsets that pass the checks but have no questions are shown
|
||||
# (comments, last page, etc.)
|
||||
if not checks[questionset.id]:
|
||||
return True
|
||||
|
||||
# if there are questions at least one needs to be visible
|
||||
for check, number in checks[questionset.id]:
|
||||
if number in skipped_questions(runinfo):
|
||||
continue
|
||||
|
||||
if passes(check):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_progress(runinfo):
|
||||
position, total = 0, 0
|
||||
|
||||
current = runinfo.questionset
|
||||
sets = current.questionnaire.questionsets()
|
||||
|
||||
checks = fetch_checks(sets)
|
||||
|
||||
# fetch the all question checks at once. This greatly improves the
|
||||
# performance of the questionset_satisfies_checks function as it
|
||||
# can avoid a roundtrip to the database for each question
|
||||
|
||||
for qs in sets:
|
||||
if questionset_satisfies_checks(qs, runinfo, checks):
|
||||
total += 1
|
||||
|
||||
if qs.id == current.id:
|
||||
position = total
|
||||
|
||||
if not all((position, total)):
|
||||
progress = 1
|
||||
else:
|
||||
progress = float(position) / float(total) * 100.00
|
||||
|
||||
# progress is always at least one percent
|
||||
progress = progress >= 1.0 and progress or 1
|
||||
|
||||
return int(progress)
|
||||
|
||||
|
||||
def fetch_checks(questionsets):
|
||||
ids = [qs.pk for qs in questionsets]
|
||||
|
||||
query = Question.objects.filter(questionset__pk__in=ids)
|
||||
query = query.values('questionset_id', 'checks', 'number')
|
||||
|
||||
checks = dict()
|
||||
for qsid in ids:
|
||||
checks[qsid] = list()
|
||||
|
||||
for result in (r for r in query):
|
||||
checks[result['questionset_id']].append(
|
||||
(result['checks'], result['number'])
|
||||
)
|
||||
|
||||
return checks
|
||||
|
||||
|
||||
def recursivly_build_partially_evaluated_js_exp_for_shownif_check(treenode, runinfo, question):
|
||||
if isinstance(treenode, BoolNot):
|
||||
return "!( %s )" % recursivly_build_partially_evaluated_js_exp_for_shownif_check(treenode.arg, runinfo, question)
|
||||
elif isinstance(treenode, BoolAnd):
|
||||
return " && ".join(
|
||||
"( %s )" % recursivly_build_partially_evaluated_js_exp_for_shownif_check(arg, runinfo, question)
|
||||
for arg in treenode.args )
|
||||
elif isinstance(treenode, BoolOr):
|
||||
return " || ".join(
|
||||
"( %s )" % recursivly_build_partially_evaluated_js_exp_for_shownif_check(arg, runinfo, question)
|
||||
for arg in treenode.args )
|
||||
else:
|
||||
assert( isinstance(treenode, Checker) )
|
||||
# ouch, we're assuming the correct syntax is always found
|
||||
question_looksee_number = treenode.expr.split(",", 1)[0]
|
||||
if Question.objects.get(number=question_looksee_number).questionset \
|
||||
!= question.questionset:
|
||||
return "true" if dep_check(treenode.expr, runinfo, {}) else "false"
|
||||
else:
|
||||
return str(treenode)
|
||||
|
||||
|
||||
def make_partially_evaluated_js_exp_for_shownif_check(checkexpression, runinfo, question):
|
||||
depparser = BooleanParser(dep_check, runinfo, {})
|
||||
parsed_bool_expression_results = depparser.boolExpr.parseString(checkexpression)[0]
|
||||
return recursivly_build_partially_evaluated_js_exp_for_shownif_check(parsed_bool_expression_results, runinfo, question)
|
||||
|
||||
|
||||
def substitute_answer(qvalues, obj):
|
||||
"""Objects with a 'text/text_xx' attribute can contain magic strings
|
||||
referring to the answers of other questions. This function takes
|
||||
any such object, goes through the stored answers (qvalues) and replaces
|
||||
the magic string with the actual value. If this isn't possible the
|
||||
magic string is removed from the text.
|
||||
|
||||
Only answers with 'store' in their check will work with this.
|
||||
|
||||
"""
|
||||
|
||||
if qvalues and obj.text:
|
||||
magic = 'subst_with_ans_'
|
||||
regex = r'subst_with_ans_(\S+)'
|
||||
|
||||
replacements = re.findall(regex, obj.text)
|
||||
text_attributes = [a for a in dir(obj) if a.startswith('text_')]
|
||||
|
||||
for answerid in replacements:
|
||||
|
||||
target = magic + answerid
|
||||
replacement = qvalues.get(answerid.lower(), '')
|
||||
|
||||
for attr in text_attributes:
|
||||
oldtext = getattr(obj, attr)
|
||||
newtext = oldtext.replace(target, replacement)
|
||||
|
||||
setattr(obj, attr, newtext)
|
||||
|
||||
|
|
@ -35,9 +35,8 @@
|
|||
{% endfor %}
|
||||
{% endblock language %}
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Sample Django Questionnaire</h1>
|
||||
<h1>{% block h1 %}Questionnaire{% endblock h1 %}</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
{% extends 'base-questionnaire.html' %}
|
||||
|
||||
{% block title %}Item-linked Questionnaire Management {% endblock %}
|
||||
|
||||
{% block topsection %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Questionnaire Tools </h1>
|
||||
|
||||
<h2>Configure a new landing page</h2>
|
||||
<form action="#" method="POST" >
|
||||
{% csrf_token %}
|
||||
<p>Label for new questionnaire: {{ form.label}}{{ form.label.errors }}</p>
|
||||
<p>Questionnaire to use:{{ form.questionnaire }}{{ form.questionnaire.errors }}</p>
|
||||
<input type="submit" value="submit" />
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,11 +1,11 @@
|
|||
{% extends "base-questionnaire.html" %}
|
||||
|
||||
{% load i18n %}
|
||||
{% block title %}
|
||||
{{ block.super }} - {{ page.title }}
|
||||
{% endblock %}
|
||||
|
||||
{% block questionnaire %}
|
||||
{{ page.body }}
|
||||
{{ page.body|safe }}
|
||||
{% if user.is_authenticated %}
|
||||
<a href="/admin/page/page/{{ page.slug }}/">(edit)</a>
|
||||
{% endif %}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base-questionnaire.html" %}
|
||||
{% block questionnaire %}
|
||||
<h2>
|
||||
Thanks for completing the survey!
|
||||
Thanks for completing the questionnaire!
|
||||
</h2>
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<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>
|
||||
<p>But comeback again, our questionnaire for {{landing.label}} isn't ready yet.</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base-questionnaire.html" %}
|
||||
{% block questionnaire %}
|
||||
<h1>
|
||||
Survey Results Summary
|
||||
Questionnaire Results Summary
|
||||
</h1>
|
||||
{% for summary in summaries %}
|
||||
|
||||
|
|
|
@ -32,9 +32,11 @@
|
|||
<label for="{{ question.number }}extra"><span class="extra-block">{{ question.extra }}</span></label>
|
||||
</li>
|
||||
{% else %}
|
||||
{% if qdict.extras %}
|
||||
<li>
|
||||
<label for="{{ question.number }}extra">{% trans "Other..." %}</label>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
{% if qdict.extras %}
|
||||
{% for key, value in qdict.extras %}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% block questionnaire %}
|
||||
|
||||
<h2>
|
||||
Thanks for completing the survey!
|
||||
Thanks for completing the questionnaire!
|
||||
</h2>
|
||||
<div class="question-text">
|
||||
{{ landing_object.claim.all.0.rights_holder }}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% load landings %}
|
||||
|
||||
{% block title %}
|
||||
Survey: {{ questionset.heading }}
|
||||
Questionnaire: {{ questionset.heading }}
|
||||
{% endblock %}
|
||||
|
||||
{% block headextra %}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
{% extends 'base-questionnaire.html' %}
|
||||
|
||||
{% block title %}item-linked Questionnaires {% endblock %}
|
||||
|
||||
{% block topsection %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Questionnaire Tools </h1>
|
||||
<h2 id="open_campaigns">Items You Can Use for Questionnaires</h2>
|
||||
<dl>
|
||||
{% for item in items %}
|
||||
|
||||
<dt>{{ item }}</dt>
|
||||
<dd>
|
||||
<dl>
|
||||
{% for landing in item.landings.all %}
|
||||
<dt>Configured questionnaire: {{ landing }} </dt>
|
||||
<dd>Link: <a href="{{ landing.url }}">{{ landing.url }}</a><br />
|
||||
Completed {{ landing.runinfohistory_set.all.count }} times</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
<a href="{% url 'new_questionnaire' item.id %}">Set up a new questionnaire</a> for this item.<br />
|
||||
{% for questionnaire in questionnaires %}
|
||||
<a href="{% url 'questionnaire_answers' questionnaire.id item.id %}">Export</a> or <a href="{% url 'answer_summary' questionnaire.id item.id %}">Summarize</a> answers to {{ questionnaire }} for this item.<br />
|
||||
|
||||
{% endfor %}
|
||||
<hr />
|
||||
</dd>
|
||||
{% empty %}
|
||||
<p>No items available</p>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
<p>
|
||||
|
||||
{% for questionnaire in questionnaires %}
|
||||
<a href="{% url 'questionnaire_answers' questionnaire.id '' %}">Export all my answers to {{ questionnaire }}</a>.<br />
|
||||
<a href="{% url 'answer_summary' questionnaire.id '' %}">Summarize my responses to {{ questionnaire }}</a>.<br />
|
||||
|
||||
{% if request.user.is_staff %}<a href="{% url 'questionnaire_answers' questionnaire.id '0' %}">Export ALL answers to {{ questionnaire }}</a>.<br />
|
||||
<a href="{% url 'answer_summary' questionnaire.id '0' %}">Summarize ALL responses to {{ questionnaire }}</a>.<br />
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Add "?next=https://example.com/any_url" to the end of a questionnaire url to add a redirect on completion of the questionnaire.
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -5,7 +5,8 @@ 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
|
||||
landing = context['runinfo'].landing
|
||||
context['landing_object'] = landing.content_object if landing else ''
|
||||
if text:
|
||||
template = Template(text)
|
||||
return template.render(context)
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
# vim: set fileencoding=utf-8
|
||||
|
||||
from django.conf.urls import *
|
||||
from django.conf import settings
|
||||
from .views import *
|
||||
from .page.views import page, langpage
|
||||
|
||||
|
@ -18,11 +19,23 @@ urlpatterns = [
|
|||
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'^landing/(?P<nonce>\w+)/$', QuestionnaireView.as_view(), name="landing"),
|
||||
]
|
||||
|
||||
# item questionnaires
|
||||
try:
|
||||
if settings.QUESTIONNAIRE_ITEM_MODEL and settings.QUESTIONNAIRE_SHOW_ITEM_RESULTS:
|
||||
urlpatterns += [
|
||||
url(r"^items/$", questionnaires, name="questionnaires"),
|
||||
url(r"^new_questionnaire/(?P<item_id>\d*)/?$", new_questionnaire, name="new_questionnaire"),
|
||||
url(r"^items/answers_(?P<qid>\d+)_(?P<item_id>\d*).csv$", export_item_csv, name="questionnaire_answers"),
|
||||
url(r"^items/summary_(?P<qid>\d+)_(?P<item_id>\d*).csv$", export_item_summary, name="answer_summary"),
|
||||
]
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
urlpatterns += [url(r'^(?P<runcode>[^/]+)/(?P<qs>[-]{0,1}\d+)/$', questionnaire, name='questionset')]
|
||||
|
||||
if not use_session:
|
||||
urlpatterns += [
|
||||
url(r'^(?P<runcode>[^/]+)/$',
|
||||
|
@ -39,3 +52,4 @@ else:
|
|||
redirect_to_prev_questionnaire,
|
||||
name='redirect_to_prev_questionnaire')
|
||||
]
|
||||
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
#!/usr/bin/python
|
||||
# vim: set fileencoding=utf-8
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from compat import commit_on_success, commit, rollback
|
||||
from hashlib import md5
|
||||
from uuid import uuid4
|
||||
|
||||
from django.apps import apps
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.template import RequestContext
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.cache import cache
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.shortcuts import render, render_to_response, get_object_or_404
|
||||
from django.shortcuts import render, 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
|
||||
|
@ -25,14 +23,18 @@ from django.utils.translation import ugettext_lazy as _
|
|||
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 .emails import _send_email
|
||||
from .models import (
|
||||
Answer, Landing, Question, Questionnaire, QuestionSet, Run, RunInfo, RunInfoHistory, Subject,
|
||||
)
|
||||
from .forms import NewLandingForm
|
||||
from .parsers import BooleanParser
|
||||
from .utils import numal_sort, split_numal, UnicodeWriter
|
||||
from .request_cache import request_cache
|
||||
from .run import (
|
||||
add_answer, delete_answer, get_runinfo, get_question,
|
||||
question_satisfies_checks, questionset_satisfies_checks,
|
||||
get_progress, make_partially_evaluated_js_exp_for_shownif_check, substitute_answer
|
||||
)
|
||||
from .dependency_checker import dep_check
|
||||
|
||||
|
||||
|
@ -46,183 +48,18 @@ try:
|
|||
except AttributeError:
|
||||
debug_questionnaire = False
|
||||
|
||||
try:
|
||||
(app_label, model_name) = settings.QUESTIONNAIRE_ITEM_MODEL.split('.', 1)
|
||||
item_model = apps.get_model(app_label=app_label, model_name=model_name)
|
||||
except AttributeError:
|
||||
item_model = None
|
||||
|
||||
|
||||
def r2r(tpl, request, **contextdict):
|
||||
"Shortcut to use RequestContext instead of Context in templates"
|
||||
contextdict['request'] = request
|
||||
return render(request, tpl, contextdict)
|
||||
|
||||
|
||||
def get_runinfo(random):
|
||||
"Return the RunInfo entry with the provided random key"
|
||||
res = RunInfo.objects.filter(random=random.lower())
|
||||
return res and res[0] or None
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def delete_answer(question, subject, run):
|
||||
"Delete the specified question/subject/run combination from the Answer table"
|
||||
Answer.objects.filter(subject=subject, run=run, question=question).delete()
|
||||
|
||||
|
||||
def add_answer(runinfo, question, answer_dict):
|
||||
"""
|
||||
Add an Answer to a Question for RunInfo, given the relevant form input
|
||||
|
||||
answer_dict contains the POST'd elements for this question, minus the
|
||||
question_{number} prefix. The question_{number} form value is accessible
|
||||
with the ANSWER key.
|
||||
"""
|
||||
answer = Answer()
|
||||
answer.question = question
|
||||
answer.subject = runinfo.subject
|
||||
answer.run = runinfo.run
|
||||
|
||||
type = question.get_type()
|
||||
|
||||
if "ANSWER" not in answer_dict:
|
||||
answer_dict['ANSWER'] = None
|
||||
|
||||
if type in Processors:
|
||||
answer.answer = Processors[type](question, answer_dict) or ''
|
||||
else:
|
||||
raise AnswerException("No Processor defined for question type %s" % type)
|
||||
|
||||
# first, delete all existing answers to this question for this particular user+run
|
||||
delete_answer(question, runinfo.subject, runinfo.run)
|
||||
|
||||
# then save the new answer to the database
|
||||
answer.save(runinfo)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_parser(runinfo, exclude=[]):
|
||||
depparser = BooleanParser(dep_check, runinfo, {})
|
||||
tagparser = BooleanParser(has_tag, runinfo)
|
||||
|
||||
fnmap = {
|
||||
"maleonly": lambda v: runinfo.subject.gender == 'male',
|
||||
"femaleonly": lambda v: runinfo.subject.gender == 'female',
|
||||
"shownif": lambda v: v and depparser.parse(v),
|
||||
"iftag": lambda v: v and tagparser.parse(v)
|
||||
}
|
||||
|
||||
for ex in exclude:
|
||||
del fnmap[ex]
|
||||
|
||||
@request_cache()
|
||||
def satisfies_checks(checks):
|
||||
if not checks:
|
||||
return True
|
||||
|
||||
checks = parse_checks(checks)
|
||||
|
||||
for check, value in checks.items():
|
||||
if check in fnmap:
|
||||
value = value and value.strip()
|
||||
if not fnmap[check](value):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return satisfies_checks
|
||||
|
||||
|
||||
@request_cache()
|
||||
def skipped_questions(runinfo):
|
||||
if not runinfo.skipped:
|
||||
return []
|
||||
|
||||
return [s.strip() for s in runinfo.skipped.split(',')]
|
||||
|
||||
|
||||
@request_cache()
|
||||
def question_satisfies_checks(question, runinfo, checkfn=None):
|
||||
if question.number in skipped_questions(runinfo):
|
||||
return False
|
||||
|
||||
checkfn = checkfn or check_parser(runinfo)
|
||||
return checkfn(question.checks)
|
||||
|
||||
|
||||
@request_cache(keyfn=lambda *args: args[0].id)
|
||||
def questionset_satisfies_checks(questionset, runinfo, checks=None):
|
||||
"""Return True if the runinfo passes the checks specified in the QuestionSet
|
||||
|
||||
Checks is an optional dictionary with the keys being questionset.pk and the
|
||||
values being the checks of the contained questions.
|
||||
|
||||
This, in conjunction with fetch_checks allows for fewer
|
||||
db roundtrips and greater performance.
|
||||
|
||||
Sadly, checks cannot be hashed and therefore the request cache is useless
|
||||
here. Thankfully the benefits outweigh the costs in my tests.
|
||||
"""
|
||||
|
||||
passes = check_parser(runinfo)
|
||||
|
||||
if not passes(questionset.checks):
|
||||
return False
|
||||
|
||||
if not checks:
|
||||
checks = dict()
|
||||
checks[questionset.id] = []
|
||||
|
||||
for q in questionset.questions():
|
||||
checks[questionset.id].append((q.checks, q.number))
|
||||
|
||||
# questionsets that pass the checks but have no questions are shown
|
||||
# (comments, last page, etc.)
|
||||
if not checks[questionset.id]:
|
||||
return True
|
||||
|
||||
# if there are questions at least one needs to be visible
|
||||
for check, number in checks[questionset.id]:
|
||||
if number in skipped_questions(runinfo):
|
||||
continue
|
||||
|
||||
if passes(check):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_progress(runinfo):
|
||||
position, total = 0, 0
|
||||
|
||||
current = runinfo.questionset
|
||||
sets = current.questionnaire.questionsets()
|
||||
|
||||
checks = fetch_checks(sets)
|
||||
|
||||
# fetch the all question checks at once. This greatly improves the
|
||||
# performance of the questionset_satisfies_checks function as it
|
||||
# can avoid a roundtrip to the database for each question
|
||||
|
||||
for qs in sets:
|
||||
if questionset_satisfies_checks(qs, runinfo, checks):
|
||||
total += 1
|
||||
|
||||
if qs.id == current.id:
|
||||
position = total
|
||||
|
||||
if not all((position, total)):
|
||||
progress = 1
|
||||
else:
|
||||
progress = float(position) / float(total) * 100.00
|
||||
|
||||
# progress is always at least one percent
|
||||
progress = progress >= 1.0 and progress or 1
|
||||
|
||||
return int(progress)
|
||||
|
||||
|
||||
def get_async_progress(request, *args, **kwargs):
|
||||
""" Returns the progress as json for use with ajax """
|
||||
|
||||
|
@ -243,24 +80,6 @@ def get_async_progress(request, *args, **kwargs):
|
|||
return response
|
||||
|
||||
|
||||
def fetch_checks(questionsets):
|
||||
ids = [qs.pk for qs in questionsets]
|
||||
|
||||
query = Question.objects.filter(questionset__pk__in=ids)
|
||||
query = query.values('questionset_id', 'checks', 'number')
|
||||
|
||||
checks = dict()
|
||||
for qsid in ids:
|
||||
checks[qsid] = list()
|
||||
|
||||
for result in (r for r in query):
|
||||
checks[result['questionset_id']].append(
|
||||
(result['checks'], result['number'])
|
||||
)
|
||||
|
||||
return checks
|
||||
|
||||
|
||||
def redirect_to_qs(runinfo, request=None):
|
||||
"Redirect to the correct and current questionset URL for this RunInfo"
|
||||
|
||||
|
@ -304,7 +123,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.
|
||||
both when using sessions or not.
|
||||
"""
|
||||
if use_session:
|
||||
runcode = request.session.get('runcode', None)
|
||||
|
@ -347,6 +166,8 @@ 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)
|
||||
if session_runcode is not None:
|
||||
|
@ -367,7 +188,7 @@ def questionnaire(request, runcode=None, qs=None):
|
|||
else:
|
||||
request.session['runcode'] = runcode
|
||||
args = []
|
||||
|
||||
|
||||
return HttpResponseRedirect(reverse("questionnaire", args=args))
|
||||
|
||||
|
||||
|
@ -390,7 +211,7 @@ def questionnaire(request, runcode=None, qs=None):
|
|||
# Only change the language to the subjects choice for the initial
|
||||
# questionnaire page (may be a direct link from an email)
|
||||
if hasattr(request, 'session'):
|
||||
request.session['django_language'] = runinfo.subject.language
|
||||
request.session[translation.LANGUAGE_SESSION_KEY] = runinfo.subject.language
|
||||
translation.activate(runinfo.subject.language)
|
||||
|
||||
if 'lang' in request.GET:
|
||||
|
@ -422,7 +243,7 @@ def questionnaire(request, runcode=None, qs=None):
|
|||
# -------------------------------------
|
||||
|
||||
# if the submitted page is different to what runinfo says, update runinfo
|
||||
# XXX - do we really want this?
|
||||
|
||||
qs = request.POST.get('questionset_id', qs)
|
||||
try:
|
||||
qsobj = QuestionSet.objects.filter(pk=qs)[0]
|
||||
|
@ -532,7 +353,7 @@ def finish_questionnaire(request, runinfo, questionnaire):
|
|||
|
||||
questionnaire_done.send(sender=None, runinfo=runinfo,
|
||||
questionnaire=questionnaire)
|
||||
lang=translation.get_language()
|
||||
lang = translation.get_language()
|
||||
redirect_url = questionnaire.redirect_url
|
||||
for x, y in (('$LANG', lang),
|
||||
('$SUBJECTID', runinfo.subject.id),
|
||||
|
@ -551,32 +372,6 @@ def finish_questionnaire(request, runinfo, questionnaire):
|
|||
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):
|
||||
if isinstance(treenode, BoolNot):
|
||||
return "!( %s )" % recursivly_build_partially_evaluated_javascript_expression_for_shownif_check(treenode.arg, runinfo, question)
|
||||
elif isinstance(treenode, BoolAnd):
|
||||
return " && ".join(
|
||||
"( %s )" % recursivly_build_partially_evaluated_javascript_expression_for_shownif_check(arg, runinfo, question)
|
||||
for arg in treenode.args )
|
||||
elif isinstance(treenode, BoolOr):
|
||||
return " || ".join(
|
||||
"( %s )" % recursivly_build_partially_evaluated_javascript_expression_for_shownif_check(arg, runinfo, question)
|
||||
for arg in treenode.args )
|
||||
else:
|
||||
assert( isinstance(treenode, Checker) )
|
||||
# ouch, we're assuming the correct syntax is always found
|
||||
question_looksee_number = treenode.expr.split(",", 1)[0]
|
||||
if Question.objects.get(number=question_looksee_number).questionset \
|
||||
!= question.questionset:
|
||||
return "true" if dep_check(treenode.expr, runinfo, {}) else "false"
|
||||
else:
|
||||
return str(treenode)
|
||||
|
||||
def make_partially_evaluated_javascript_expression_for_shownif_check(checkexpression, runinfo, question):
|
||||
depparser = BooleanParser(dep_check, runinfo, {})
|
||||
parsed_bool_expression_results = depparser.boolExpr.parseString(checkexpression)[0]
|
||||
return recursivly_build_partially_evaluated_javascript_expression_for_shownif_check(parsed_bool_expression_results, runinfo, question)
|
||||
|
||||
def show_questionnaire(request, runinfo, errors={}):
|
||||
"""
|
||||
Return the QuestionSet template
|
||||
|
@ -635,27 +430,27 @@ def show_questionnaire(request, runinfo, errors={}):
|
|||
|
||||
# add javascript dependency checks
|
||||
cd = question.getcheckdict()
|
||||
|
||||
|
||||
# Note: dep_check() is showing up on pages where questions rely on previous pages' questions -
|
||||
# this causes disappearance of questions, since there are no qvalues for questions on previous
|
||||
# pages. BUT depon will be false if the question is a SAMEAS of another question with no off-page
|
||||
# checks. This will make no bad dep_check()s appear for these SAMEAS questions, circumventing the
|
||||
# problem. Eventually need to fix either getcheckdict() (to screen out questions on previous pages)
|
||||
# checks. This will make no bad dep_check()s appear for these SAMEAS questions, circumventing the
|
||||
# problem. Eventually need to fix either getcheckdict() (to screen out questions on previous pages)
|
||||
# or prevent JavaScript from hiding questions when check_dep() cannot find a key in qvalues.
|
||||
depon = cd.get('requiredif', None) or cd.get('dependent', None) or cd.get('shownif', None)
|
||||
if depon:
|
||||
willberequiredif = bool(cd.get("requiredif", None) )
|
||||
willbedependent = bool(cd.get("dependent", None) )
|
||||
willbe_shownif = (not willberequiredif) and (not willbedependent) and bool(cd.get("shownif", None))
|
||||
|
||||
|
||||
# jamie and mark funkyness to be only done if depon is shownif, some similar thought is due to requiredif
|
||||
# for shownon, we have to deal with the fact that only the answers from this page are available to the JS
|
||||
# so we do a partial parse to form the checks="" attribute
|
||||
# so we do a partial parse to form the checks="" attribute
|
||||
if willbe_shownif:
|
||||
qdict['checkstring'] = ' checks="%s"' % make_partially_evaluated_javascript_expression_for_shownif_check(
|
||||
qdict['checkstring'] = ' checks="%s"' % make_partially_evaluated_js_exp_for_shownif_check(
|
||||
depon, runinfo, question
|
||||
)
|
||||
|
||||
|
||||
else:
|
||||
# extra args to BooleanParser are not required for toString
|
||||
parser = BooleanParser(dep_check)
|
||||
|
@ -721,7 +516,7 @@ def show_questionnaire(request, runinfo, errors={}):
|
|||
prev_url = reverse('redirect_to_prev_questionnaire')
|
||||
else:
|
||||
prev_url = reverse('redirect_to_prev_questionnaire', args=[runinfo.random, runinfo.questionset.sortid])
|
||||
|
||||
|
||||
current_answers = []
|
||||
if debug_questionnaire:
|
||||
current_answers = Answer.objects.filter(subject=runinfo.subject, run=runinfo.run).order_by('id')
|
||||
|
@ -744,40 +539,10 @@ def show_questionnaire(request, runinfo, errors={}):
|
|||
)
|
||||
r['Cache-Control'] = 'no-cache'
|
||||
r['Expires'] = "Thu, 24 Jan 1980 00:00:00 GMT"
|
||||
r.set_cookie('questionset_id', str(questionset.id))
|
||||
r.set_cookie('questionset_id', str(questionset.id))
|
||||
return r
|
||||
|
||||
|
||||
def substitute_answer(qvalues, obj):
|
||||
"""Objects with a 'text/text_xx' attribute can contain magic strings
|
||||
referring to the answers of other questions. This function takes
|
||||
any such object, goes through the stored answers (qvalues) and replaces
|
||||
the magic string with the actual value. If this isn't possible the
|
||||
magic string is removed from the text.
|
||||
|
||||
Only answers with 'store' in their check will work with this.
|
||||
|
||||
"""
|
||||
|
||||
if qvalues and obj.text:
|
||||
magic = 'subst_with_ans_'
|
||||
regex = r'subst_with_ans_(\S+)'
|
||||
|
||||
replacements = re.findall(regex, obj.text)
|
||||
text_attributes = [a for a in dir(obj) if a.startswith('text_')]
|
||||
|
||||
for answerid in replacements:
|
||||
|
||||
target = magic + answerid
|
||||
replacement = qvalues.get(answerid.lower(), '')
|
||||
|
||||
for attr in text_attributes:
|
||||
oldtext = getattr(obj, attr)
|
||||
newtext = oldtext.replace(target, replacement)
|
||||
|
||||
setattr(obj, attr, newtext)
|
||||
|
||||
|
||||
def set_language(request, runinfo=None, next=None):
|
||||
"""
|
||||
Change the language, save it to runinfo if provided, and
|
||||
|
@ -796,7 +561,7 @@ def set_language(request, runinfo=None, next=None):
|
|||
lang_code = request.GET.get('lang', None)
|
||||
if lang_code and translation.check_for_language(lang_code):
|
||||
if hasattr(request, 'session'):
|
||||
request.session['django_language'] = lang_code
|
||||
request.session[translation.LANGUAGE_SESSION_KEY] = lang_code
|
||||
else:
|
||||
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code)
|
||||
if runinfo:
|
||||
|
@ -805,6 +570,114 @@ def set_language(request, runinfo=None, next=None):
|
|||
return response
|
||||
|
||||
|
||||
|
||||
@permission_required("questionnaire.management")
|
||||
def send_email(request, runinfo_id):
|
||||
if request.method != "POST":
|
||||
return HttpResponse("This page MUST be called as a POST request.")
|
||||
runinfo = get_object_or_404(RunInfo, pk=int(runinfo_id))
|
||||
successful = _send_email(runinfo)
|
||||
return r2r("emailsent.html", request, runinfo=runinfo, successful=successful)
|
||||
|
||||
|
||||
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.
|
||||
|
||||
It uses a Subject with the givenname of 'Anonymous' and the
|
||||
surname of 'User'. If this Subject does not exist, it will
|
||||
be created.
|
||||
|
||||
This can be used with a URL pattern like:
|
||||
(r'^take/(?P<questionnaire_id>[0-9]+)/$', 'questionnaire.views.generate_run'),
|
||||
"""
|
||||
qu = get_object_or_404(Questionnaire, id=questionnaire_id)
|
||||
qs = qu.questionsets()[0]
|
||||
|
||||
if subject_id is not None:
|
||||
su = get_object_or_404(Subject, pk=subject_id)
|
||||
else:
|
||||
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)
|
||||
r = Run.objects.create(runid=key)
|
||||
run = RunInfo.objects.create(subject=su, random=key, run=r, questionset=qs, landing=landing)
|
||||
if not use_session:
|
||||
kwargs = {'runcode': key}
|
||||
else:
|
||||
kwargs = {}
|
||||
request.session['runcode'] = key
|
||||
|
||||
questionnaire_start.send(sender=None, runinfo=run, questionnaire=qu)
|
||||
response = HttpResponseRedirect(reverse('questionnaire', kwargs=kwargs))
|
||||
response.set_cookie('next', context.get('next',''))
|
||||
return response
|
||||
|
||||
def generate_error(request):
|
||||
return 400/0
|
||||
|
||||
class QuestionnaireView(TemplateView):
|
||||
template_name = "pages/generic.html"
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super(QuestionnaireView, 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)
|
||||
|
||||
try:
|
||||
(app_label, model_name) = settings.QUESTIONNAIRE_ITEM_MODEL.split('.', 1)
|
||||
item_model = apps.get_model(app_label=app_label, model_name=model_name)
|
||||
except AttributeError:
|
||||
item_model = None
|
||||
|
||||
|
||||
@login_required
|
||||
def new_questionnaire(request, item_id):
|
||||
if item_id:
|
||||
item = get_object_or_404(item_model, id=item_id)
|
||||
form = NewLandingForm()
|
||||
else:
|
||||
item = None
|
||||
form = NewLandingForm()
|
||||
if request.method == 'POST':
|
||||
form = NewLandingForm(data=request.POST)
|
||||
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})
|
||||
|
||||
|
||||
|
||||
def questionnaires(request):
|
||||
print "here"
|
||||
if not request.user.is_authenticated() :
|
||||
return render(request, "questionnaires.html")
|
||||
items = item_model.objects.all()
|
||||
questionnaires = Questionnaire.objects.all()
|
||||
return render(request, "questionnaires.html", {"items":items, "questionnaires":questionnaires})
|
||||
|
||||
|
||||
def _table_headers(questions):
|
||||
"""
|
||||
Return the header labels for a set of questions as a list of strings.
|
||||
|
@ -833,19 +706,21 @@ def _table_headers(questions):
|
|||
columns.append(qnum)
|
||||
return columns
|
||||
|
||||
|
||||
default_extra_headings = [u'subject', u'run id']
|
||||
|
||||
|
||||
def default_extra_entries(subject, run):
|
||||
return ["%s/%s" % (subject.id, subject.ip_address), run.id]
|
||||
|
||||
|
||||
@login_required
|
||||
def export_csv(request, qid,
|
||||
def export_csv(request, qid,
|
||||
extra_headings=default_extra_headings,
|
||||
extra_entries=default_extra_entries,
|
||||
answer_filter=None,
|
||||
filecode=0,
|
||||
):
|
||||
):
|
||||
"""
|
||||
For a given questionnaire id, generate a CSV containing all the
|
||||
answers for all subjects.
|
||||
|
@ -857,7 +732,7 @@ 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()
|
||||
|
||||
questionnaire = get_object_or_404(Questionnaire, pk=int(qid))
|
||||
|
@ -877,6 +752,43 @@ def export_csv(request, qid,
|
|||
return response
|
||||
|
||||
|
||||
def item_answer_filter(item_id):
|
||||
def item_filter(answers):
|
||||
if item_model:
|
||||
items = item_model.objects.filter(id=item_id)
|
||||
return answers.filter(run__run_info_histories__landing__items__in=items)
|
||||
else:
|
||||
return answers.none()
|
||||
return item_filter
|
||||
|
||||
|
||||
# wrapper for export_csv to customize the report table
|
||||
@login_required
|
||||
def export_item_csv(request, qid, item_id):
|
||||
def extra_entries(subject, run):
|
||||
landing = completed = None
|
||||
try:
|
||||
landing = run.run_info_histories.all()[0].landing
|
||||
completed = run.run_info_histories.all()[0].completed
|
||||
except IndexError:
|
||||
try:
|
||||
landing = run.run_infos.all()[0].landing
|
||||
completed = run.run_infos.all()[0].created
|
||||
except IndexError:
|
||||
label = wid = "error"
|
||||
if landing:
|
||||
label = landing.label
|
||||
wid = landing.object_id
|
||||
return [wid, subject.ip_address, run.id, completed, label]
|
||||
|
||||
extra_headings = [u'item id', u'subject ip address', u'run id', u'date completed', u'landing label']
|
||||
return export_csv(request, qid,
|
||||
extra_entries=extra_entries,
|
||||
extra_headings=extra_headings,
|
||||
answer_filter=item_answer_filter(item_id),
|
||||
filecode=item_id)
|
||||
|
||||
|
||||
def answer_export(questionnaire, answers=None, answer_filter=None):
|
||||
"""
|
||||
questionnaire -- questionnaire model for export
|
||||
|
@ -957,25 +869,6 @@ def answer_export(questionnaire, answers=None, answer_filter=None):
|
|||
out.append((subject, run, row))
|
||||
return headings, out
|
||||
|
||||
@login_required
|
||||
def export_summary(request, qid,
|
||||
answer_filter=None,
|
||||
):
|
||||
"""
|
||||
For a given questionnaire id, generate a CSV containing a summary of
|
||||
answers for all subjects.
|
||||
qid -- questionnaire_id
|
||||
answer_filter -- custom filter for the answers. If this is present, the filter must manage access.
|
||||
"""
|
||||
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")
|
||||
|
||||
|
||||
questionnaire = get_object_or_404(Questionnaire, pk=int(qid))
|
||||
summaries = answer_summary(questionnaire, answer_filter=answer_filter)
|
||||
|
||||
return render(request, "pages/summaries.html", {'summaries':summaries})
|
||||
|
||||
|
||||
def answer_summary(questionnaire, answers=None, answer_filter=None):
|
||||
"""
|
||||
|
@ -1030,78 +923,31 @@ def answer_summary(questionnaire, answers=None, answer_filter=None):
|
|||
return summary
|
||||
|
||||
|
||||
def has_tag(tag, runinfo):
|
||||
""" Returns true if the given runinfo contains the given tag. """
|
||||
return tag in (t.strip() for t in runinfo.tags.split(','))
|
||||
|
||||
|
||||
@permission_required("questionnaire.management")
|
||||
def send_email(request, runinfo_id):
|
||||
if request.method != "POST":
|
||||
return HttpResponse("This page MUST be called as a POST request.")
|
||||
runinfo = get_object_or_404(RunInfo, pk=int(runinfo_id))
|
||||
successful = _send_email(runinfo)
|
||||
return r2r("emailsent.html", request, runinfo=runinfo, successful=successful)
|
||||
|
||||
|
||||
def generate_run(request, questionnaire_id, subject_id=None, context={}):
|
||||
@login_required
|
||||
def export_summary(request, qid, answer_filter=None):
|
||||
"""
|
||||
A view that can generate a RunID instance anonymously,
|
||||
and then redirect to the questionnaire itself.
|
||||
|
||||
It uses a Subject with the givenname of 'Anonymous' and the
|
||||
surname of 'User'. If this Subject does not exist, it will
|
||||
be created.
|
||||
|
||||
This can be used with a URL pattern like:
|
||||
(r'^take/(?P<questionnaire_id>[0-9]+)/$', 'questionnaire.views.generate_run'),
|
||||
For a given questionnaire id, generate a CSV containing a summary of
|
||||
answers for all subjects.
|
||||
qid -- questionnaire_id
|
||||
answer_filter -- custom filter for the answers. If this is present, the filter must manage access.
|
||||
"""
|
||||
qu = get_object_or_404(Questionnaire, id=questionnaire_id)
|
||||
qs = qu.questionsets()[0]
|
||||
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")
|
||||
|
||||
if subject_id is not None:
|
||||
su = get_object_or_404(Subject, pk=subject_id)
|
||||
else:
|
||||
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)
|
||||
r = Run.objects.create(runid=key)
|
||||
run = RunInfo.objects.create(subject=su, random=key, run=r, questionset=qs, landing=landing)
|
||||
if not use_session:
|
||||
kwargs = {'runcode': key}
|
||||
else:
|
||||
kwargs = {}
|
||||
request.session['runcode'] = key
|
||||
questionnaire = get_object_or_404(Questionnaire, pk=int(qid))
|
||||
summaries = answer_summary(questionnaire, answer_filter=answer_filter)
|
||||
|
||||
questionnaire_start.send(sender=None, runinfo=run, questionnaire=qu)
|
||||
response = HttpResponseRedirect(reverse('questionnaire', kwargs=kwargs))
|
||||
response.set_cookie('next', context.get('next',''))
|
||||
return response
|
||||
return render(request, "pages/summaries.html", {'summaries':summaries})
|
||||
|
||||
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)
|
||||
@login_required
|
||||
def export_item_summary(request, qid, item_id):
|
||||
"""
|
||||
wrapper without filter
|
||||
"""
|
||||
return export_summary(
|
||||
request,
|
||||
qid,
|
||||
answer_filter=item_answer_filter(item_id),
|
||||
)
|
||||
|
|
4
setup.py
4
setup.py
|
@ -10,8 +10,8 @@ setup(
|
|||
version="4.0.0",
|
||||
description="A Django application for creating online questionnaires/surveys.",
|
||||
long_description=read("README.md"),
|
||||
author="Eldest Daughter, LLC.","Free Ebook Foundation"
|
||||
author_email="gcaprio@eldestdaughter.com", "eric@hellman.net"
|
||||
author="Eldest Daughter, LLC., Free Ebook Foundation",
|
||||
author_email="gcaprio@eldestdaughter.com, eric@hellman.net",
|
||||
license="BSD",
|
||||
url="https://github.com/EbookFoundation/fef-questionnaire",
|
||||
packages=find_packages(exclude=["example"]),
|
||||
|
|
Loading…
Reference in New Issue