Merge pull request #1 from EbookFoundation/release40

Release 4.0
dj111py38 4.0.0
eshellman 2017-06-19 23:36:49 -04:00 committed by GitHub
commit a3dfce7e90
31 changed files with 1219 additions and 787 deletions

1
.gitignore vendored
View File

@ -7,6 +7,7 @@ local_settings.py
*egg-info
*.egg
build
dist
.project
.pydevproject
.settings

210
README.md
View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

10
example/models.py Normal file
View File

@ -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

View File

@ -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

View File

@ -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">&nbsp;</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">&nbsp;</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -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)),
)

View File

@ -18,7 +18,7 @@
{
"fields": {
"admin_access_only": false,
"html": "survey html here",
"html": "questionnaire html here",
"name": "MappingSurvey",
"parse_html": false,
"redirect_url": ""

12
questionnaire/forms.py Normal file
View File

@ -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,
)

View File

@ -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):

View File

@ -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')),

View File

@ -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:

View File

@ -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

258
questionnaire/run.py Normal file
View File

@ -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)

View File

@ -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">

View File

@ -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 %}

View File

@ -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 %}

View File

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

View File

@ -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 %}

View File

@ -1,7 +1,7 @@
{% extends "base-questionnaire.html" %}
{% block questionnaire %}
<h1>
Survey Results Summary
Questionnaire Results Summary
</h1>
{% for summary in summaries %}

View File

@ -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 %}

View File

@ -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 }}

View File

@ -5,7 +5,7 @@
{% load landings %}
{% block title %}
Survey: {{ questionset.heading }}
Questionnaire: {{ questionset.heading }}
{% endblock %}
{% block headextra %}

View File

@ -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 %}

View File

@ -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)

View File

@ -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')
]

View File

@ -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),
)

View File

@ -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"]),