add other item code to app
* refactor views * add example fixtures * consistent terminology for questionnairesrelease40
parent
195dcda657
commit
9454be4d91
36
README.md
36
README.md
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -0,0 +1,60 @@
|
|||
- fields: {title: "A Christmas Carol"}
|
||||
model: mysite.book
|
||||
pk: 4
|
||||
- fields: {title: "A Little Princess"}
|
||||
model: mysite.book
|
||||
pk: 5
|
||||
- fields: {title: "A Portrait of the Artist as a Young Man"}
|
||||
model: mysite.book
|
||||
pk: 6
|
||||
- fields: {title: "A Room with a View"}
|
||||
model: mysite.book
|
||||
pk: 7
|
||||
- fields: {title: "Agnes Grey"}
|
||||
model: mysite.book
|
||||
pk: 8
|
||||
- fields: {title: "Anne of Green Gables"}
|
||||
model: mysite.book
|
||||
pk: 9
|
||||
- fields: {title: "The Personal History, Adventures, Experience and Observation of David Copperfield the Younger of Blunderstone Rookery"}
|
||||
model: mysite.book
|
||||
pk: 10
|
||||
- fields: {title: "Far From the Madding Crowd"}
|
||||
model: mysite.book
|
||||
pk: 11
|
||||
- fields: {title: "Howards End"}
|
||||
model: mysite.book
|
||||
pk: 12
|
||||
- fields: {title: "Jacob\'s Room"}
|
||||
model: mysite.book
|
||||
pk: 13
|
||||
- fields: {title: "The Merry Adventures of Robin Hood"}
|
||||
model: mysite.book
|
||||
pk: 14
|
||||
- fields: {title: "Sense and Sensibility"}
|
||||
model: mysite.book
|
||||
pk: 15
|
||||
- fields: {title: "The Secret Garden"}
|
||||
model: mysite.book
|
||||
pk: 16
|
||||
- fields: {title: "The Adventures of Tom Sawyer"}
|
||||
model: mysite.book
|
||||
pk: 17
|
||||
- fields: {title: "The Invisible Man"}
|
||||
model: mysite.book
|
||||
pk: 18
|
||||
- fields: {title: "The Last of the Mohicans"}
|
||||
model: mysite.book
|
||||
pk: 19
|
||||
- fields: {title: "Oliver Twist, or the Parish Boy\'s Progress"}
|
||||
model: mysite.book
|
||||
pk: 20
|
||||
- fields: {title: "Peter Pan in Kensington Gardens"}
|
||||
model: mysite.book
|
||||
pk: 21
|
||||
- fields: {title: "Tales of the Jazz Age"}
|
||||
model: mysite.book
|
||||
pk: 22
|
||||
- fields: {title: "Tess of the D'Urbervilles: A Pure Woman Faithfully Presented"}
|
||||
model: mysite.book
|
||||
pk: 23
|
|
@ -0,0 +1,337 @@
|
|||
- fields: {admin_access_only: false, html: '', name: 'Example Questionnaire', parse_html: false, redirect_url: /}
|
||||
model: questionnaire.questionnaire
|
||||
pk: 1
|
||||
- fields: {checks: '', heading: Page 1, parse_html: true, questionnaire: 1, sortid: 1,
|
||||
text_de: <h2> Biervorlieben</h2>, text_en: <h2>Beer Preferences</h2>}
|
||||
model: questionnaire.questionset
|
||||
pk: 1
|
||||
- fields: {checks: '', heading: Page 2, parse_html: false, questionnaire: 1, sortid: 2,
|
||||
text_de: h1. Python Web Frameworks, text_en: h1. Python Web Frameworks}
|
||||
model: questionnaire.questionset
|
||||
pk: 2
|
||||
- fields: {checks: '', heading: Finished!, parse_html: true, questionnaire: 1, sortid: 99,
|
||||
text_de: "<h2> Vielen Dank </h2>\r\n \r\n Wir hoffen, dass Sie uns in Zukunft
|
||||
wieder besuchen!", text_en: "<h2>Thank you! </h2>\r\n \r\n We hope that you
|
||||
visit us again in the future!"}
|
||||
model: questionnaire.questionset
|
||||
pk: 3
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: null, footer_en: '',
|
||||
number: '1', parse_html: false, questionset: 1, sort_id: null, text_de: 'Trinken
|
||||
Sie Bier?', text_en: 'Do you drink beer?', type: choice-yesno}
|
||||
model: questionnaire.question
|
||||
pk: 1
|
||||
- fields: {checks: 'requiredif="1,yes"', extra_de: '', extra_en: '', footer_de: null,
|
||||
footer_en: '', number: '2', parse_html: false, questionset: 1, sort_id: null,
|
||||
text_de: 'Was Art von Bier trinken Sie am meisten?', text_en: 'What type of beer
|
||||
do you drink predominantly?', type: choice-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 2
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: null, footer_en: '',
|
||||
number: '3', parse_html: false, questionset: 1, sort_id: null, text_de: 'Welche
|
||||
von dieser Bieren haben Sie probiert?', text_en: 'Which of these brands of beer
|
||||
have you tried?', type: choice-multiple}
|
||||
model: questionnaire.question
|
||||
pk: 3
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: null, footer_en: '',
|
||||
number: '4', parse_html: false, questionset: 2, sort_id: null, text_de: 'Which
|
||||
Python Web Frameworks have you tried?', text_en: 'Which Python Web Frameworks
|
||||
have you tried?', type: choice-multiple-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 4
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: null, footer_en: '',
|
||||
number: '5', parse_html: false, questionset: 2, sort_id: null, text_de: "Welche
|
||||
m\xF6gen Sie am liebsten?", text_en: 'Which do you like the most?', type: choice-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 5
|
||||
- fields: {question: 2, sortid: 1, tags: '', text_de: Pils, text_en: Pils/Bitter,
|
||||
value: pils}
|
||||
model: questionnaire.choice
|
||||
pk: 1
|
||||
- fields: {question: 2, sortid: 2, tags: '', text_de: Helles, text_en: Lager, value: helles}
|
||||
model: questionnaire.choice
|
||||
pk: 2
|
||||
- fields: {question: 2, sortid: 3, tags: '', text_de: Weizen, text_en: Wheat-beer,
|
||||
value: weizen}
|
||||
model: questionnaire.choice
|
||||
pk: 3
|
||||
- fields: {question: 3, sortid: 1, tags: '', text_de: Heineken, text_en: Heineken,
|
||||
value: heineken}
|
||||
model: questionnaire.choice
|
||||
pk: 4
|
||||
- fields: {question: 3, sortid: 2, tags: '', text_de: Becks, text_en: Becks, value: becks}
|
||||
model: questionnaire.choice
|
||||
pk: 5
|
||||
- fields: {question: 3, sortid: 3, tags: '', text_de: "L\xF6wenbr\xE4u", text_en: "L\xF6wenbr\xE4u",
|
||||
value: lowenbrau}
|
||||
model: questionnaire.choice
|
||||
pk: 6
|
||||
- fields: {question: 2, sortid: 4, tags: '', text_de: "Altbier/D\xFCssel", text_en: Altbier,
|
||||
value: altbier}
|
||||
model: questionnaire.choice
|
||||
pk: 7
|
||||
- fields: {question: 2, sortid: 5, tags: '', text_de: "K\xF6lsch", text_en: "K\xF6lsch",
|
||||
value: koelsch}
|
||||
model: questionnaire.choice
|
||||
pk: 8
|
||||
- fields: {question: 4, sortid: 1, tags: '', text_de: Django, text_en: Django, value: django}
|
||||
model: questionnaire.choice
|
||||
pk: 9
|
||||
- fields: {question: 4, sortid: 2, tags: '', text_de: Pylons, text_en: Pylons, value: pylons}
|
||||
model: questionnaire.choice
|
||||
pk: 10
|
||||
- fields: {question: 4, sortid: 3, tags: '', text_de: Turbogears, text_en: Turbogears,
|
||||
value: turbogears}
|
||||
model: questionnaire.choice
|
||||
pk: 11
|
||||
- fields: {question: 4, sortid: 4, tags: '', text_de: CherryPy, text_en: CherryPy,
|
||||
value: cherrypy}
|
||||
model: questionnaire.choice
|
||||
pk: 12
|
||||
- fields: {question: 5, sortid: 1, tags: '', text_de: Django, text_en: Django, value: django}
|
||||
model: questionnaire.choice
|
||||
pk: 13
|
||||
- fields: {question: 5, sortid: 2, tags: '', text_de: Pylons, text_en: Pylons, value: pylons}
|
||||
model: questionnaire.choice
|
||||
pk: 14
|
||||
- fields: {question: 5, sortid: 3, tags: '', text_de: Turbogears, text_en: Turbogears,
|
||||
value: turbogears}
|
||||
model: questionnaire.choice
|
||||
pk: 15
|
||||
- fields: {question: 5, sortid: 4, tags: '', text_de: CherryPy, text_en: CherryPy,
|
||||
value: cherrypy}
|
||||
model: questionnaire.choice
|
||||
pk: 16
|
||||
- fields: {body_de: "Wilkommen zu der Fragebogenbeispielseite!\r\n\r\nWenn Sie einen
|
||||
einfachen Fragebogen sehen wollen, <a href=\"/q/take/1\"> \"tun Sie's!</a>\r\n\r\nSie
|
||||
k\xF6nnen das <a href=\"/admin/\">Admin Interface</a> benutzen, um den Fragebogen
|
||||
zu \xE4ndern.", body_en: "Welcome to the Example Questionnaire website!\r\n\r\nIf you wish to
|
||||
take a sample questionnaire, <a href=\"q/take/1\">go for it!</a>. Or <a href=\"q/items/\">link
|
||||
a questionnaire to an item</a>\r\n\r\nUse the <a href=\"/admin/\">Admin Interface</a> to
|
||||
change the questionnaire.", public: true,
|
||||
title_de: '', title_en: Welcome}
|
||||
model: page.page
|
||||
pk: index
|
||||
- fields: {admin_access_only: false, html: questionnaire html here, name: MappingSurvey,
|
||||
parse_html: false, redirect_url: ''}
|
||||
model: questionnaire.questionnaire
|
||||
pk: 3
|
||||
- fields: {checks: '', heading: Open Access Ebooks (Part 1), parse_html: true, questionnaire: 3,
|
||||
sortid: 1, text_de: '', text_en: " <h1> Introduction </h1> \r\n <p> \r\nWelcome, reader of
|
||||
<i>{{ landing_object.title }}</i>! And thanks for visiting Unglue.it to complete
|
||||
this survey, part of a research project to understand how open access ebooks
|
||||
are discovered and how readers use them. For more information, please see <a
|
||||
href=\"http://www.publishing.umich.edu/projects/mapping-the-free-ebook/\">the
|
||||
project description</a>.\r\n </p> \r\n <p> \r\nAs Open Access publishers, {{
|
||||
landing_object.claim.all.0.rights_holder }} are truly committed to making academic
|
||||
research broadly accessible - so we want to understand how people like you are
|
||||
actually accessing and using our Open Access titles. \r\n </p> \r\n <p> \r\nWe
|
||||
have a bunch of questions for you (well - only 10 actually) about how you found
|
||||
this book and what you\u2019re going to do with it. Please tell us the things
|
||||
you think are interesting or relevant. We really want to know!\r\n </p> \r\n
|
||||
<p> \r\n[Privacy policy: There are no marketing traps, we\u2019re not going
|
||||
to spy on you or swamp you with emails afterwards, or tell our \u201Cfriends\u201D
|
||||
about you - we\u2019re just going to store your answers to create a database
|
||||
of usage examples that can be used to understand what Open Access publishing
|
||||
enables. A <a href=\"http://www.publishing.umich.edu/projects/mapping-the-free-ebook/\">report
|
||||
of the results</a> will be made available in July 2017 ]\r\n</p>"}
|
||||
model: questionnaire.questionset
|
||||
pk: 5
|
||||
- fields: {checks: '', heading: Now About You..., parse_html: true, questionnaire: 3,
|
||||
sortid: 2, text_de: '', text_en: ' <p> And now, four questions about you as well ... </p> '}
|
||||
model: questionnaire.questionset
|
||||
pk: 6
|
||||
- fields: {checks: '', heading: Follow-up, parse_html: true, questionnaire: 3, sortid: 3,
|
||||
text_en: " <p> We would really like to be able to follow up with some of the respondents
|
||||
to this questionnaire to ask them a few more questions - particularly if you\u2019ve
|
||||
told us something really interesting in a comment (for example). [There will
|
||||
also be a little reward (a free book no less!) for those of you we do contact
|
||||
in this way.] </p> \r\n\r\n <p> Thanks so much for your time and efforts answering
|
||||
these questions for us - we love you for it! </p> \r\n\r\n <p> We hope you enjoy
|
||||
<i>{{ landing_object.title }}</i>. </p> \r\n\r\n <p> {{ landing_object.claim.all.0.rights_holder
|
||||
}} and Unglue.it </p> \r\n"}
|
||||
model: questionnaire.questionset
|
||||
pk: 7
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '1', parse_html: true,
|
||||
questionset: 5, sort_id: 1, text_de: '', text_en: "How did you find out about this book in
|
||||
the first place? <br /> <br /> \r\n\r\nFor example: Was it from a Google search?
|
||||
Following a wikipedia link? A tweet? Referenced in another book? A late night
|
||||
session with a friend? - or in some other way?\r\n", type: open}
|
||||
model: questionnaire.question
|
||||
pk: 16
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '2', parse_html: false,
|
||||
questionset: 5, sort_id: 2, text_de: '', text_en: "How did you get hold of this particular
|
||||
copy? \r\n\r\nFor example: Did you download it from the publisher's website?
|
||||
Amazon or another retailer? Find it on academia.edu? Or somewhere like aaaaarg?
|
||||
Get it from a friend? Your library?", type: open}
|
||||
model: questionnaire.question
|
||||
pk: 17
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '3', parse_html: true,
|
||||
questionset: 5, sort_id: 3, text_de: '', text_en: 'Why are you interested in this book?', type: choice-multiple-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 18
|
||||
- fields: {checks: '', extra_de: '', extra_en: 'If Yes - is there any particular reason why you
|
||||
are using this version rather than one of the others?', footer_de: '', footer_en: '', number: '4',
|
||||
parse_html: false, questionset: 5, sort_id: 4, text_de: '', text_en: 'Are you aware that this
|
||||
title is available in multiple different digital and printed formats?', type: choice-yesnocomment-optional}
|
||||
model: questionnaire.question
|
||||
pk: 19
|
||||
- fields: {checks: '', extra_de: '', extra_en: 'Please tell us in more detail:', footer_de: '', footer_en: "\r\n\r\n\r\n\r\n\r\n\r\n\r\n",
|
||||
number: '5', parse_html: false, questionset: 5, sort_id: 5, text_de: '', text_en: 'What are
|
||||
you going to do with it now you have it?', type: choice-multiple-freeform}
|
||||
model: questionnaire.question
|
||||
pk: 20
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '1', parse_html: false,
|
||||
questionset: 6, sort_id: null, text_de: '', text_en: 'Where do you live?', type: choice-freeform-optional}
|
||||
model: questionnaire.question
|
||||
pk: 21
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '2', parse_html: false,
|
||||
questionset: 6, sort_id: null, text_de: '', text_en: 'What do you do for a living?', type: open}
|
||||
model: questionnaire.question
|
||||
pk: 22
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: "\r\n\r\n \r\n\r\n", number: '4',
|
||||
parse_html: false, questionset: 6, sort_id: null, text_de: '', text_en: 'When did you finish
|
||||
your formal education?', type: choice-freeform-optional}
|
||||
model: questionnaire.question
|
||||
pk: 23
|
||||
- fields: {checks: required-no, extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '5', parse_html: false,
|
||||
questionset: 6, sort_id: null, text_de: '', text_en: ' Is there anything else you would like
|
||||
to tell us or think we should know about how you found or are using the ebook?
|
||||
or about yourself?', type: open-textfield}
|
||||
model: questionnaire.question
|
||||
pk: 24
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '1', parse_html: false,
|
||||
questionset: 7, sort_id: null, text_de: '', text_en: "If you\u2019re willing, then please leave
|
||||
us an email address where we could make contact with you (information which
|
||||
we won\u2019t share or make public).\r\n", type: open}
|
||||
model: questionnaire.question
|
||||
pk: 25
|
||||
- fields: {checks: '', extra_de: '', extra_en: '', footer_de: '', footer_en: '', number: '3', parse_html: false,
|
||||
questionset: 6, sort_id: null, text_de: '', text_en: 'How old are you?', type: choice-optional}
|
||||
model: questionnaire.question
|
||||
pk: 26
|
||||
- fields: {question: 18, sortid: 1, tags: '', text_de: '', text_en: "For personal use - I\u2019m
|
||||
interested in the topic ", value: personal}
|
||||
model: questionnaire.choice
|
||||
pk: 17
|
||||
- fields: {question: 18, sortid: 2, tags: '', text_de: '', text_en: 'For my job - it relates to
|
||||
what I do ', value: job}
|
||||
model: questionnaire.choice
|
||||
pk: 18
|
||||
- fields: {question: 18, sortid: 3, tags: '', text_de: '', text_en: I need to read it for a course,
|
||||
value: course}
|
||||
model: questionnaire.choice
|
||||
pk: 19
|
||||
- fields: {question: 20, sortid: 1, tags: '', text_de: '', text_en: 'Save it, in case I need to
|
||||
use it in the future', value: save}
|
||||
model: questionnaire.choice
|
||||
pk: 20
|
||||
- fields: {question: 20, sortid: 2, tags: '', text_de: '', text_en: "Skim through it and see if
|
||||
it\u2019s at all interesting", value: skim}
|
||||
model: questionnaire.choice
|
||||
pk: 21
|
||||
- fields: {question: 20, sortid: 3, tags: '', text_de: '', text_en: "There\u2019s only really a
|
||||
section/chapter I\u2019m interested in - I\u2019ll probably just read that",
|
||||
value: section}
|
||||
model: questionnaire.choice
|
||||
pk: 22
|
||||
- fields: {question: 20, sortid: 4, tags: '', text_de: '', text_en: "The whole book looks fascinating
|
||||
- I\u2019m going to read it all!", value: whole}
|
||||
model: questionnaire.choice
|
||||
pk: 23
|
||||
- fields: {question: 20, sortid: 5, tags: '', text_de: '', text_en: "I\u2019m going to adapt it
|
||||
and use it (or, at least, parts of it) for another purpose (eg a student coursepack,
|
||||
lecture/briefing notes \u2026)", value: adapt}
|
||||
model: questionnaire.choice
|
||||
pk: 24
|
||||
- fields: {question: 20, sortid: 6, tags: '', text_de: '', text_en: 'Share it with my friends ',
|
||||
value: share}
|
||||
model: questionnaire.choice
|
||||
pk: 25
|
||||
- fields: {question: 20, sortid: 7, tags: '', text_de: '', text_en: Print it out, value: print}
|
||||
model: questionnaire.choice
|
||||
pk: 26
|
||||
- fields: {question: 20, sortid: 8, tags: '', text_de: '', text_en: "I\u2019m creating/collating
|
||||
a (online) library", value: catalog}
|
||||
model: questionnaire.choice
|
||||
pk: 27
|
||||
- fields: {question: 20, sortid: 9, tags: '', text_de: '', text_en: "Something else entirely \u2026. ",
|
||||
value: else}
|
||||
model: questionnaire.choice
|
||||
pk: 28
|
||||
- fields: {question: 21, sortid: 1, tags: '', text_de: '', text_en: Canada/USA, value: us}
|
||||
model: questionnaire.choice
|
||||
pk: 29
|
||||
- fields: {question: 21, sortid: 4, tags: '', text_de: '', text_en: Europe, value: eu}
|
||||
model: questionnaire.choice
|
||||
pk: 30
|
||||
- fields: {question: 21, sortid: 3, tags: '', text_de: '', text_en: South America, value: sa}
|
||||
model: questionnaire.choice
|
||||
pk: 31
|
||||
- fields: {question: 21, sortid: 2, tags: '', text_de: '', text_en: Central America/ Caribbean,
|
||||
value: ca}
|
||||
model: questionnaire.choice
|
||||
pk: 32
|
||||
- fields: {question: 21, sortid: 9, tags: '', text_de: '', text_en: Other Asia, value: as}
|
||||
model: questionnaire.choice
|
||||
pk: 33
|
||||
- fields: {question: 21, sortid: 6, tags: '', text_de: '', text_en: Africa, value: af}
|
||||
model: questionnaire.choice
|
||||
pk: 34
|
||||
- fields: {question: 21, sortid: 5, tags: '', text_de: '', text_en: Middle East, value: me}
|
||||
model: questionnaire.choice
|
||||
pk: 35
|
||||
- fields: {question: 21, sortid: 11, tags: '', text_de: '', text_en: Another Planet, value: ap}
|
||||
model: questionnaire.choice
|
||||
pk: 36
|
||||
- fields: {question: 23, sortid: 1, tags: '', text_de: '', text_en: "I haven\u2019t - I\u2019m
|
||||
still a student", value: x}
|
||||
model: questionnaire.choice
|
||||
pk: 37
|
||||
- fields: {question: 23, sortid: 2, tags: '', text_de: '', text_en: At primary/elementary school,
|
||||
value: '8'}
|
||||
model: questionnaire.choice
|
||||
pk: 38
|
||||
- fields: {question: 23, sortid: 3, tags: '', text_de: '', text_en: At high school/secondary school,
|
||||
value: h}
|
||||
model: questionnaire.choice
|
||||
pk: 39
|
||||
- fields: {question: 23, sortid: 4, tags: '', text_de: '', text_en: After trade qualifications,
|
||||
value: t}
|
||||
model: questionnaire.choice
|
||||
pk: 40
|
||||
- fields: {question: 23, sortid: 5, tags: '', text_de: '', text_en: 'At College/Undergraduate Degree ',
|
||||
value: c}
|
||||
model: questionnaire.choice
|
||||
pk: 41
|
||||
- fields: {question: 23, sortid: 6, tags: '', text_de: '', text_en: At Grad School/post-graduate
|
||||
university, value: g}
|
||||
model: questionnaire.choice
|
||||
pk: 42
|
||||
- fields: {question: 18, sortid: 4, tags: '', text_de: '', text_en: 'If other, tell us more...',
|
||||
value: other}
|
||||
model: questionnaire.choice
|
||||
pk: 43
|
||||
- fields: {question: 26, sortid: 1, tags: '', text_de: '', text_en: under 18, value: teen}
|
||||
model: questionnaire.choice
|
||||
pk: 46
|
||||
- fields: {question: 26, sortid: 2, tags: '', text_de: '', text_en: 18-30, value: young}
|
||||
model: questionnaire.choice
|
||||
pk: 47
|
||||
- fields: {question: 26, sortid: 3, tags: '', text_de: '', text_en: 31-60, value: mid}
|
||||
model: questionnaire.choice
|
||||
pk: 48
|
||||
- fields: {question: 26, sortid: 4, tags: '', text_de: '', text_en: over 60, value: old}
|
||||
model: questionnaire.choice
|
||||
pk: 49
|
||||
- fields: {question: 26, sortid: 5, tags: '', text_de: '', text_en: decline to say, value: x}
|
||||
model: questionnaire.choice
|
||||
pk: 50
|
||||
- fields: {question: 21, sortid: 10, tags: '', text_de: '', text_en: Oceania, value: oc}
|
||||
model: questionnaire.choice
|
||||
pk: 51
|
||||
- fields: {question: 21, sortid: 7, tags: '', text_de: '', text_en: India, value: in}
|
||||
model: questionnaire.choice
|
||||
pk: 52
|
||||
- fields: {question: 21, sortid: 8, tags: '', text_de: '', text_en: China, value: zh}
|
||||
model: questionnaire.choice
|
||||
pk: 53
|
|
@ -1,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
|
|
@ -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
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
|
||||
<title>{% block title %}Questionnaire{% endblock title %}</title>
|
||||
|
||||
<link rel="stylesheet" href="/static/bootstrap/bootstrap.min.css" type="text/css"/>
|
||||
<link rel="stylesheet" href="/static/questionnaire.css"></script>
|
||||
|
||||
<style type="text/css">
|
||||
{% block styleextra %}
|
||||
|
||||
{% endblock %}
|
||||
</style>
|
||||
|
||||
{% block headextra %}
|
||||
{% endblock %}
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="content">
|
||||
|
||||
<div id="languages">
|
||||
{% block language %}
|
||||
{% for lang in LANGUAGES %}
|
||||
{% if not forloop.first %} | {% endif %}
|
||||
<a href="/setlang/?lang={{ lang.0 }}&next={{ request.path }}">{{ lang.1 }}</a>
|
||||
{% endfor %}
|
||||
{% endblock language %}
|
||||
</div>
|
||||
|
||||
<div class="page-header">
|
||||
<h1>Sample Django Questionnaire</h1>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="span1"> </div>
|
||||
<div class="span14">
|
||||
{% block content %}
|
||||
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud execitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.</p>
|
||||
{% endblock %}
|
||||
</div>
|
||||
<div class="span1"> </div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
|
@ -1,18 +1,16 @@
|
|||
from django.conf.urls import patterns, include, url
|
||||
from django.contrib import admin
|
||||
|
||||
import questionnaire
|
||||
|
||||
admin.autodiscover()
|
||||
|
||||
urlpatterns = patterns('',
|
||||
|
||||
url(r'q/', include('questionnaire.urls')),
|
||||
|
||||
url(r'^take/(?P<questionnaire_id>[0-9]+)/$', 'questionnaire.views.generate_run'),
|
||||
url(r'^$', 'questionnaire.page.views.page', {'page_to_render' : 'index'}),
|
||||
url(r'^(?P<lang>..)/(?P<page_to_trans>.*)\.html$', 'questionnaire.page.views.langpage'),
|
||||
url(r'^(?P<page_to_render>.*)\.html$', 'questionnaire.page.views.page'),
|
||||
url(r'^setlang/$', 'questionnaire.views.set_language'),
|
||||
url(r'q/', include('questionnaire.urls')),
|
||||
|
||||
# admin
|
||||
url(r'^admin/doc/', include('django.contrib.admindocs.urls')),
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
)
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
{
|
||||
"fields": {
|
||||
"admin_access_only": false,
|
||||
"html": "survey html here",
|
||||
"html": "questionnaire html here",
|
||||
"name": "MappingSurvey",
|
||||
"parse_html": false,
|
||||
"redirect_url": ""
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
from django import forms
|
||||
from .models import Questionnaire
|
||||
|
||||
class NewLandingForm(forms.Form):
|
||||
label = forms.CharField(max_length=64, required=True)
|
||||
questionnaire = forms.ModelChoiceField(
|
||||
Questionnaire.objects.all(),
|
||||
widget=forms.widgets.RadioSelect(),
|
||||
empty_label=None,
|
||||
required=True,
|
||||
)
|
||||
|
|
@ -3,7 +3,7 @@ from ...models import Landing
|
|||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "make survey nonces with the specified label"
|
||||
help = "make landing nonces with the specified label"
|
||||
args = "<how_many> <label>"
|
||||
|
||||
def handle(self, how_many=1, label="no label yet", **options):
|
|
@ -75,8 +75,8 @@ class Migration(migrations.Migration):
|
|||
('name', models.CharField(max_length=128)),
|
||||
('redirect_url', models.CharField(default=b'', help_text=b"URL to redirect to when Questionnaire is complete. Macros: $SUBJECTID, $RUNID, $LANG. Leave blank to render the 'complete.$LANG.html' template.", max_length=128, blank=True)),
|
||||
('html', models.TextField(verbose_name='Html', blank=True)),
|
||||
('parse_html', models.BooleanField(default=False, verbose_name=b'Render html instead of name for survey?')),
|
||||
('admin_access_only', models.BooleanField(default=False, verbose_name=b'Only allow access to logged in users? (This allows entering paper surveys without allowing new external submissions)')),
|
||||
('parse_html', models.BooleanField(default=False, verbose_name=b'Render html instead of name for questionnaire?')),
|
||||
('admin_access_only', models.BooleanField(default=False, verbose_name=b'Only allow access to logged in users? (This allows entering paper questionnaires without allowing new external submissions)')),
|
||||
],
|
||||
options={
|
||||
'permissions': (('export', 'Can export questionnaire answers'), ('management', 'Management Tools')),
|
||||
|
|
|
@ -4,6 +4,7 @@ import re
|
|||
import uuid
|
||||
from datetime import datetime
|
||||
from transmeta import TransMeta
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.contenttypes.fields import GenericForeignKey
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
|
@ -87,8 +88,8 @@ class Questionnaire(models.Model):
|
|||
name = models.CharField(max_length=128)
|
||||
redirect_url = models.CharField(max_length=128, help_text="URL to redirect to when Questionnaire is complete. Macros: $SUBJECTID, $RUNID, $LANG. Leave blank to render the 'complete.$LANG.html' template.", default="", blank=True)
|
||||
html = models.TextField(u'Html', blank=True)
|
||||
parse_html = models.BooleanField("Render html instead of name for survey?", null=False, default=False)
|
||||
admin_access_only = models.BooleanField("Only allow access to logged in users? (This allows entering paper surveys without allowing new external submissions)", null=False, default=False)
|
||||
parse_html = models.BooleanField("Render html instead of name for questionnaire?", null=False, default=False)
|
||||
admin_access_only = models.BooleanField("Only allow access to logged in users? (This allows entering paper questionnaires without allowing new external submissions)", null=False, default=False)
|
||||
|
||||
def __unicode__(self):
|
||||
return self.name
|
||||
|
@ -126,7 +127,12 @@ class Landing(models.Model):
|
|||
return self.label
|
||||
|
||||
def url(self):
|
||||
return settings.BASE_URL_SECURE + reverse('landing', args=[self.nonce])
|
||||
try:
|
||||
return settings.BASE_URL_SECURE + reverse('landing', args=[self.nonce])
|
||||
except AttributeError:
|
||||
# not using sites
|
||||
return reverse('landing', args=[self.nonce])
|
||||
|
||||
|
||||
def config_landing(sender, instance, created, **kwargs):
|
||||
if created:
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
# Create your views here.
|
||||
from django.shortcuts import render, render_to_response
|
||||
from django.shortcuts import render
|
||||
from django.conf import settings
|
||||
from django.template import RequestContext
|
||||
from django import http
|
||||
from django.utils import translation
|
||||
from .models import Page
|
||||
|
@ -10,12 +9,11 @@ def page(request, page_to_render):
|
|||
try:
|
||||
p = Page.objects.get(slug=page_to_render, public=True)
|
||||
except Page.DoesNotExist:
|
||||
return render(request, "pages/{}.html".format(page_to_render),
|
||||
{ "request" : request,},
|
||||
return render(request, "pages/{}.html".format(page_to_render),
|
||||
{ "request" : request,},
|
||||
)
|
||||
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):
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
{% extends 'base-questionnaire.html' %}
|
||||
|
||||
{% block title %}Item-linked Questionnaire Management {% endblock %}
|
||||
|
||||
{% block topsection %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Questionnaire Tools </h1>
|
||||
|
||||
<h2>Configure a new landing page</h2>
|
||||
<form action="#" method="POST" >
|
||||
{% csrf_token %}
|
||||
<p>Label for new questionnaire: {{ form.label}}{{ form.label.errors }}</p>
|
||||
<p>Questionnaire to use:{{ form.questionnaire }}{{ form.questionnaire.errors }}</p>
|
||||
<input type="submit" value="submit" />
|
||||
</form>
|
||||
{% endblock %}
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base-questionnaire.html" %}
|
||||
{% block questionnaire %}
|
||||
<h2>
|
||||
Thanks for completing the survey!
|
||||
Thanks for completing the questionnaire!
|
||||
</h2>
|
||||
|
||||
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
<h2>Tell us stuff!</h2>
|
||||
<p>We are excited that this is Open Access book and really hope that it will be shared around, read by lots of people all over the world, and used in lots of exciting new ways. We would love to hear about you and how you are using it - please would you share your story with us using this link: (it will only take about 3 minutes - and you could just get a free book as well ….)</p>
|
||||
<p>But comeback again, our survey for {{landing.label}} isn't ready yet.</p>
|
||||
<p>But comeback again, our questionnaire for {{landing.label}} isn't ready yet.</p>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{% extends "base-questionnaire.html" %}
|
||||
{% block questionnaire %}
|
||||
<h1>
|
||||
Survey Results Summary
|
||||
Questionnaire Results Summary
|
||||
</h1>
|
||||
{% for summary in summaries %}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
{% block questionnaire %}
|
||||
|
||||
<h2>
|
||||
Thanks for completing the survey!
|
||||
Thanks for completing the questionnaire!
|
||||
</h2>
|
||||
<div class="question-text">
|
||||
{{ landing_object.claim.all.0.rights_holder }}
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% load landings %}
|
||||
|
||||
{% block title %}
|
||||
Survey: {{ questionset.heading }}
|
||||
Questionnaire: {{ questionset.heading }}
|
||||
{% endblock %}
|
||||
|
||||
{% block headextra %}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
{% extends 'base-questionnaire.html' %}
|
||||
|
||||
{% block title %}item-linked Questionnaires {% endblock %}
|
||||
|
||||
{% block topsection %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
|
||||
<h1>Questionnaire Tools </h1>
|
||||
<h2 id="open_campaigns">items You Can Use for Questionnaires</h2>
|
||||
<dl>
|
||||
{% for item in items %}
|
||||
|
||||
<dt>{{ item }}</dt>
|
||||
<dd>
|
||||
<dl>
|
||||
{% for landing in item.landings.all %}
|
||||
<dt>Configured questionnaire: {{ landing }} </dt>
|
||||
<dd>Link: <a href="{{ landing.url }}">{{ landing.url }}</a><br />
|
||||
Completed {{ landing.runinfohistory_set.all.count }} times</dd>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
<a href="{% url 'new_questionnaire' item.id %}">Set up a new questionnaire</a> for this item.<br />
|
||||
{% for questionnaire in questionnaires %}
|
||||
<a href="{% url 'questionnaire_answers' questionnaire.id item.id %}">Export</a> or <a href="{% url 'answer_summary' questionnaire.id item.id %}">Summarize</a> answers to {{ questionnaire }} for this item.<br />
|
||||
|
||||
{% endfor %}
|
||||
<hr />
|
||||
</dd>
|
||||
{% empty %}
|
||||
<p>No items available</p>
|
||||
{% endfor %}
|
||||
</dl>
|
||||
<p>
|
||||
|
||||
{% for questionnaire in questionnaires %}
|
||||
<a href="{% url 'questionnaire_answers' questionnaire.id '' %}">Export all my answers to {{ questionnaire }}</a>.<br />
|
||||
<a href="{% url 'answer_summary' questionnaire.id '' %}">Summarize my responses to {{ questionnaire }}</a>.<br />
|
||||
|
||||
{% if request.user.is_staff %}<a href="{% url 'questionnaire_answers' questionnaire.id '0' %}">Export ALL answers to {{ questionnaire }}</a>.<br />
|
||||
<a href="{% url 'answer_summary' questionnaire.id '0' %}">Summarize ALL responses to {{ questionnaire }}</a>.<br />
|
||||
{% endif %}
|
||||
|
||||
{% endfor %}
|
||||
|
||||
</p>
|
||||
|
||||
<p>
|
||||
Add "?next=https://example.com/any_url" to the end of a questionnaire url to add a redirect on completion of the questionnaire.
|
||||
</p>
|
||||
{% endblock %}
|
|
@ -1,6 +1,7 @@
|
|||
# vim: set fileencoding=utf-8
|
||||
|
||||
from django.conf.urls import *
|
||||
from django.conf import settings
|
||||
from .views import *
|
||||
from .page.views import page, langpage
|
||||
|
||||
|
@ -18,11 +19,23 @@ urlpatterns = [
|
|||
url(r'^(?P<page_to_render>.*)\.html$', page),
|
||||
url(r'^(?P<lang>..)/(?P<page_to_trans>.*)\.html$', langpage),
|
||||
url(r'^setlang/$', set_language),
|
||||
url(r'^landing/(?P<nonce>\w+)/$', SurveyView.as_view(), name="landing"),
|
||||
url(r'^(?P<runcode>[^/]+)/(?P<qs>[-]{0,1}\d+)/$',
|
||||
questionnaire, name='questionset'),
|
||||
url(r'^landing/(?P<nonce>\w+)/$', QuestionnaireView.as_view(), name="landing"),
|
||||
]
|
||||
|
||||
# item questionnaires
|
||||
try:
|
||||
if settings.QUESTIONNAIRE_ITEM_MODEL and settings.QUESTIONNAIRE_SHOW_ITEM_RESULTS:
|
||||
urlpatterns += [
|
||||
url(r"^items/$", questionnaires, name="questionnaires"),
|
||||
url(r"^new_questionnaire/(?P<item_id>\d*)/?$", new_questionnaire, name="new_questionnaire"),
|
||||
url(r"^items/answers_(?P<qid>\d+)_(?P<item_id>\d*).csv$", export_item_csv, name="questionnaire_answers"),
|
||||
url(r"^items/summary_(?P<qid>\d+)_(?P<item_id>\d*).csv$", export_item_summary, name="answer_summary"),
|
||||
]
|
||||
except AttributeError:
|
||||
pass
|
||||
|
||||
urlpatterns += [url(r'^(?P<runcode>[^/]+)/(?P<qs>[-]{0,1}\d+)/$', questionnaire, name='questionset')]
|
||||
|
||||
if not use_session:
|
||||
urlpatterns += [
|
||||
url(r'^(?P<runcode>[^/]+)/$',
|
||||
|
@ -39,3 +52,4 @@ else:
|
|||
redirect_to_prev_questionnaire,
|
||||
name='redirect_to_prev_questionnaire')
|
||||
]
|
||||
|
||||
|
|
|
@ -1,22 +1,20 @@
|
|||
#!/usr/bin/python
|
||||
# vim: set fileencoding=utf-8
|
||||
import json
|
||||
import logging
|
||||
import random
|
||||
import re
|
||||
import tempfile
|
||||
|
||||
from compat import commit_on_success, commit, rollback
|
||||
from hashlib import md5
|
||||
from uuid import uuid4
|
||||
|
||||
from django.apps import apps
|
||||
from django.http import HttpResponse, HttpResponseRedirect
|
||||
from django.template import RequestContext
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.core.cache import cache
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
from django.shortcuts import render, render_to_response, get_object_or_404
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
from django.views.generic.base import TemplateView
|
||||
from django.db import transaction
|
||||
from django.conf import settings
|
||||
from datetime import datetime
|
||||
from django.utils import translation
|
||||
|
@ -25,14 +23,18 @@ from django.utils.translation import ugettext_lazy as _
|
|||
from . import QuestionProcessors
|
||||
from . import questionnaire_start, questionset_start, questionset_done, questionnaire_done
|
||||
from . import AnswerException
|
||||
from . import Processors
|
||||
from . import profiler
|
||||
from .models import *
|
||||
from .parsers import *
|
||||
from .parsers import BoolNot, BoolAnd, BoolOr, Checker
|
||||
from .emails import _send_email, send_emails
|
||||
from .emails import _send_email
|
||||
from .models import (
|
||||
Answer, Landing, Question, Questionnaire, QuestionSet, Run, RunInfo, RunInfoHistory, Subject,
|
||||
)
|
||||
from .forms import NewLandingForm
|
||||
from .parsers import BooleanParser
|
||||
from .utils import numal_sort, split_numal, UnicodeWriter
|
||||
from .request_cache import request_cache
|
||||
from .run import (
|
||||
add_answer, delete_answer, get_runinfo, get_question,
|
||||
question_satisfies_checks, questionset_satisfies_checks,
|
||||
get_progress, make_partially_evaluated_js_exp_for_shownif_check, substitute_answer
|
||||
)
|
||||
from .dependency_checker import dep_check
|
||||
|
||||
|
||||
|
@ -46,183 +48,18 @@ try:
|
|||
except AttributeError:
|
||||
debug_questionnaire = False
|
||||
|
||||
try:
|
||||
(app_label, model_name) = settings.QUESTIONNAIRE_ITEM_MODEL.split('.', 1)
|
||||
item_model = apps.get_model(app_label=app_label, model_name=model_name)
|
||||
except AttributeError:
|
||||
item_model = None
|
||||
|
||||
|
||||
def r2r(tpl, request, **contextdict):
|
||||
"Shortcut to use RequestContext instead of Context in templates"
|
||||
contextdict['request'] = request
|
||||
return render(request, tpl, contextdict)
|
||||
|
||||
|
||||
def get_runinfo(random):
|
||||
"Return the RunInfo entry with the provided random key"
|
||||
res = RunInfo.objects.filter(random=random.lower())
|
||||
return res and res[0] or None
|
||||
|
||||
|
||||
def get_question(number, questionset):
|
||||
"Return the specified Question (by number) from the specified Questionset"
|
||||
res = Question.objects.filter(number=number, questionset=questionset)
|
||||
return res and res[0] or None
|
||||
|
||||
|
||||
def delete_answer(question, subject, run):
|
||||
"Delete the specified question/subject/run combination from the Answer table"
|
||||
Answer.objects.filter(subject=subject, run=run, question=question).delete()
|
||||
|
||||
|
||||
def add_answer(runinfo, question, answer_dict):
|
||||
"""
|
||||
Add an Answer to a Question for RunInfo, given the relevant form input
|
||||
|
||||
answer_dict contains the POST'd elements for this question, minus the
|
||||
question_{number} prefix. The question_{number} form value is accessible
|
||||
with the ANSWER key.
|
||||
"""
|
||||
answer = Answer()
|
||||
answer.question = question
|
||||
answer.subject = runinfo.subject
|
||||
answer.run = runinfo.run
|
||||
|
||||
type = question.get_type()
|
||||
|
||||
if "ANSWER" not in answer_dict:
|
||||
answer_dict['ANSWER'] = None
|
||||
|
||||
if type in Processors:
|
||||
answer.answer = Processors[type](question, answer_dict) or ''
|
||||
else:
|
||||
raise AnswerException("No Processor defined for question type %s" % type)
|
||||
|
||||
# first, delete all existing answers to this question for this particular user+run
|
||||
delete_answer(question, runinfo.subject, runinfo.run)
|
||||
|
||||
# then save the new answer to the database
|
||||
answer.save(runinfo)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def check_parser(runinfo, exclude=[]):
|
||||
depparser = BooleanParser(dep_check, runinfo, {})
|
||||
tagparser = BooleanParser(has_tag, runinfo)
|
||||
|
||||
fnmap = {
|
||||
"maleonly": lambda v: runinfo.subject.gender == 'male',
|
||||
"femaleonly": lambda v: runinfo.subject.gender == 'female',
|
||||
"shownif": lambda v: v and depparser.parse(v),
|
||||
"iftag": lambda v: v and tagparser.parse(v)
|
||||
}
|
||||
|
||||
for ex in exclude:
|
||||
del fnmap[ex]
|
||||
|
||||
@request_cache()
|
||||
def satisfies_checks(checks):
|
||||
if not checks:
|
||||
return True
|
||||
|
||||
checks = parse_checks(checks)
|
||||
|
||||
for check, value in checks.items():
|
||||
if check in fnmap:
|
||||
value = value and value.strip()
|
||||
if not fnmap[check](value):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
return satisfies_checks
|
||||
|
||||
|
||||
@request_cache()
|
||||
def skipped_questions(runinfo):
|
||||
if not runinfo.skipped:
|
||||
return []
|
||||
|
||||
return [s.strip() for s in runinfo.skipped.split(',')]
|
||||
|
||||
|
||||
@request_cache()
|
||||
def question_satisfies_checks(question, runinfo, checkfn=None):
|
||||
if question.number in skipped_questions(runinfo):
|
||||
return False
|
||||
|
||||
checkfn = checkfn or check_parser(runinfo)
|
||||
return checkfn(question.checks)
|
||||
|
||||
|
||||
@request_cache(keyfn=lambda *args: args[0].id)
|
||||
def questionset_satisfies_checks(questionset, runinfo, checks=None):
|
||||
"""Return True if the runinfo passes the checks specified in the QuestionSet
|
||||
|
||||
Checks is an optional dictionary with the keys being questionset.pk and the
|
||||
values being the checks of the contained questions.
|
||||
|
||||
This, in conjunction with fetch_checks allows for fewer
|
||||
db roundtrips and greater performance.
|
||||
|
||||
Sadly, checks cannot be hashed and therefore the request cache is useless
|
||||
here. Thankfully the benefits outweigh the costs in my tests.
|
||||
"""
|
||||
|
||||
passes = check_parser(runinfo)
|
||||
|
||||
if not passes(questionset.checks):
|
||||
return False
|
||||
|
||||
if not checks:
|
||||
checks = dict()
|
||||
checks[questionset.id] = []
|
||||
|
||||
for q in questionset.questions():
|
||||
checks[questionset.id].append((q.checks, q.number))
|
||||
|
||||
# questionsets that pass the checks but have no questions are shown
|
||||
# (comments, last page, etc.)
|
||||
if not checks[questionset.id]:
|
||||
return True
|
||||
|
||||
# if there are questions at least one needs to be visible
|
||||
for check, number in checks[questionset.id]:
|
||||
if number in skipped_questions(runinfo):
|
||||
continue
|
||||
|
||||
if passes(check):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def get_progress(runinfo):
|
||||
position, total = 0, 0
|
||||
|
||||
current = runinfo.questionset
|
||||
sets = current.questionnaire.questionsets()
|
||||
|
||||
checks = fetch_checks(sets)
|
||||
|
||||
# fetch the all question checks at once. This greatly improves the
|
||||
# performance of the questionset_satisfies_checks function as it
|
||||
# can avoid a roundtrip to the database for each question
|
||||
|
||||
for qs in sets:
|
||||
if questionset_satisfies_checks(qs, runinfo, checks):
|
||||
total += 1
|
||||
|
||||
if qs.id == current.id:
|
||||
position = total
|
||||
|
||||
if not all((position, total)):
|
||||
progress = 1
|
||||
else:
|
||||
progress = float(position) / float(total) * 100.00
|
||||
|
||||
# progress is always at least one percent
|
||||
progress = progress >= 1.0 and progress or 1
|
||||
|
||||
return int(progress)
|
||||
|
||||
|
||||
def get_async_progress(request, *args, **kwargs):
|
||||
""" Returns the progress as json for use with ajax """
|
||||
|
||||
|
@ -243,24 +80,6 @@ def get_async_progress(request, *args, **kwargs):
|
|||
return response
|
||||
|
||||
|
||||
def fetch_checks(questionsets):
|
||||
ids = [qs.pk for qs in questionsets]
|
||||
|
||||
query = Question.objects.filter(questionset__pk__in=ids)
|
||||
query = query.values('questionset_id', 'checks', 'number')
|
||||
|
||||
checks = dict()
|
||||
for qsid in ids:
|
||||
checks[qsid] = list()
|
||||
|
||||
for result in (r for r in query):
|
||||
checks[result['questionset_id']].append(
|
||||
(result['checks'], result['number'])
|
||||
)
|
||||
|
||||
return checks
|
||||
|
||||
|
||||
def redirect_to_qs(runinfo, request=None):
|
||||
"Redirect to the correct and current questionset URL for this RunInfo"
|
||||
|
||||
|
@ -304,7 +123,7 @@ def redirect_to_prev_questionnaire(request, runcode=None, qs=None):
|
|||
"""
|
||||
Takes the questionnaire set in the session and redirects to the
|
||||
previous questionnaire if any. Used for linking to previous pages
|
||||
both when using sessions or not.
|
||||
both when using sessions or not.
|
||||
"""
|
||||
if use_session:
|
||||
runcode = request.session.get('runcode', None)
|
||||
|
@ -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),
|
||||
)
|
||||
|
|
5
setup.py
5
setup.py
|
@ -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'
|
||||
|
|
Loading…
Reference in New Issue