From 1ebe75f9f7a294abf46a0b7155186746abecd66d Mon Sep 17 00:00:00 2001 From: eric Date: Mon, 21 Aug 2017 12:03:38 -0400 Subject: [PATCH] remove questionnaire duplicate --- questionnaire/LICENSE | 27 - questionnaire/README.md | 360 ------ questionnaire/admin.py | 97 -- questionnaire/templates/pages/summaries.html | 35 - questionnaire/urls.py | 41 - questionnaire/utils.py | 92 -- questionnaire/views.py | 1107 ------------------ 7 files changed, 1759 deletions(-) delete mode 100644 questionnaire/LICENSE delete mode 100644 questionnaire/README.md delete mode 100644 questionnaire/admin.py delete mode 100644 questionnaire/templates/pages/summaries.html delete mode 100644 questionnaire/urls.py delete mode 100644 questionnaire/utils.py delete mode 100644 questionnaire/views.py diff --git a/questionnaire/LICENSE b/questionnaire/LICENSE deleted file mode 100644 index 179c23d8..00000000 --- a/questionnaire/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) Robert Thomson and individual contributors. -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - 3. Neither the name of Robert Thomson, Seantis, nor the names of its - contributors may be used to endorse or promote products derived from - this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/questionnaire/README.md b/questionnaire/README.md deleted file mode 100644 index 0e8764da..00000000 --- a/questionnaire/README.md +++ /dev/null @@ -1,360 +0,0 @@ -FEF Questionnaire -===================== - -Introduction ------------- - -FEF Questionnaire is a Django questionnaire app which is easily customizable -and includes advanced dependency support using boolean expressions. - -It allows an administrator to create and edit questionnaires in the Django -admin interface, with support for multiple languages. - -It can be run either as a survey where subjects are solicited by email, or as a web-based poll. - -In either mode, an instance can be linked to an arbitrary object via the django content-types module. - -Try out the questionaire on the Unglue.it page for "Open Access Ebooks" https://unglue.it/work/82028/ - -History -------- - -The questionnaire app was originally developed by [Seantis](https://github.com/seantis), itself derived from [rmt](https://github.com/rmt). Eldest Daughter picked up the project and named it [ED-questionnaire](git://github.com/eldest-daughter/ed-questionnaire) because they had been using it and the Seantis version had entered a steady state of development. There are several feature changes they wanted and decided to head up the maintenance themselves. - -The old versions are tagged as follows: - - * tag 1.0 - state of last commit by the original developer (rmt) - * tag 1.1 - contains merged changes by other forks improving the original - * tag 2.0 - original updated trunk from Seantis version - * tag 2.5 - contains the original Seantis version and all PRs merged in as of 12/09/15. It's considered to be the backwards compatible version of the repository. - -The "ED-questionnaire" version was dubbed v3.0. It is not compatible with the v2.x branches. - -The "FEF-questionnaire" was created to add the ability to link the questionnaire to individual books in a book database. We'll call this v4.0 - -About this Manual ------------------ - -FEF Questionnaire is not a very well documented app so far to say the least. This manual should give you a general idea of the layout and concepts of it, but it is not as comprehensive as it should be. - -What it does cover is the following: - - * **Integration** lays out the steps needed to create a new Django app together with the questionnaire. The same steps can be used to integrate the questionnaire into an existing site (though you would be entering unpaved ways). - * **Concepts** talks about the data model and the design of the application. - * **Migration** explains how a questionnaire defined with 1.0 can be used in 2.0. - * **2.0 Postmortem** talks about some experiences made during the development of 2.0. - -Integration ------------ - -This part of the docs will take you through the steps needed to create a questionnaire app from scratch. It should also be quite handy for the task of integrating the questionnaire into an existing site. - -First, create a folder for your new site: - - mkdir site - cd site - -Create a virtual environment so your python packages don't influence your system - - virtualenv --no-site-packages -p python2.5 . - -Activate your virtual environment - - source bin/activate - -Install Django - - pip install django - -Create your Django site - - django-admin.py startproject mysite - -Create a place for the questionnare - - cd mysite - mkdir apps - cd apps - -Clone the questionnaire source - - git clone git://github.com/EbookFoundation/fef-questionnaire.git - -You should now have a ed-questionnaire folder in your apps folder - - cd fef-questionnaire - -The next step is to install the questionnaire. - - python setup.py install - -If you are working with ed-questionnaire from your own fork you may want to use `python setup.py develop` instead, which will save you from running `python setup.py install` every time the questionnaire changes. - -Now let's configure your basic questionnaire. - -First, you want to setup the languages used in your questionnaire. Open up your `mysite` folder in your favorite text editor. - -Open `mysite/mysite/settings.py` and add following lines, representing your languages of choice: - - LANGUAGES = ( - ('en', 'English'), - ('de', 'Deutsch') - ) - -At the top of `settings.py` you should at this point add: - - import os.path - -We will use that below for the setup of the folders. - -In the same file add the questionnaire static directory to your STATICFILES_DIRS: - - STATICFILES_DIRS = ( - os.path.abspath('./apps/fef-questionnaire/questionnaire/static/'), - ) - -Also add the locale and request cache middleware to MIDDLEWARE_CLASSES: - - 'django.middleware.locale.LocaleMiddleware', - 'questionnaire.request_cache.RequestCacheMiddleware', - -If you are using Django 1.7 you will need to comment out the following line, like so: - # 'django.middleware.security.SecurityMiddleware', -otherwise you will get an error when trying to start the server. - -Add the questionnaire template directory as well as your own to TEMPLATE_DIRS: - - os.path.abspath('./apps/fef-questionnaire/questionnaire/templates'), - os.path.abspath('./templates'), - -And finally, add `transmeta`, `questionnaire` to your INSTALLED_APPS: - - 'django.contrib.sites', - 'transmeta', - 'questionnaire', - 'questionnaire.page', - -To get the "sites" framework working you also need to add the following setting: - - SITE_ID = 1 - -Next up we want to edit the `urls.py` file of your project to link the questionnaire views to your site's url configuration. - -For an empty site with enabled admin interface you add: - - from django.conf.urls import patterns, include, url - - from django.contrib import admin - admin.autodiscover() - - urlpatterns = patterns('', - url(r'^admin/', include(admin.site.urls)), - - # questionnaire urls - url(r'q/', include('questionnaire.urls')), - ) - -Having done that we can initialize our database. (For this to work you must have setup your DATABASES in `settings.py`.). First, in your CLI navigate back to the `mysite` folder: - - cd ../.. - -The check that you are in the proper folder, type `ls`: if you can see `manage.py` in your list of files, you are good. Otherwise, find your way to the folder that contains that file. Then type: - - python manage.py syncdb - python manage.py migrate - -The questionnaire expects a `base.html` template to be there, with certain stylesheets and blocks inside. Have a look at `./apps/fef-questionnaire/example/templates/base.html`. - -For now you might want to just copy the `base.html` to your own template folder. - - mkdir templates - cd templates - cp ../apps/fef-questionnaire/example/templates/base.html . - -Congratulations, you have setup the basics of the questionnaire! At this point this site doesn't really do anything, as there are no questionnaires defined. - -To see an example questionnaire you can do the following (Note: this will only work if you have both English and German defined as Languages in `settings.py`): - - python manage.py loaddata ./apps/fef-questionnaire/example/fixtures/initial_data.yaml - -You may then start your development server: - - python manage.py runserver - -And navigate to [localhost:8000](http://localhost:8000/). - -Concepts --------- - -The ED Questionnaire has the following tables, described in detail below. - - * Subject - * RunInfo - * RunInfoHistory - * Question - * Choice - * QuestionSet - * Questionnaire - * Answer - * Landing - -### Subject - -A subject is someone filling out a questionnaire. - -Subjects are primarily useful in a study where the participants answer a questionnaire repeatedly. In this case a subject may be entered. Whoever is conducting the study (i.e. the person running the questionnaire app), may then periodically send emails inviting the subjects to fill out the questionnaire. - -Sending Emails is covered in detail later. - -Of course, not every questionnaire is part of a study. Sometimes you just want to find out what people regard as more awesome: pirates or ninjas*? - -*(it's pirates!) - -Though a poll would be a better choice for this example, one can find the answer to that question with ED Questionnaire by using an anonymous subject. The next chapter *Questionnaire* will talk about that in more detail. - -### RunInfo - -A runinfo refers to the currently active run of a subject. - -A subject who is presently taking a questionnaire is considered to be on a run. The runinfo refers to that run and carries information about it. - -The most important information associated with a runinfo is the subject, a random value that is used to generate the unique url to the questionnaire, the result of already answered questions and the progress. - -Once a run is over it is deleted with some information being carried over to the RunInfoHistory. - -Runs can be tagged by any number of comma separated tags. If tags are used, questions can be made to only show up if the given tag is part of the RunInfo. - -### RunInfoHistory - -The runinfo history is used to refer to a set of answers. - -### Question - -A question is anything you want to ask a subject. There are a number of different types you can use: - - * **choice-yesno** - Yes or No - * **choice-yesnocomment** - Yes or No with a chance to comment on the answer - * **choice-yesnodontknow** - Yes or No or Whaaa? - * **open** - A simple one line input box - * **open-textfield** - A box for lengthy answers - * **choice** - A list of choices to choose from - * **choice-freeform** - A list of choices with a chance to enter something else - * **choice-multiple** - A list of choices with multiple answers - * **choice-multiple-freeform** - Multiple Answers with multiple user defined answers - * **range** - A range of number from which one number can be chosen - * **number** - A number - * **timeperiod** - A timeperiod - * **custom** - Custom question using a custom template - * **comment** - Not a question, but only a comment displayed to the user - * **sameas** - Same type as another question - -*Some of these types, depend on checks or choices. The number question for instance can be controlled by setting the checks to something like `range=1-100 step=1`. The range question may also use the before-mentioned checks and also `unit=%`. Other questions like the choice-multiple-freeform need a `extracount=10` if ten extra options should be given to the user. - -I would love to go into all the details here but time I have not so I my only choice is to kindly refer you to the qprocessor submodule which handles all the question types.* - -Next up is the question number. The question number defines the order of questions alphanumerically as long as a number of questions are shown on the same page. The number is also used to refer to the question. - -The text of the question is what the user will be asked. There can be one text for each language defined in the `settings.py` file. - -The extra is an additional piece of information shown to the user. As of yet not all questions support this, but most do. - -An important aspect of questions (and their parents, QuestionSets) is the checks field. The checks field does a lot of things (possibly too many), the most important of which is to define if a certain question or questionset should be shown to the current subject. - -The most important checks on the question are the following: - - * **required** A required question must be answered by the user - * **requiredif="number,answer"** Means that the question is required if the question with *number* is equal to *answer*. - * **shownif** Same as requiredif, but defining if the question is shown at all. - * **maleonly** Only shown to male subjects - * **femaleonly** Only shown to female subjects - * **iftag="tag"** Question is only shown if the given tag is in the RunInfo - -Checks allow for simple boolean expressions like this: -`iftag="foo or bar"` or `requiredif="1,yes and 2,no"` - -### Choice - -A choice is a possible value for a multiple choice question. - -### QuestionSet - -A number of questions together form a questionset. A questionset is ultimately single page of questions. Questions in the same questionset are shown on the same page. - -QuestionSets also have checks, with the same options as Questions. There's only one difference, **required** and **requiredif** don't do anything. - -A questionset which contains no visible questions (as defined by **shownif**) is skipped. - -### Answer - -Contains the answer to a question. The value of the answer is stored as JSON. - -### Questionnaire - -A questionnaire is a group of questionsets together. - -### Landing - -In Poll mode, the landing url links a Questionnaire to an Object and a User to a Subject. - -Migration of 1.x to 2.0 ------------------------ - -2.0 added new fields to the questionnaire, but it did so in a backwards compatible way. None of the new fields are mandatory and no changes should be necessary to your existing questionnaire. Since we do not have any relevant testing data however, you might find yourself on your own if it doesn't work. Please file an issue if you think we did something wrong, so we can fix it and help you. - -As Django per default does not provide a way to migrate database schemas, we pretty much make use of the bulldozer way of migrating, by exporting the data from one database and import it into a newly created one. - -From you existing 1.x site do: - - python manage.py dumpdata >> export.yaml - -Copy your file to your new site and in your new site, create your empty database: - - python manage.py syncdb - -You may then import your data from your old site, which should probably work :) - - python manage.py loaddata export.yaml - -This of course covers only the data migration. How to migrate your custom tailored site to use questionnaire 2.0 is unfortunately something we cannot really document. - -2.0 Postmortem --------------- - -2.0 was the result of the work we put into Seantis questionnaire for our second project with it. We did this project without the help of the questionnaire's creator and were pretty much on our own during that time. - -Here's what we think we learned: - -### ED.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. - -If it was a library you could just work with a nice API, which does not exist. - -### Don't Go Crazy with Your Checks - -We used a fair amount of checks in both questionset and questions to control a complex questionnaire. We offloaded the complexity of the questionnaire into an Excel file defined by the customer and generated checks to copy that complexity into our application. - -Though this approach certainly works fine it does not give you a good performance. The problem is, if you have hundreds of questions controlled by runinfo tags, that you end up with most CPU cycles spent on calculating the progress bar on each request. It is precisely for that reason that we implemented the QUESTIONNAIRE_PROGRESS setting (you can learn more about that by looking at the example settings.py). - -We managed to keep our rendering time low by doing the progress bar using AJAX after a page was rendered. It is only a workaround though. Calculating the progress of a run in a huge questionnaire remains a heavy operation, so for really huge questionnaires one might consider removing the progress bar altogether. There is still some optimization to be made, but it essentially will remain the slowest part of the questionnaire, because at the end of the day interpreting loads of checks is not something you can do in a fast way, unless your name is PyPy and your programmers are insanely talented. - -### There are not Enough Tests - -There are a few that do some simple testing, but more are needed. More tests would also mean that more refactoring could be done which would be nice, because there certainly is a need for some refactoring. - -### The Admin Interface is not Good Enough - -Django admin is a nice feature to have, but we either don't leverage it well enough, or it is not the right tool for the questionnaire. In any case, if you are expecting your customer to work with the questionnaire's structure you might have to write your own admin interface. The current one is not good enough. - -4.0 Changes --------------- -Version 4.0 has not been tested for compatibility with previous versions. - -* Broken back links have been fixed. The application works in session mode and non-session mode. -* We've updated to Bootstrap 3.3.6 and implemented label tags for accessibility -* "landings" have been added so that survey responses can be linked to arbitrary models in an application. template tags have been added that allow questions and answers to refer to those models. -* question types have been added so that choices can be offered without making the question required. -* styling of required questions has been spiffed up - - - diff --git a/questionnaire/admin.py b/questionnaire/admin.py deleted file mode 100644 index c82ec5cc..00000000 --- a/questionnaire/admin.py +++ /dev/null @@ -1,97 +0,0 @@ -from django.utils.translation import ugettext as _ -from django.contrib import admin -from django.core.urlresolvers import reverse -from .models import (Choice, Questionnaire, Question, QuestionSet, Subject, - RunInfo, RunInfoHistory, Answer, DBStylesheet, Landing) - -adminsite = admin.site - - -class SubjectAdmin(admin.ModelAdmin): - search_fields = ['surname', 'givenname', 'email'] - list_display = ['surname', 'givenname', 'email'] - - -class ChoiceAdmin(admin.ModelAdmin): - list_display = ['sortid', 'text', 'value', 'question'] - - -class ChoiceInline(admin.TabularInline): - ordering = ['sortid'] - model = Choice - extra = 5 - - -class QuestionSetAdmin(admin.ModelAdmin): - ordering = ['questionnaire', 'sortid', ] - list_filter = ['questionnaire', ] - list_display = ['questionnaire', 'heading', 'sortid', ] - list_editable = ['sortid', ] - - -class QuestionAdmin(admin.ModelAdmin): - ordering = ['questionset__questionnaire', 'questionset', 'sort_id', 'number'] - inlines = [ChoiceInline] - list_filter = ['questionset__questionnaire'] - - def changelist_view(self, request, extra_context=None): - "Hack to have Questionnaire list accessible for custom changelist template" - if not extra_context: - extra_context = {} - - questionnaire_id = request.GET.get('questionset__questionnaire__id__exact', None) - if questionnaire_id: - args = {"id": questionnaire_id} - else: - args = {} - extra_context['questionnaires'] = Questionnaire.objects.filter(**args).order_by('name') - return super(QuestionAdmin, self).changelist_view(request, extra_context) - - -class QuestionnaireAdmin(admin.ModelAdmin): - list_display = ('name', 'redirect_url', 'export') - readonly_fields = ('export',) - - def export(self, obj): - csv_url = reverse("export_csv", args=[obj.id,]) - summary_url = reverse("export_summary", args=[obj.id,]) - return '{} {}'.format( - csv_url, _("Download data"), summary_url, _("Show summary") - ) - - export.allow_tags = True - export.short_description = _('Export to CSV') - - -class RunInfoAdmin(admin.ModelAdmin): - list_display = ['random', 'run', 'subject', 'created', 'emailsent', 'lastemailerror'] - pass - - -class RunInfoHistoryAdmin(admin.ModelAdmin): - pass - - -class AnswerAdmin(admin.ModelAdmin): - search_fields = ['subject__email', 'run__id', 'question__number', 'answer'] - list_display = ['id', 'run', 'subject', 'question'] - list_filter = ['subject', 'run__id'] - ordering = [ 'id', 'subject', 'run__id', 'question', ] - -from django.contrib import admin - -# new in dj1.7 -# @admin.register(Landing) -class LandingAdmin(admin.ModelAdmin): - list_display = ('label', 'content_type', 'object_id', ) - ordering = [ 'object_id', ] - -adminsite.register(Questionnaire, QuestionnaireAdmin) -adminsite.register(Question, QuestionAdmin) -adminsite.register(QuestionSet, QuestionSetAdmin) -adminsite.register(Subject, SubjectAdmin) -adminsite.register(RunInfo, RunInfoAdmin) -adminsite.register(RunInfoHistory, RunInfoHistoryAdmin) -adminsite.register(Answer, AnswerAdmin) -adminsite.register(Landing, LandingAdmin) -adminsite.register(DBStylesheet) diff --git a/questionnaire/templates/pages/summaries.html b/questionnaire/templates/pages/summaries.html deleted file mode 100644 index e52ffa96..00000000 --- a/questionnaire/templates/pages/summaries.html +++ /dev/null @@ -1,35 +0,0 @@ -{% extends "base-questionnaire.html" %} -{% block questionnaire %} -

- Survey Results Summary -

-{% for summary in summaries %} - -

Question

-

-{{summary.1|safe}} -

-{% if summary.2 %} -

Choices

- -{% endif %} -

Free Text Answers

- - -{% endfor %} - -{% endblock %} diff --git a/questionnaire/urls.py b/questionnaire/urls.py deleted file mode 100644 index 7439038d..00000000 --- a/questionnaire/urls.py +++ /dev/null @@ -1,41 +0,0 @@ -# vim: set fileencoding=utf-8 - -from django.conf.urls import * -from .views import * -from .page.views import page, langpage - -urlpatterns = [ - url(r'^$', - questionnaire, name='questionnaire_noargs'), - url(r'^csv/(?P\d+)/$', - export_csv, name='export_csv'), - url(r'^summary/(?P\d+)/$', - export_summary, name='export_summary'), - url(r'^(?P[^/]+)/progress/$', - get_async_progress, name='progress'), - url(r'^take/(?P[0-9]+)/$', generate_run), - url(r'^$', page, {'page_to_render' : 'index'}), - url(r'^(?P.*)\.html$', page), - url(r'^(?P..)/(?P.*)\.html$', langpage), - url(r'^setlang/$', set_language), - url(r'^landing/(?P\w+)/$', SurveyView.as_view(), name="landing"), - url(r'^(?P[^/]+)/(?P[-]{0,1}\d+)/$', - questionnaire, name='questionset'), -] - -if not use_session: - urlpatterns += [ - url(r'^(?P[^/]+)/$', - questionnaire, name='questionnaire'), - url(r'^(?P[^/]+)/(?P[-]{0,1}\d+)/prev/$', - redirect_to_prev_questionnaire, - name='redirect_to_prev_questionnaire'), - ] -else: - urlpatterns += [ - url(r'^$', - questionnaire, name='questionnaire'), - url(r'^prev/$', - redirect_to_prev_questionnaire, - name='redirect_to_prev_questionnaire') - ] diff --git a/questionnaire/utils.py b/questionnaire/utils.py deleted file mode 100644 index fabb9b25..00000000 --- a/questionnaire/utils.py +++ /dev/null @@ -1,92 +0,0 @@ -#!/usr/bin/python -import codecs -import cStringIO -import csv - -from django.conf import settings -try: - use_session = settings.QUESTIONNAIRE_USE_SESSION -except AttributeError: - use_session = False - -def split_numal(val): - """Split, for example, '1a' into (1, 'a') ->>> split_numal("11a") -(11, 'a') ->>> split_numal("99") -(99, '') ->>> split_numal("a") -(0, 'a') ->>> split_numal("") -(0, '') - """ - if not val: - return 0, '' - for i in range(len(val)): - if not val[i].isdigit(): - return int(val[0:i] or '0'), val[i:] - return int(val), '' - - - -def numal_sort(a, b): - """Sort a list numeric-alphabetically - ->>> vals = "1a 1 10 10a 10b 11 2 2a z".split(" "); \\ -... vals.sort(numal_sort); \\ -... " ".join(vals) -'z 1 1a 2 2a 10 10a 10b 11' - """ - anum, astr = split_numal(a) - bnum, bstr = split_numal(b) - cmpnum = cmp(anum, bnum) - if(cmpnum == 0): - return cmp(astr.lower(), bstr.lower()) - return cmpnum - -def numal0_sort(a, b): - """ - numal_sort on the first items in the list - """ - return numal_sort(a[0], b[0]) - -def get_runid_from_request(request): - if use_session: - return request.session.get('runcode', None) - else: - return request.runinfo.run.runid - -if __name__ == "__main__": - import doctest - doctest.testmod() - -class UnicodeWriter: - """ - COPIED from http://docs.python.org/library/csv.html example: - - A CSV writer which will write rows to CSV file "f", - which is encoded in the given encoding. - """ - - def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): - # Redirect output to a queue - self.queue = cStringIO.StringIO() - self.writer = csv.writer(self.queue, dialect=dialect, **kwds) - self.stream = f - self.encoder = codecs.getincrementalencoder(encoding)() - - def writerow(self, row): - self.writer.writerow([unicode(s).encode("utf-8") for s in row]) - # Fetch UTF-8 output from the queue ... - data = self.queue.getvalue() - data = data.decode("utf-8") - # ... and reencode it into the target encoding - data = self.encoder.encode(data) - # write to the target stream - self.stream.write(data) - # empty queue - self.queue.truncate(0) - - def writerows(self, rows): - for row in rows: - self.writerow(row) diff --git a/questionnaire/views.py b/questionnaire/views.py deleted file mode 100644 index 6af14f5a..00000000 --- a/questionnaire/views.py +++ /dev/null @@ -1,1107 +0,0 @@ -#!/usr/bin/python -# vim: set fileencoding=utf-8 -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.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.views.generic.base import TemplateView -from django.db import transaction -from django.conf import settings -from datetime import datetime -from django.utils import translation -from django.utils.translation import ugettext_lazy as _ - -from . import QuestionProcessors -from . import questionnaire_start, questionset_start, questionset_done, questionnaire_done -from . import AnswerException -from . import Processors -from . import profiler -from .models import * -from .parsers import * -from .parsers import BoolNot, BoolAnd, BoolOr, Checker -from .emails import _send_email, send_emails -from .utils import numal_sort, split_numal, UnicodeWriter -from .request_cache import request_cache -from .dependency_checker import dep_check - - -try: - use_session = settings.QUESTIONNAIRE_USE_SESSION -except AttributeError: - use_session = False - -try: - debug_questionnaire = settings.QUESTIONNAIRE_DEBUG -except AttributeError: - debug_questionnaire = False - - -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 """ - - if 'runcode' in kwargs: - runcode = kwargs['runcode'] - else: - session_runcode = request.session.get('runcode', None) - if session_runcode is not None: - runcode = session_runcode - - runinfo = get_runinfo(runcode) - response = dict(progress=get_progress(runinfo)) - - cache.set('progress' + runinfo.random, response['progress']) - response = HttpResponse(json.dumps(response), - content_type='application/javascript'); - response["Cache-Control"] = "no-cache" - 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" - - # cache current questionset - qs = runinfo.questionset - - # skip questionsets that don't pass - if not questionset_satisfies_checks(runinfo.questionset, runinfo): - - next = runinfo.questionset.next() - - while next and not questionset_satisfies_checks(next, runinfo): - next = next.next() - - runinfo.questionset = next - runinfo.save() - - hasquestionset = bool(next) - else: - hasquestionset = True - - # empty ? - if not hasquestionset: - logging.warn('no questionset in questionnaire which passes the check') - return finish_questionnaire(request, runinfo, qs.questionnaire) - - if not use_session: - args = [runinfo.random, runinfo.questionset.sortid] - urlname = 'questionset' - else: - args = [] - request.session['qs'] = runinfo.questionset.sortid - request.session['runcode'] = runinfo.random - urlname = 'questionnaire' - - url = reverse(urlname, args=args) - return HttpResponseRedirect(url) - - -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. - """ - if use_session: - runcode = request.session.get('runcode', None) - - if runcode is not None: - runinfo = get_runinfo(runcode) - if qs: - qs = get_object_or_404(QuestionSet, sortid=qs, questionnaire=runinfo.questionset.questionnaire) - prev_qs = qs.prev() - else: - prev_qs = runinfo.questionset.prev() - - while prev_qs and not questionset_satisfies_checks(prev_qs, runinfo): - prev_qs = prev_qs.prev() - - if runinfo and prev_qs: - if use_session: - request.session['runcode'] = runinfo.random - request.session['qs'] = prev_qs.sortid - return HttpResponseRedirect(reverse('questionnaire')) - - else: - runinfo.questionset = prev_qs - runinfo.save() - return HttpResponseRedirect(reverse('questionnaire', args=[runinfo.random])) - - return HttpResponseRedirect('/') - -@commit_on_success -def questionnaire(request, runcode=None, qs=None): - """ - Process submit answers (if present) and redirect to next page - - If this is a POST request, parse the submitted data in order to store - all the submitted answers. Then return to the next questionset or - return a completed response. - - If this isn't a POST request, redirect to the main page. - - We only commit on success, to maintain consistency. We also specifically - rollback if there were errors processing the answers for this questionset. - """ - if use_session: - session_runcode = request.session.get('runcode', None) - if session_runcode is not None: - runcode = session_runcode - - session_qs = request.session.get('qs', None) - if session_qs is not None: - qs = session_qs - - # if runcode provided as query string, redirect to the proper page - if not runcode: - runcode = request.GET.get('runcode') - if not runcode: - return HttpResponseRedirect("/") - else: - if not use_session: - args = [runcode, ] - else: - request.session['runcode'] = runcode - args = [] - - return HttpResponseRedirect(reverse("questionnaire", args=args)) - - - runinfo = get_runinfo(runcode) - - if not runinfo: - commit() - return HttpResponseRedirect('/') - - if runinfo.questionset.questionnaire.admin_access_only == 1: - if not request.user.is_superuser: - return HttpResponseRedirect(runinfo.questionset.questionnaire.redirect_url) - - # let the runinfo have a piggy back ride on the request - # so we can easily use the runinfo in places like the question processor - # without passing it around - request.runinfo = runinfo - - if not qs: - # Only change the language to the subjects choice for the initial - # questionnaire page (may be a direct link from an email) - if hasattr(request, 'session'): - request.session['django_language'] = runinfo.subject.language - translation.activate(runinfo.subject.language) - - if 'lang' in request.GET: - return set_language(request, runinfo, request.path) - - # -------------------------------- - # --- Handle non-POST requests --- - # -------------------------------- - - if request.method != "POST": - if qs is not None: - qs = get_object_or_404(QuestionSet, sortid=qs, questionnaire=runinfo.questionset.questionnaire) - if runinfo.random.startswith('test:'): - pass # ok for testing - elif qs.sortid > runinfo.questionset.sortid: - # you may jump back, but not forwards - return redirect_to_qs(runinfo, request) - runinfo.questionset = qs - runinfo.save() - commit() - # no questionset id in URL, so redirect to the correct URL - if qs is None: - return redirect_to_qs(runinfo, request) - questionset_start.send(sender=None, runinfo=runinfo, questionset=qs) - return show_questionnaire(request, runinfo) - - # ------------------------------------- - # --- Process POST with QuestionSet --- - # ------------------------------------- - - # 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] - if qsobj.questionnaire == runinfo.questionset.questionnaire: - if runinfo.questionset != qsobj: - runinfo.questionset = qsobj - runinfo.save() - except: - pass - - questionnaire = runinfo.questionset.questionnaire - questionset = runinfo.questionset - - - # to confirm that we have the correct answers - expected = questionset.questions() - - items = request.POST.items() - extra = {} # question_object => { "ANSWER" : "123", ... } - - # this will ensure that each question will be processed, even if we did not receive - # any fields for it. Also works to ensure the user doesn't add extra fields in - for x in expected: - items.append((u'question_%s_Trigger953' % x.number, None)) - - # generate the answer_dict for each question, and place in extra - for item in items: - key, value = item[0], item[1] - if key.startswith('question_'): - answer = key.split("_", 2) - question = get_question(answer[1], questionset) - if not question: - logging.warn("Unknown question when processing: %s" % answer[1]) - continue - extra[question] = ans = extra.get(question, {}) - if (len(answer) == 2): - ans['ANSWER'] = value - elif (len(answer) == 3): - ans[answer[2]] = value - else: - logging.warn("Poorly formed form element name: %r" % answer) - continue - extra[question] = ans - - errors = {} - for question, ans in extra.items(): - if not question_satisfies_checks(question, runinfo): - continue - if u"Trigger953" not in ans: - logging.warn("User attempted to insert extra question (or it's a bug)") - continue - try: - cd = question.getcheckdict() - # requiredif is the new way - depon = cd.get('requiredif', None) or cd.get('dependent', None) - if depon: - depparser = BooleanParser(dep_check, runinfo, extra) - if not depparser.parse(depon): - # if check is not the same as answer, then we don't care - # about this question plus we should delete it from the DB - delete_answer(question, runinfo.subject, runinfo.run) - if cd.get('store', False): - runinfo.set_cookie(question.number, None) - continue - add_answer(runinfo, question, ans) - if cd.get('store', False): - runinfo.set_cookie(question.number, ans['ANSWER']) - except AnswerException, e: - errors[question.number] = e - except Exception: - logging.exception("Unexpected Exception") - rollback() - raise - - if len(errors) > 0: - res = show_questionnaire(request, runinfo, errors=errors) - rollback() - return res - - questionset_done.send(sender=None, runinfo=runinfo, questionset=questionset) - - next = questionset.next() - while next and not questionset_satisfies_checks(next, runinfo): - next = next.next() - runinfo.questionset = next - runinfo.save() - if use_session: - request.session['prev_runcode'] = runinfo.random - - if next is None: # we are finished - return finish_questionnaire(request, runinfo, questionnaire) - - commit() - return redirect_to_qs(runinfo, request) - - -def finish_questionnaire(request, runinfo, questionnaire): - hist = RunInfoHistory() - hist.subject = runinfo.subject - hist.run = runinfo.run - hist.completed = datetime.now() - hist.questionnaire = questionnaire - hist.tags = runinfo.tags - hist.skipped = runinfo.skipped - hist.landing = runinfo.landing - hist.save() - - questionnaire_done.send(sender=None, runinfo=runinfo, - questionnaire=questionnaire) - lang=translation.get_language() - redirect_url = questionnaire.redirect_url - for x, y in (('$LANG', lang), - ('$SUBJECTID', runinfo.subject.id), - ('$RUNID', runinfo.run.runid),): - redirect_url = redirect_url.replace(x, str(y)) - - if runinfo.run.runid in ('12345', '54321') \ - or runinfo.run.runid.startswith('test:'): - runinfo.questionset = QuestionSet.objects.filter(questionnaire=questionnaire).order_by('sortid')[0] - runinfo.save() - else: - runinfo.delete() - commit() - if redirect_url: - return HttpResponseRedirect(redirect_url) - 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 - - Also add the javascript dependency code. - """ - - request.runinfo = runinfo - - if request.GET.get('show_all') == '1': # for debugging purposes. - questions = runinfo.questionset.questionnaire.questions() - else: - questions = runinfo.questionset.questions() - - show_all = request.GET.get('show_all') == '1' # for debugging purposes in some cases we may want to show all questions on one screen. - questionset = runinfo.questionset - questions = questionset.questionnaire.questions() if show_all else questionset.questions() - - qlist = [] - jsinclude = [] # js files to include - cssinclude = [] # css files to include - jstriggers = [] - qvalues = {} - - # initialize qvalues - cookiedict = runinfo.get_cookiedict() - - for k, v in cookiedict.items(): - qvalues[k] = v - - substitute_answer(qvalues, runinfo.questionset) - - #we make it clear to the user that we're going to sort by sort id then number, so why wasn't it doing that? - questions = sorted(questions, key=lambda question: (question.sort_id, question.number)) - - for question in questions: - # if we got here the questionset will at least contain one question - # which passes, so this is all we need to check for - question_visible = question_satisfies_checks(question, runinfo) or show_all - Type = question.get_type() - _qnum, _qalpha = split_numal(question.number) - - qdict = { - 'css_style': '' if question_visible else 'display:none;', - 'template': 'questionnaire/%s.html' % (Type), - 'qnum': _qnum, - 'qalpha': _qalpha, - 'qtype': Type, - 'qnum_class': (_qnum % 2 == 0) and " qeven" or " qodd", - 'qalpha_class': _qalpha and (ord(_qalpha[-1]) % 2 \ - and ' alodd' or ' aleven') or '', - } - - # substitute answer texts - substitute_answer(qvalues, question) - - # 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) - # 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 - if willbe_shownif: - qdict['checkstring'] = ' checks="%s"' % make_partially_evaluated_javascript_expression_for_shownif_check( - depon, runinfo, question - ) - - else: - # extra args to BooleanParser are not required for toString - parser = BooleanParser(dep_check) - qdict['checkstring'] = ' checks="%s"' % parser.toString(depon) - jstriggers.append('qc_%s' % question.number) - - footerdep = cd.get('footerif', None) - if footerdep: - parser = BooleanParser(dep_check) - qdict['footerchecks'] = ' checks="%s"' % parser.toString(footerdep) - jstriggers.append('qc_%s_footer' % question.number) - - if 'default' in cd and not question.number in cookiedict: - qvalues[question.number] = cd['default'] - if Type in QuestionProcessors: - qdict.update(QuestionProcessors[Type](request, question)) - if 'jsinclude' in qdict: - if qdict['jsinclude'] not in jsinclude: - jsinclude.extend(qdict['jsinclude']) - if 'cssinclude' in qdict: - if qdict['cssinclude'] not in cssinclude: - cssinclude.extend(qdict['jsinclude']) - if 'jstriggers' in qdict: - jstriggers.extend(qdict['jstriggers']) - if 'qvalue' in qdict and not question.number in cookiedict: - qvalues[question.number] = qdict['qvalue'] - if 'qvalues' in qdict: - # for multiple selection - for choice in qdict['qvalues']: - qvalues[choice] = 'true' - - qlist.append((question, qdict)) - - try: - has_progress = settings.QUESTIONNAIRE_PROGRESS in ('async', 'default') - async_progress = settings.QUESTIONNAIRE_PROGRESS == 'async' - except AttributeError: - has_progress = True - async_progress = False - - if has_progress: - if async_progress: - progress = cache.get('progress' + runinfo.random, 1) - else: - progress = get_progress(runinfo) - else: - progress = 0 - - if request.POST: - for k, v in request.POST.items(): - if k.startswith("question_"): - s = k.split("_") - if s[-1] == "value": # multiple checkboxes with value boxes - qvalues["_".join(s[1:])] = v - elif len(s) == 4: - qvalues[s[1] + '_' + v] = '1' # evaluates true in JS - elif len(s) == 3 and s[2] == 'comment': - qvalues[s[1] + '_' + s[2]] = v - else: - qvalues[s[1]] = v - - if use_session: - 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') - - - r = r2r("questionnaire/questionset.html", request, - questionset=runinfo.questionset, - runinfo=runinfo, - errors=errors, - qlist=qlist, - progress=progress, - triggers=jstriggers, - qvalues=qvalues, - jsinclude=jsinclude, - cssinclude=cssinclude, - async_progress=async_progress, - async_url=reverse('progress', args=[runinfo.random]), - prev_url=prev_url, - current_answers=current_answers, - ) - r['Cache-Control'] = 'no-cache' - r['Expires'] = "Thu, 24 Jan 1980 00:00:00 GMT" - 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 - redirect to the provided URL (or the last URL). - Can also be used by a url handler, w/o runinfo & next. - """ - if not next: - next = request.GET.get('next', request.POST.get('next', None)) - if not next: - next = request.META.get('HTTP_REFERER', None) - if not next: - next = '/' - response = HttpResponseRedirect(next) - response['Expires'] = "Thu, 24 Jan 1980 00:00:00 GMT" - if request.method == 'GET': - lang_code = request.GET.get('lang', None) - if lang_code and translation.check_for_language(lang_code): - if hasattr(request, 'session'): - request.session['django_language'] = lang_code - else: - response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code) - if runinfo: - runinfo.subject.language = lang_code - runinfo.subject.save() - return response - - -def _table_headers(questions): - """ - Return the header labels for a set of questions as a list of strings. - - This will create separate columns for each multiple-choice possiblity - and freeform options, to avoid mixing data types and make charting easier. - """ - ql = list(questions.order_by( - 'questionset__sortid', 'number') - ) - #ql.sort(lambda x, y: numal_sort(x.number, y.number)) - columns = [] - for q in ql: - qnum = '{}.{}'.format(q.questionset.sortid, q.number) - if q.type.startswith('choice-yesnocomment'): - columns.extend([qnum, qnum + "-freeform"]) - elif q.type.startswith('choice-freeform'): - columns.extend([qnum, qnum + "-freeform"]) - elif q.type.startswith('choice-multiple'): - cl = [c.value for c in q.choice_set.all()] - cl.sort(numal_sort) - columns.extend([qnum + '-' + value for value in cl]) - if q.type == 'choice-multiple-freeform': - columns.append(qnum + '-freeform') - else: - 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, - 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. - qid -- questionnaire_id - extra_headings -- customize the headings for extra columns, - extra_entries -- function returning a list of extra column entries, - answer_filter -- custom filter for the answers. If this is present, the filter must manage access. - filecode -- code for filename - """ - 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)) - headings, answers = answer_export(questionnaire, answer_filter=answer_filter) - - writer = UnicodeWriter(fd) - writer.writerow(extra_headings + headings) - for subject, run, answer_row in answers: - row = extra_entries(subject, run) + [ - a if a else '--' for a in answer_row] - writer.writerow(row) - fd.seek(0) - - response = HttpResponse(fd, content_type="text/csv") - response['Content-Length'] = fd.tell() - response['Content-Disposition'] = 'attachment; filename="answers-%s-%s.csv"' % (qid, filecode) - return response - - -def answer_export(questionnaire, answers=None, answer_filter=None): - """ - questionnaire -- questionnaire model for export - answers -- query set of answers to include in export, defaults to all - answer_filter -- filter for the answers - - Return a flat dump of column headings and all the answers for a - questionnaire (in query set answers) in the form (headings, answers) - where headings is: - ['question1 number', ...] - and answers is: - [(subject1, 'runid1', ['answer1.1', ...]), ... ] - - The headings list might include items with labels like - 'questionnumber-freeform'. Those columns will contain all the freeform - answers for that question (separated from the other answer data). - - Multiple choice questions will have one column for each choice with - labels like 'questionnumber-choice'. - - The items in the answers list are unicode strings or empty strings - if no answer was given. The number of elements in each answer list will - always match the number of headings. - """ - if answers is None: - answers = Answer.objects.all() - if answer_filter: - answers = answer_filter(answers) - answers = answers.filter( - question__questionset__questionnaire=questionnaire).order_by( - 'subject', 'run__runid', 'question__questionset__sortid', 'question__number') - answers = answers.select_related() - questions = Question.objects.filter( - questionset__questionnaire=questionnaire) - headings = _table_headers(questions) - - coldict = {} - for num, col in enumerate(headings): # use coldict to find column indexes - coldict[col] = num - # collect choices for each question - qchoicedict = {} - for q in questions: - qchoicedict[q.id] = [x[0] for x in q.choice_set.values_list('value')] - - runid = subject = None - out = [] - row = [] - run = None - for answer in answers: - if answer.run != run or answer.subject != subject: - if row: - out.append((subject, run, row)) - run = answer.run - subject = answer.subject - row = [""] * len(headings) - ans = answer.split_answer() - if type(ans) == int: - ans = str(ans) - for choice in ans: - col = None - qnum = '{}.{}'.format(answer.question.questionset.sortid, answer.question.number) - if type(choice) == list: - # freeform choice - choice = choice[0] - col = coldict.get(qnum + '-freeform', None) - if col is None: # look for enumerated choice column (multiple-choice) - col = coldict.get(qnum + '-' + unicode(choice), None) - if col is None: # single-choice items - if ((not qchoicedict[answer.question.id]) or - choice in qchoicedict[answer.question.id]): - col = coldict.get(qnum, None) - if col is None: # last ditch, if not found throw it in a freeform column - col = coldict.get(qnum + '-freeform', None) - if col is not None: - row[col] = choice - # and don't forget about the last one - if row: - 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): - """ - questionnaire -- questionnaire model for summary - answers -- query set of answers to include in summary, defaults to all - - Return a summary of the answer totals in answer_qs in the form: - [('q1', 'question1 text', - [('choice1', 'choice1 text', num), ...], - ['freeform1', ...]), ...] - - questions are returned in questionnaire order - choices are returned in question order - freeform options are case-insensitive sorted - """ - - if answers is None: - answers = Answer.objects.all() - if answer_filter: - answers = answer_filter(answers) - answers = answers.filter(question__questionset__questionnaire=questionnaire) - questions = Question.objects.filter( - questionset__questionnaire=questionnaire).order_by( - 'questionset__sortid', 'number') - - summary = [] - for question in questions: - q_type = question.get_type() - if q_type.startswith('choice-yesno'): - choices = [('yes', _('Yes')), ('no', _('No'))] - if 'dontknow' in q_type: - choices.append(('dontknow', _("Don't Know"))) - elif q_type.startswith('choice'): - choices = [(c.value, c.text) for c in question.choices()] - else: - choices = [] - choice_totals = dict([(k, 0) for k, v in choices]) - freeforms = [] - for a in answers.filter(question=question): - ans = a.split_answer() - for choice in ans: - if type(choice) == list: - freeforms.extend(choice) - elif choice in choice_totals: - choice_totals[choice] += 1 - else: - # be tolerant of improperly marked data - freeforms.append(choice) - freeforms.sort(numal_sort) - summary.append((question.number, question.text, [ - (n, t, choice_totals[n]) for (n, t) in choices], freeforms)) - 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={}): - """ - 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[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 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)