add other item code to app

* refactor views
* add example fixtures
* consistent terminology for questionnaires
release40
eric 2017-06-19 22:20:46 -04:00
parent 195dcda657
commit 9454be4d91
24 changed files with 1020 additions and 587 deletions

View File

@ -27,11 +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" 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 "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
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:
@ -101,18 +101,25 @@ Add the questionnaire template directory as well as your own to TEMPLATES:
If you want to use multiple languages, add the i18n context processor to TEMPLATES
'context_processors': ['django.template.context_processors.i18n',]
And finally, add `transmeta`, `questionnaire` to your INSTALLED_APPS:
Now add `transmeta`, `questionnaire` to your INSTALLED_APPS:
'transmeta',
'questionnaire',
'questionnaire.page',
Next up we want to edit the `urls.py` file of your project to link the questionnaire views to your site's url configuration. See the example app to see how.
And finally, 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.
### Initialize the database
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:
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 ../..
@ -141,17 +148,20 @@ Open `mysite/mysite/settings.py` and add following lines, representing your lang
('de', 'Deutsch')
)
To run more than one language, set
python manage.py sync_transmeta_db
If you've added a language, you'll need to
python manage.py makemigrations
python manage.py migrate
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/)
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`):
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`):
python manage.py loaddata ./apps/fef-questionnaire/example/fixtures/initial_data.yaml
python manage.py loaddata ./apps/fef-questionnaire/example/fixtures/example.yaml
python manage.py loaddata ./apps/fef-questionnaire/example/fixtures/books.yaml
### Start the server!
@ -274,7 +284,7 @@ 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
-----------------------
@ -288,7 +298,7 @@ Version 4.0 does not support migration of 1.X data files.
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.
@ -321,6 +331,8 @@ Version 4.0 has not been tested for compatibility with previous versions.
* 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,127 +0,0 @@
- fields: {admin_access_only: false, html: '', name: example, 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: {answer: '["yes"]', question: 1, run: 9, subject: 11}
model: questionnaire.answer
pk: 1
- fields: {answer: '["weizen"]', question: 2, run: 9, subject: 11}
model: questionnaire.answer
pk: 2
- fields: {answer: '["becks", "heineken"]', question: 3, run: 9, subject: 11}
model: questionnaire.answer
pk: 3
- fields: {answer: '["django", "pylons", ["rails"]]', question: 4, run: 9, subject: 11}
model: questionnaire.answer
pk: 4
- fields: {answer: '["django"]', question: 5, run: 9, subject: 11}
model: questionnaire.answer
pk: 5
- fields: {body_de: "Wilkommen zu der Fragebogenbeispielseite!\r\n\r\nWenn Sie einen
einfachen Fragebogen sehen wollen, <a href=\"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=\"take/1\">go for it!</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

View File

@ -40,6 +40,7 @@ INSTALLED_APPS = (
'transmeta',
'questionnaire',
'questionnaire.page',
'mysite',
)
MIDDLEWARE_CLASSES = (
@ -132,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.
@ -141,4 +143,10 @@ QUESTIONNAIRE_PROGRESS = 'async'
# user goes through the steps of the question set.
QUESTIONNAIRE_USE_SESSION = False
# 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,},
)
print request.session[translation.LANGUAGE_SESSION_KEY]
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):

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

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

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

@ -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)
@ -369,7 +188,7 @@ def questionnaire(request, runcode=None, qs=None):
else:
request.session['runcode'] = runcode
args = []
return HttpResponseRedirect(reverse("questionnaire", args=args))
@ -424,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]
@ -516,7 +335,7 @@ def questionnaire(request, runcode=None, qs=None):
if next is None: # we are finished
return finish_questionnaire(request, runinfo, questionnaire)
commit()
return redirect_to_qs(runinfo, request)
@ -534,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),
@ -553,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
@ -637,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)
@ -723,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')
@ -746,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
@ -807,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.
@ -835,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.
@ -859,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))
@ -879,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
@ -959,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):
"""
@ -1032,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

@ -14,9 +14,6 @@ setup(
author_email="gcaprio@eldestdaughter.com, eric@hellman.net",
license="BSD",
url="https://github.com/EbookFoundation/fef-questionnaire",
dependency_links=[
'https://github.com/eshellman/django-transmeta/archive/v0.7.3-eshellman.tar.gz'
],
packages=find_packages(exclude=["example"]),
include_package_data=True,
classifiers=[
@ -32,7 +29,7 @@ setup(
zip_safe=False,
install_requires=[
'django',
'django-transmeta >= 0.7.4',
'django-transmeta',
'django-compat',
'pyyaml',
'pyparsing'