Merge pull request #565 from Gluejar/add-questionnaire

add the questionnaire module
pull/1/head
Raymond Yee 2016-04-29 09:57:00 -07:00
commit 574854ec5a
90 changed files with 7825 additions and 38 deletions

View File

@ -46,7 +46,6 @@ from regluit.core.lookups import (
from regluit.libraryauth.models import Library, Block, CardPattern, EmailPattern
from regluit.libraryauth.admin import LibraryAdmin, BlockAdmin, CardPatternAdmin, EmailPatternAdmin
from regluit.survey.admin import LandingAdmin, Landing
class RegluitAdmin(AdminSite):
login_template = 'registration/login.html'
@ -299,4 +298,21 @@ admin_site.register(NoticeSetting, NoticeSettingAdmin)
admin_site.register(Notice, NoticeAdmin)
admin_site.register(ObservedItem)
from regluit.questionnaire.admin import (
Questionnaire, QuestionnaireAdmin, Question, QuestionAdmin,
QuestionSet, QuestionSetAdmin, RunInfo, RunInfoAdmin, RunInfoHistory, RunInfoHistoryAdmin,
Answer, AnswerAdmin, LandingAdmin, Landing,
)
from regluit.questionnaire.admin import Subject as QSubject
from regluit.questionnaire.admin import SubjectAdmin as QSubjectAdmin
admin_site.register(Landing, LandingAdmin)
admin_site.register(Questionnaire, QuestionnaireAdmin)
admin_site.register(Question, QuestionAdmin)
admin_site.register(QuestionSet, QuestionSetAdmin)
admin_site.register(QSubject, QSubjectAdmin)
admin_site.register(RunInfo, RunInfoAdmin)
admin_site.register(RunInfoHistory, RunInfoHistoryAdmin)
admin_site.register(Answer, AnswerAdmin)

View File

@ -0,0 +1,32 @@
{% extends "base.html" %}
{% block title %}{{ block.super }}Questionnaire{% endblock title %}
{% block search_box %}{% endblock %}
{% block extra_css %}
<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>
{% endblock %}
{% block extra_head %}
{% block headextra %}
{% endblock %}
{% endblock %}
{% 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 %}
{% block content %}
<div id="main-container">
<div class="js-main">
{% block questionnaire %}{% endblock questionnaire %}
</div>
</div>
{% endblock %}

View File

@ -61,6 +61,7 @@
<div class="js-logo">
<a href="{{ landingurl }}"><img src="/static/images/logo.png" alt="unglue.it" title="unglue.it" /></a>
</div>
{% block search_box %}
{% if not suppress_search_box %}
<div class="js-search">
<div class="js-search-inner">
@ -73,6 +74,7 @@
</div>
</div>
{% endif %}
{% endblock %}
{% if user.is_authenticated %}
<div class="js-topmenu" id="authenticated">
<ul class="menu">
@ -121,13 +123,6 @@
</div>
{% block news %}
{% if not suppress_search_box %}
{% comment %}
<div class="launch_top">
Happy <a href="https://twitter.com/ebookday">#ebookday</a>! We're celebrating with a list of <a href="{% url 'cc_list'%}">Creative Commons eBooks</a>
</div>
{% endcomment %}
{% endif %}
{% endblock %}
{% block topsection %}{% endblock %}

99
questionnaire/__init__.py Normal file
View File

@ -0,0 +1,99 @@
"""
questionnaire - Django Questionnaire App
========================================
Create flexible questionnaires.
Author: Robert Thomson <git AT corporatism.org>
"""
from django.conf import settings
from django.dispatch import Signal
import imp
__all__ = ['question_proc', 'answer_proc', 'add_type', 'AnswerException',
'questionset_done', 'questionnaire_done', ]
QuestionChoices = []
QuestionProcessors = {} # supply additional information to the templates
Processors = {} # for processing answers
questionnaire_start = Signal(providing_args=["runinfo", "questionnaire"])
questionset_start = Signal(providing_args=["runinfo", "questionset"])
questionset_done = Signal(providing_args=["runinfo", "questionset"])
questionnaire_done = Signal(providing_args=["runinfo", "questionnaire"])
class AnswerException(Exception):
"""Thrown from an answer processor to generate an error message"""
pass
def question_proc(*names):
"""
Decorator to create a question processor for one or more
question types.
Usage:
@question_proc('typename1', 'typename2')
def qproc_blah(request, question):
...
"""
def decorator(func):
global QuestionProcessors
for name in names:
QuestionProcessors[name] = func
return func
return decorator
def answer_proc(*names):
"""
Decorator to create an answer processor for one or more
question types.
Usage:
@question_proc('typename1', 'typename2')
def qproc_blah(request, question):
...
"""
def decorator(func):
global Processors
for name in names:
Processors[name] = func
return func
return decorator
def add_type(id, name):
"""
Register a new question type in the admin interface.
At least an answer processor must also be defined for this
type.
Usage:
add_type('mysupertype', 'My Super Type [radio]')
"""
global QuestionChoices
QuestionChoices.append((id, name))
from . import qprocessors # make sure ours are imported first # noqa
add_type('sameas', 'Same as Another Question (put sameas=question.number in checks or sameasid=question.id)')
for app in settings.INSTALLED_APPS:
try:
app_path = __import__(app, {}, {}, [app.split('.')[-1]]).__path__
except AttributeError:
continue
try:
imp.find_module('qprocessors', app_path)
except ImportError:
continue
__import__("%s.qprocessors" % app)

93
questionnaire/admin.py Normal file
View File

@ -0,0 +1,93 @@
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,])
return '<a href="%s">%s</a>' % (csv_url, _("Download data"))
export.allow_tags = True
export.short_description = _('Export to CSV')
class RunInfoAdmin(admin.ModelAdmin):
list_display = ['random', 'runid', 'subject', 'created', 'emailsent', 'lastemailerror']
pass
class RunInfoHistoryAdmin(admin.ModelAdmin):
pass
class AnswerAdmin(admin.ModelAdmin):
search_fields = ['subject__email', 'runid', 'question__number', 'answer']
list_display = ['id', 'runid', 'subject', 'question']
list_filter = ['subject', 'runid']
ordering = [ 'id', 'subject', 'runid', 'question', ]
from django.contrib import admin
# new in dj1.7
# @admin.register(Landing)
class LandingAdmin(admin.ModelAdmin):
pass
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)

View File

@ -0,0 +1,23 @@
from django.conf import settings
def load_settings(request):
"""
A template context processor that adds the Questionnaire
Settings to the template context.
"""
try:
use_session = settings.QUESTIONNAIRE_USE_SESSION
except AttributeError:
use_session = False
try:
debug_questionnaire = settings.QUESTIONNAIRE_DEBUG
except AttributeError:
debug_questionnaire = False
context = {
'debug_questionnaire': debug_questionnaire,
'use_session': use_session
}
return context

View File

@ -0,0 +1,147 @@
from .models import Question, Answer
import logging
def explode_answer_into_list(answers):
answer_list = []
for answer in answers:
if type(answer) == type(list()):
string_list = filter(lambda v: not isinstance(v, int), answer)
# remove all number values from list - this means choice-multiple-values's values
answer_list.extend(string_list)
else:
answer_list.append(answer)
return answer_list
def check_actual_answers_against_expression(check_answer, actual_answer, check_question):
# Numeric Value Expressions
if check_answer[0:1] in "<>":
try:
actual_answer = float(actual_answer)
if check_answer[1:2] == "=":
check_value = float(check_answer[2:])
else:
check_value = float(check_answer[1:])
except:
logging.error("ERROR: must use numeric values with < <= => > checks (%r)" % check_question)
return False
if check_answer.startswith("<="):
return actual_answer <= check_value
if check_answer.startswith(">="):
return actual_answer >= check_value
if check_answer.startswith("<"):
return actual_answer < check_value
if check_answer.startswith(">"):
return actual_answer > check_value
# Convert answer to list if not already one
if type(actual_answer) != type(list()):
actual_answer = [actual_answer]
# Negative Value Expressions
if check_answer.startswith("!"):
if len(actual_answer) == 0:
return False
for actual_answer in actual_answer:
if actual_answer == '':
return False
if check_answer[1:].strip() == actual_answer.strip():
return False
return True
# Positive Value Expressions
for actual_answer in actual_answer:
if check_answer.strip() == actual_answer.strip():
return True
return False
def dep_check(expr, runinfo, answerdict):
"""
Given a comma separated question number and expression, determine if the
provided answer to the question number satisfies the expression.
If the expression starts with >, >=, <, or <=, compare the rest of
the expression numerically and return False if it's not able to be
converted to an integer.
If the expression starts with !, return true if the rest of the expression
does not match the answer.
Otherwise return true if the expression matches the answer.
If there is no comma and only a question number, it checks if the answer
is "yes"
When looking up the answer, it first checks if it's in the answerdict,
then it checks runinfo's cookies, then it does a database lookup to find
the answer.
The use of the comma separator is purely historical.
"""
if hasattr(runinfo, 'questionset'):
questionnaire = runinfo.questionset.questionnaire
elif hasattr(runinfo, 'questionnaire'):
questionnaire = runinfo.questionnaire
else:
assert False
# Parse expression
if "," not in expr:
expr += ",yes"
check_question_number, check_answer = expr.split(",", 1)
# Get question to check
try:
check_question = Question.objects.get(number=check_question_number,
questionset__questionnaire=questionnaire)
except Question.DoesNotExist:
return False
# Parse & load actual answer(s) from user
if check_question in answerdict:
# test for membership in multiple choice questions
# FIXME: only checking answerdict
for k, v in answerdict[check_question].items():
if not k.startswith('multiple_'):
continue
if check_answer.startswith("!"):
if check_answer[1:].strip() == v.strip():
return False
elif check_answer.strip() == v.strip():
return True
actual_answer = answerdict[check_question].get('ANSWER', '')
elif hasattr(runinfo, 'get_cookie') and runinfo.get_cookie(check_question_number, False):
actual_answer = runinfo.get_cookie(check_question_number)
else:
# retrieve from database
answer_object = Answer.objects.filter(question=check_question,
runid=runinfo.runid,
subject=runinfo.subject)
if answer_object:
actual_answer = answer_object[0].split_answer()
logging.warn("Put `store` in checks field for question %s" % check_question_number)
else:
actual_answer = None
if not actual_answer:
if check_question.getcheckdict():
actual_answer = check_question.getcheckdict().get('default')
if actual_answer is None:
actual_answer = u''
# Convert freeform question type answers from a list of lists to just a single list.
# FIXME: Figure out why the hell freeform questions store their values as lists within lists.
#Don't shrink to one list if it isn't even a list though, otherwise we could get a string
#becoming a character array and falsely failing the comparison.
answer_list = actual_answer #might not actually be a list but meh
if type(actual_answer) == type(list()):
answer_list = explode_answer_into_list(actual_answer)
return check_actual_answers_against_expression(check_answer, answer_list, check_question)

161
questionnaire/emails.py Normal file
View File

@ -0,0 +1,161 @@
# -*- coding: utf-8
"""
Functions to send email reminders to users.
"""
import random, time, smtplib, rfc822
from datetime import datetime
from email.Header import Header
from email.Utils import formataddr, parseaddr
from django.core.mail import get_connection, EmailMessage
from django.contrib.auth.decorators import login_required
from django.template import Context, loader
from django.utils import translation
from django.conf import settings
from django.http import Http404, HttpResponse
from django.shortcuts import render_to_response, get_object_or_404
from .models import Subject, QuestionSet, RunInfo, Questionnaire
try: from hashlib import md5
except: from md5 import md5
def encode_emailaddress(address):
"""
Encode an email address as ASCII using the Encoded-Word standard.
Needed to work around http://code.djangoproject.com/ticket/11144
"""
try: return address.encode('ascii')
except UnicodeEncodeError: pass
nm, addr = parseaddr(address)
return formataddr( (str(Header(nm, settings.DEFAULT_CHARSET)), addr) )
def _new_random(subject):
"""
Create a short unique randomized string.
Returns: subject_id + 'z' +
md5 hexdigest of subject's surname, nextrun date, and a random number
"""
return "%dz%s" % (subject.id, md5(subject.surname + str(subject.nextrun) + hex(random.randint(1,999999))).hexdigest()[:6])
def _new_runinfo(subject, questionset):
"""
Create a new RunInfo entry with a random code
If a unique subject+runid entry already exists, return that instead..
That should only occurs with manual database changes
"""
nextrun = subject.nextrun
runid = str(nextrun.year)
entries = list(RunInfo.objects.filter(runid=runid, subject=subject))
if len(entries)>0:
r = entries[0]
else:
r = RunInfo()
r.random = _new_random(subject)
r.subject = subject
r.runid = runid
r.emailcount = 0
r.created = datetime.now()
r.questionset = questionset
r.save()
if nextrun.month == 2 and nextrun.day == 29: # the only exception?
subject.nextrun = datetime(nextrun.year + 1, 2, 28)
else:
subject.nextrun = datetime(nextrun.year + 1, nextrun.month, nextrun.day)
subject.save()
return r
def _send_email(runinfo):
"Send the email for a specific runinfo entry"
subject = runinfo.subject
translation.activate(subject.language)
tmpl = loader.get_template(settings.QUESTIONNAIRE_EMAIL_TEMPLATE)
c = Context()
c['surname'] = subject.surname
c['givenname'] = subject.givenname
c['gender'] = subject.gender
c['email'] = subject.email
c['random'] = runinfo.random
c['runid'] = runinfo.runid
c['created'] = runinfo.created
c['site'] = getattr(settings, 'QUESTIONNAIRE_URL', '(settings.QUESTIONNAIRE_URL not set)')
email = tmpl.render(c)
emailFrom = settings.QUESTIONNAIRE_EMAIL_FROM
emailSubject, email = email.split("\n",1) # subject must be on first line
emailSubject = emailSubject.strip()
emailFrom = emailFrom.replace("$RUNINFO", runinfo.random)
emailTo = '"%s, %s" <%s>' % (subject.surname, subject.givenname, subject.email)
emailTo = encode_emailaddress(emailTo)
emailFrom = encode_emailaddress(emailFrom)
try:
conn = get_connection()
msg = EmailMessage(emailSubject, email, emailFrom, [ emailTo ],
connection=conn)
msg.send()
runinfo.emailcount = 1 + runinfo.emailcount
runinfo.emailsent = datetime.now()
runinfo.lastemailerror = "OK, accepted by server"
runinfo.save()
return True
except smtplib.SMTPRecipientsRefused:
runinfo.lastemailerror = "SMTP Recipient Refused"
except smtplib.SMTPHeloError:
runinfo.lastemailerror = "SMTP Helo Error"
except smtplib.SMTPSenderRefused:
runinfo.lastemailerror = "SMTP Sender Refused"
except smtplib.SMTPDataError:
runinfo.lastemailerror = "SMTP Data Error"
runinfo.save()
return False
def send_emails(request=None, qname=None):
"""
1. Create a runinfo entry for each subject who is due and has state 'active'
2. Send an email for each runinfo entry whose subject receives email,
providing that the last sent email was sent more than a week ago.
This can be called either by "./manage.py questionnaire_emails" (without
request) or through the web, if settings.EMAILCODE is set and matches.
"""
if request and request.GET.get('code') != getattr(settings,'EMAILCODE', False):
raise Http404
if not qname:
qname = getattr(settings, 'QUESTIONNAIRE_DEFAULT', None)
if not qname:
raise Exception("QUESTIONNAIRE_DEFAULT not in settings")
questionnaire = Questionnaire.objects.get(name=qname)
questionset = QuestionSet.objects.filter(questionnaire__name=qname).order_by('sortid')
if not questionset:
raise Exception("No questionsets for questionnaire '%s' (in settings.py)" % qname)
return
questionset = questionset[0]
viablesubjects = Subject.objects.filter(nextrun__lte = datetime.now(), state='active')
for s in viablesubjects:
r = _new_runinfo(s, questionset)
runinfos = RunInfo.objects.filter(subject__formtype='email', questionset__questionnaire=questionnaire)
WEEKAGO = time.time() - (60 * 60 * 24 * 7) # one week ago
outlog = []
for r in runinfos:
if r.runid.startswith('test:'):
continue
if r.emailcount == -1:
continue
if r.emailcount == 0 or time.mktime(r.emailsent.timetuple()) < WEEKAGO:
try:
if _send_email(r):
outlog.append(u"[%s] %s, %s: OK" % (r.runid, r.subject.surname, r.subject.givenname))
else:
outlog.append(u"[%s] %s, %s: %s" % (r.runid, r.subject.surname, r.subject.givenname, r.lastemailerror))
except Exception, e:
outlog.append("Exception: [%s] %s: %s" % (r.runid, r.subject.surname, str(e)))
if request:
return HttpResponse("Sent Questionnaire Emails:\n "
+"\n ".join(outlog), content_type="text/plain")
return "\n".join(outlog)

View File

@ -0,0 +1,512 @@
[
{
"fields": {
"anonymous": false,
"email": "test@example.com",
"formtype": "email",
"gender": "male",
"givenname": "TestGivenname",
"ip_address": null,
"language": "de",
"nextrun": "2009-05-15",
"state": "active",
"surname": "TestSurname"
},
"model": "questionnaire.subject",
"pk": 1
},
{
"fields": {
"admin_access_only": false,
"html": "survey html here",
"name": "MappingSurvey",
"parse_html": false,
"redirect_url": ""
},
"model": "questionnaire.questionnaire",
"pk": 3
},
{
"fields": {
"content_type": 22,
"label": "Open Book Publishers",
"nonce": "xxxxxx",
"object_id": 81834,
"questionnaire": 3
},
"model": "questionnaire.landing",
"pk": 1234
},
{
"fields": {
"checks": "",
"heading": "Open Access Ebooks (Part 1)",
"parse_html": true,
"questionnaire": 3,
"sortid": 1,
"text_en": " <h1> Introduction </h1> \r\n <p> \r\nWelcome, reader of <i>{{ landing_object.title }}</i>! And thanks for visiting Unglue.it to help us out with this \u2026\r\n </p> \r\n <p> \r\nAs Open Access publishers, {{ landing_object.last_campaign.publisher }} 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 9 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 surreptitiously drop cookies on you to carry around for us, 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."
},
"model": "questionnaire.questionset",
"pk": 5
},
{
"fields": {
"checks": "",
"heading": "Now About You...",
"parse_html": true,
"questionnaire": 3,
"sortid": 2,
"text_en": " <p> And now, three 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.last_campaign.publisher }} and Unglue.it </p> \r\n"
},
"model": "questionnaire.questionset",
"pk": 7
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "1",
"parse_html": true,
"questionset": 5,
"sort_id": 1,
"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 (we don\u2019t need to know much more about that!)? - or in some other way?\r\n",
"type": "open"
},
"model": "questionnaire.question",
"pk": 16
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "2",
"parse_html": false,
"questionset": 5,
"sort_id": 2,
"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? ",
"type": "open"
},
"model": "questionnaire.question",
"pk": 17
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "3",
"parse_html": true,
"questionset": 5,
"sort_id": 3,
"text_en": "Why are you interested in this book?",
"type": "choice-multiple-freeform"
},
"model": "questionnaire.question",
"pk": 18
},
{
"fields": {
"checks": "",
"extra_en": "If Yes - why are you using this edition and not one of the other ones?",
"footer_en": "",
"number": "4",
"parse_html": false,
"questionset": 5,
"sort_id": 4,
"text_en": "Are you aware that this title is available in multiple different digital and printed formats?",
"type": "choice-yesnocomment"
},
"model": "questionnaire.question",
"pk": 19
},
{
"fields": {
"checks": "",
"extra_en": "Please tell us in more detail:",
"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_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_en": "",
"footer_en": "",
"number": "1",
"parse_html": false,
"questionset": 6,
"sort_id": null,
"text_en": "Where do you live?",
"type": "choice-freeform"
},
"model": "questionnaire.question",
"pk": 21
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "2",
"parse_html": false,
"questionset": 6,
"sort_id": null,
"text_en": "What do you do for a living?",
"type": "open"
},
"model": "questionnaire.question",
"pk": 22
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "\r\n\r\n \r\n\r\n",
"number": "3",
"parse_html": false,
"questionset": 6,
"sort_id": null,
"text_en": "When did you finish your formal education?",
"type": "choice-freeform"
},
"model": "questionnaire.question",
"pk": 23
},
{
"fields": {
"checks": "required-no",
"extra_en": "",
"footer_en": "",
"number": "4",
"parse_html": false,
"questionset": 6,
"sort_id": null,
"text_en": "Is there anything else you would like to tell us, or think we should know?",
"type": "open-textfield"
},
"model": "questionnaire.question",
"pk": 24
},
{
"fields": {
"checks": "",
"extra_en": "",
"footer_en": "",
"number": "1",
"parse_html": false,
"questionset": 7,
"sort_id": null,
"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": {
"question": 18,
"sortid": 1,
"tags": "",
"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_en": "For my job - it relates to what I do ",
"value": "job"
},
"model": "questionnaire.choice",
"pk": 18
},
{
"fields": {
"question": 18,
"sortid": 3,
"tags": "",
"text_en": "I need to read it for a course",
"value": "course"
},
"model": "questionnaire.choice",
"pk": 19
},
{
"fields": {
"question": 20,
"sortid": 1,
"tags": "",
"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_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_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_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_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_en": "Share it with my friends ",
"value": "share"
},
"model": "questionnaire.choice",
"pk": 25
},
{
"fields": {
"question": 20,
"sortid": 7,
"tags": "",
"text_en": "Print it out and ceremoniously burn it!",
"value": "print"
},
"model": "questionnaire.choice",
"pk": 26
},
{
"fields": {
"question": 20,
"sortid": 8,
"tags": "",
"text_en": "I\u2019m creating/collating a (online) library",
"value": "catalog"
},
"model": "questionnaire.choice",
"pk": 27
},
{
"fields": {
"question": 20,
"sortid": 9,
"tags": "",
"text_en": "Something else entirely \u2026. ",
"value": "else"
},
"model": "questionnaire.choice",
"pk": 28
},
{
"fields": {
"question": 21,
"sortid": 1,
"tags": "",
"text_en": "USA/Canada",
"value": "us"
},
"model": "questionnaire.choice",
"pk": 29
},
{
"fields": {
"question": 21,
"sortid": 2,
"tags": "",
"text_en": "Europe",
"value": "eu"
},
"model": "questionnaire.choice",
"pk": 30
},
{
"fields": {
"question": 21,
"sortid": 3,
"tags": "",
"text_en": "South America",
"value": "sa"
},
"model": "questionnaire.choice",
"pk": 31
},
{
"fields": {
"question": 21,
"sortid": 4,
"tags": "",
"text_en": "Central America/ Caribbean",
"value": "ca"
},
"model": "questionnaire.choice",
"pk": 32
},
{
"fields": {
"question": 21,
"sortid": 5,
"tags": "",
"text_en": "Asia",
"value": "as"
},
"model": "questionnaire.choice",
"pk": 33
},
{
"fields": {
"question": 21,
"sortid": 6,
"tags": "",
"text_en": "Africa",
"value": "af"
},
"model": "questionnaire.choice",
"pk": 34
},
{
"fields": {
"question": 21,
"sortid": 7,
"tags": "",
"text_en": "Middle East",
"value": "me"
},
"model": "questionnaire.choice",
"pk": 35
},
{
"fields": {
"question": 21,
"sortid": 8,
"tags": "",
"text_en": "Another Planet",
"value": "ap"
},
"model": "questionnaire.choice",
"pk": 36
},
{
"fields": {
"question": 23,
"sortid": 1,
"tags": "",
"text_en": "I haven\u2019t - I\u2019m still a student",
"value": "x"
},
"model": "questionnaire.choice",
"pk": 37
},
{
"fields": {
"question": 23,
"sortid": 2,
"tags": "",
"text_en": "At primary school",
"value": "8"
},
"model": "questionnaire.choice",
"pk": 38
},
{
"fields": {
"question": 23,
"sortid": 3,
"tags": "",
"text_en": "At high school",
"value": "h"
},
"model": "questionnaire.choice",
"pk": 39
},
{
"fields": {
"question": 23,
"sortid": 4,
"tags": "",
"text_en": "After trade qualifications",
"value": "t"
},
"model": "questionnaire.choice",
"pk": 40
},
{
"fields": {
"question": 23,
"sortid": 5,
"tags": "",
"text_en": "At College/Undergraduate Degree ",
"value": "c"
},
"model": "questionnaire.choice",
"pk": 41
},
{
"fields": {
"question": 23,
"sortid": 6,
"tags": "",
"text_en": "At Grad School/post-graduate university",
"value": "g"
},
"model": "questionnaire.choice",
"pk": 42
}
]

View File

@ -0,0 +1,440 @@
- fields: {
state: active,
surname: TestSurname,
givenname: TestGivenname,
email: test@example.com,
gender: male,
formtype: email,
language: de,
nextrun: 2009-05-15
}
model: questionnaire.subject
pk: 1
- fields: {
name: Test,
redirect_url: /
}
model: questionnaire.questionnaire
pk: 1
- fields: {
name: TagTest,
redirect_url: /
}
model: questionnaire.questionnaire
pk: 2
- fields: {
checks: '',
heading: 001_TestQS,
questionnaire: 1,
sortid: 1,
text_de: Test Fragebogenseite 1,
text_en: Test QuestionSet 1,
}
model: questionnaire.questionset
pk: 1
- fields: {
checks: '',
heading: 002_TestQS,
questionnaire: 1,
sortid: 2,
text_de: Test Fragebogenseite 1,
text_en: Test QuestionSet 1,
}
model: questionnaire.questionset
pk: 2
- fields: {
checks: '',
heading: always,
questionnaire: 2,
sortid: 1,
text_de: always shown,
text_en: always shown,
}
model: questionnaire.questionset
pk: 3
- fields: {
checks: 'iftag=testtag',
heading: shown with testtag,
questionnaire: 2,
sortid: 2,
text_de: shown with testtag,
text_en: shown with testtag,
}
model: questionnaire.questionset
pk: 4
- fields: {
cookies: null,
created: !!timestamp '2009-05-15 10:19:03.905232',
emailcount: 0,
emailsent: !!timestamp '2009-05-16 15:05:11.775082',
lastemailerror: null,
questionset: 1,
random: 'test:test',
runid: 'test:test',
state: '',
subject: 1
}
model: questionnaire.runinfo
pk: 1
- fields: {
cookies: null,
created: !!timestamp '2009-05-15 10:19:03.905232',
emailcount: 0,
emailsent: !!timestamp '2009-05-16 15:05:11.775082',
lastemailerror: null,
questionset: 3,
random: 'test:withtags',
runid: 'test:withtags',
state: '',
tags: 'testtag',
subject: 1
}
model: questionnaire.runinfo
pk: 2
- fields: {
cookies: null,
created: !!timestamp '2009-05-15 10:19:03.905232',
emailcount: 0,
emailsent: !!timestamp '2009-05-16 15:05:11.775082',
lastemailerror: null,
questionset: 3,
random: 'test:withouttags',
runid: 'test:withouttags',
state: '',
tags: '',
subject: 1
}
model: questionnaire.runinfo
pk: 3
- fields: {
completed: 2009-05-16,
runid: 'test:test',
subject: 1,
questionnaire: 1,
}
model: questionnaire.runinfohistory
pk: 1
- fields: {
checks: '',
extra_de: '',
extra_en: '',
number: '1',
questionset: 1,
text_de: '[de] Question 1',
text_en: '[en] Question 1',
type: open
}
model: questionnaire.question
pk: 1
- fields: {
checks: '',
extra_de: '',
extra_en: '',
number: '2',
questionset: 1,
text_de: '[de] Question 2',
text_en: '[en] Question 2',
type: open-textfield
}
model: questionnaire.question
pk: 2
- fields: {
checks: '',
extra_de: '',
extra_en: '',
number: '3',
questionset: 1,
text_de: '[de] Question 3',
text_en: '[en] Question 3',
type: choice-yesno
}
model: questionnaire.question
pk: 3
- fields: {
checks: '',
extra_de: '',
extra_en: '',
number: '4',
questionset: 1,
text_de: '[de] Question 4',
text_en: '[en] Question 4',
type: choice-yesnodontknow
}
model: questionnaire.question
pk: 4
- fields: {
checks: required-yes,
extra_de: '',
extra_en: '',
number: '5',
questionset: 1,
text_de: '[de] Question 5',
text_en: '[en] Question 5',
type: choice-yesnocomment
}
model: questionnaire.question
pk: 5
- fields: {
checks: required-no,
extra_de: '',
extra_en: '',
number: '6',
questionset: 1,
text_de: '[de] Question 6',
text_en: '[en] Question 6',
type: choice-yesnocomment
}
model: questionnaire.question
pk: 6
- fields: {
checks: 'range=5-10 requiredif=3,yes',
extra_de: '',
extra_en: '',
number: '7',
questionset: 1,
text_de: '[de] Question 7, requiredif 3,yes',
text_en: '[en] Question 7, requiredif 3,yes',
type: range
}
model: questionnaire.question
pk: 7
- fields: {
checks: 'units=day,week,month,year',
extra_de: '',
extra_en: '',
number: '8',
questionset: 1,
text_de: '[de] Question 8',
text_en: '[en] Question 8',
type: timeperiod
}
model: questionnaire.question
pk: 8
- fields: {
checks: '',
extra_de: '',
extra_en: '',
number: '9',
questionset: 2,
text_de: '[de] Question 9',
text_en: '[en] Question 9',
type: choice
}
model: questionnaire.question
pk: 9
- fields: {
checks: '',
extra_de: '',
extra_en: '',
number: '10',
questionset: 2,
text_de: '[de] Question 10',
text_en: '[en] Question 10',
type: choice-freeform
}
model: questionnaire.question
pk: 10
- fields: {
checks: '',
extra_de: '',
extra_en: '',
number: '11',
questionset: 2,
text_de: '[de] Question 11',
text_en: '[en] Question 11',
type: choice-multiple
}
model: questionnaire.question
pk: 11
- fields: {
checks: '',
extra_de: '',
extra_en: '',
number: '12',
questionset: 2,
text_de: '[de] Question 12',
text_en: '[en] Question 12',
type: choice-multiple-freeform
}
model: questionnaire.question
pk: 12
- fields: {
checks: '',
extra_de: '',
extra_en: '',
number: '12',
questionset: 3,
text_de: '[de] tag test always',
text_en: '[en] tag test always',
type: choice-multiple-freeform
}
model: questionnaire.question
pk: 13
- fields: {
checks: 'iftag=testtag',
extra_de: '',
extra_en: '',
number: '12',
questionset: 3,
text_de: '[de] tag test checked',
text_en: '[en] tag test checked',
type: choice-multiple-freeform
}
model: questionnaire.question
pk: 14
- fields: {
checks: '',
extra_de: '',
extra_en: '',
number: '12',
questionset: 4,
text_de: '[de] tag test always',
text_en: '[en] tag test always',
type: choice-multiple-freeform
}
model: questionnaire.question
pk: 15
- fields: {
question: 9,
sortid: 1,
text_de: Option 1,
text_en: Choice 1,
value: q9_choice1
}
model: questionnaire.choice
pk: 1
- fields: {
question: 9,
sortid: 2,
text_de: Option 2,
text_en: Choice 2,
value: q9_choice2
}
model: questionnaire.choice
pk: 2
- fields: {
question: 9,
sortid: 3,
text_de: Option 3,
text_en: Choice 3,
value: q9_choice3
}
model: questionnaire.choice
pk: 3
- fields: {
question: 9,
sortid: 4,
text_de: Option 4,
text_en: Choice 4,
value: q9_choice4
}
model: questionnaire.choice
pk: 4
- fields: {
question: 10,
sortid: 1,
text_de: Option 1,
text_en: Choice 1,
value: q10_choice1
}
model: questionnaire.choice
pk: 5
- fields: {
question: 10,
sortid: 2,
text_de: Option 2,
text_en: Choice 2,
value: q10_choice2
}
model: questionnaire.choice
pk: 6
- fields: {
question: 10,
sortid: 3,
text_de: Option 3,
text_en: Choice 3,
value: q10_choice3
}
model: questionnaire.choice
pk: 7
- fields: {
question: 10,
sortid: 4,
text_de: Option 4,
text_en: Choice 4,
value: q10_choice4
}
model: questionnaire.choice
pk: 8
- fields: {
question: 11,
sortid: 1,
text_de: Option 1,
text_en: Choice 1,
value: q11_choice1
}
model: questionnaire.choice
pk: 9
- fields: {
question: 11,
sortid: 2,
text_de: Option 2,
text_en: Choice 2,
value: q11_choice2
}
model: questionnaire.choice
pk: 10
- fields: {
question: 11,
sortid: 3,
text_de: Option 3,
text_en: Choice 3,
value: q11_choice3
}
model: questionnaire.choice
pk: 11
- fields: {
question: 11,
sortid: 4,
text_de: Option 4,
text_en: Choice 4,
value: q11_choice4
}
model: questionnaire.choice
pk: 12
- fields: {
question: 12,
sortid: 1,
text_de: Option 1,
text_en: Choice 1,
value: q12_choice1
}
model: questionnaire.choice
pk: 13
- fields: {
question: 12,
sortid: 2,
text_de: Option 2,
text_en: Choice 2,
value: q12_choice2
}
model: questionnaire.choice
pk: 14
- fields: {
question: 12,
sortid: 3,
text_de: Option 3,
text_en: Choice 3,
value: q12_choice3
}
model: questionnaire.choice
pk: 15
- fields: {
question: 12,
sortid: 4,
text_de: Option 4,
text_en: Choice 4,
value: q12_choice4
}
model: questionnaire.choice
pk: 16

View File

@ -0,0 +1,58 @@
"""
Wrapper for loading templates from the filesystem.
"""
from django.conf import settings
from django.template import TemplateDoesNotExist
from django.utils._os import safe_join
from django.utils import translation
def get_template_sources(template_name, template_dirs=None):
"""
Returns the absolute paths to "template_name", when appended to each
directory in "template_dirs". Any paths that don't lie inside one of the
template dirs are excluded from the result set, for security reasons.
"""
if not template_dirs:
template_dirs = settings.TEMPLATE_DIRS
for template_dir in template_dirs:
try:
yield safe_join(template_dir, template_name)
except UnicodeDecodeError:
# The template dir name was a bytestring that wasn't valid UTF-8.
raise
except ValueError:
# The joined path was located outside of this particular
# template_dir (it might be inside another one, so this isn't
# fatal).
pass
def _load_template_source(template_name, template_dirs=None):
tried = []
for filepath in get_template_sources(template_name, template_dirs):
try:
return (open(filepath).read().decode(settings.FILE_CHARSET), filepath)
except IOError:
tried.append(filepath)
if tried:
error_msg = "Tried %s" % tried
else:
error_msg = "Your TEMPLATE_DIRS setting is empty. Change it to point to at least one template directory."
raise TemplateDoesNotExist, error_msg
def load_template_source(template_name, template_dirs=None):
"""Assuming the current language is German.
If template_name is index.$LANG.html, try index.de.html then index.html
Also replaces .. with . when attempting fallback.
"""
if "$LANG" in template_name:
lang = translation.get_language()
try:
t = template_name.replace("$LANG", lang)
res = _load_template_source(t, template_dirs)
return res
except TemplateDoesNotExist:
t = template_name.replace("$LANG", "").replace("..",".")
return _load_template_source(t, template_dirs)
return _load_template_source(template_name, template_dirs)
load_template_source.is_usable = True

View File

@ -0,0 +1,18 @@
# vim: set fileencoding=utf-8
import questionnaire
from django.conf.urls.defaults import *
from .views import *
urlpatterns = patterns('',
url(r'^q/(?P<runcode>[^/]+)/(?P<qs>\d+)/$',
'questionnaire.views.questionnaire', name='questionset'),
url(r'^q/([^/]+)/',
'questionnaire.views.questionnaire', name='questionset'),
url(r'^q/manage/csv/(\d+)/',
'questionnaire.views.export_csv'),
url(r'^q/manage/sendemail/(\d+)/$',
'questionnaire.views.send_email'),
url(r'^q/manage/manage/sendemails/$',
'questionnaire.views.send_emails'),
)

View File

@ -0,0 +1,191 @@
"""
Basic Test Suite for Questionnaire Application
Unfortunately Django 1.0 only has TestCase and not TransactionTestCase
so we can't test that a submitted page with an error does not have any
answers submitted to the DB.
"""
from django.test import TestCase
from django.test.client import Client
from .models import *
from datetime import datetime
import os
class TypeTest(TestCase):
fixtures = ( 'testQuestions.yaml', )
urls = 'questionnaire.test_urls'
def setUp(self):
self.ansdict1 = {
'questionset_id' : '1',
'question_1' : 'Open Answer 1',
'question_2' : 'Open Answer 2\r\nMultiline',
'question_3' : 'yes',
'question_4' : 'dontknow',
'question_5' : 'yes',
'question_5_comment' : 'this comment is required because of required-yes check',
'question_6' : 'no',
'question_6_comment' : 'this comment is required because of required-no check',
'question_7' : '5',
'question_8_unit' : 'week',
'question_8' : '2',
}
self.ansdict2 = {
'questionset_id' : '2',
'question_9' : 'q9_choice1', # choice
'question_10' : '_entry_', # choice-freeform
'question_10_comment' : 'my freeform',
'question_11_multiple_2' : 'q11_choice2', # choice-multiple
'question_11_multiple_4' : 'q11_choice4', # choice-multiple
'question_12_multiple_1' : 'q12_choice1',# choice-multiple-freeform
'question_12_more_1' : 'blah', # choice-multiple-freeform
}
runinfo = self.runinfo = RunInfo.objects.get(runid='test:test')
self.runid = runinfo.runid
self.subject_id = runinfo.subject_id
def test010_redirect(self):
"Check redirection from generic questionnaire to questionset"
response = self.client.get('/q/test:test/')
self.assertEqual(response['Location'], 'http://testserver/q/test:test/1/')
def test020_get_questionset_1(self):
"Get first page of Questions"
response = self.client.get('/q/test:test/1/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template[0].name, 'questionnaire/questionset.html')
def test030_language_setting(self):
"Set the language and confirm it is set in DB"
response = self.client.get('/q/test:test/1/', {"lang" : "en"})
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], 'http://testserver/q/test:test/1/')
response = self.client.get('/q/test:test/1/')
assert "Don't Know" in response.content
self.assertEqual(response.status_code, 200)
runinfo = RunInfo.objects.get(runid='test:test')
self.assertEqual(runinfo.subject.language, 'en')
response = self.client.get('/q/test:test/1/', {"lang" : "de"})
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], 'http://testserver/q/test:test/1/')
response = self.client.get('/q/test:test/1/')
assert "Weiss nicht" in response.content
self.assertEqual(response.status_code, 200)
runinfo = RunInfo.objects.get(runid='test:test')
self.assertEqual(runinfo.subject.language, 'de')
def test040_missing_question(self):
"Post questions with a mandatory field missing"
c = self.client
ansdict = self.ansdict1.copy()
del ansdict['question_3']
response = c.post('/q/test:test/1/', ansdict)
self.assertEqual(response.status_code, 200)
errors = response.context[-1]['errors']
self.assertEqual(len(errors), 1) and errors.has_key('3')
def test050_missing_question(self):
"Post questions with a mandatory field missing"
c = self.client
ansdict = self.ansdict1.copy()
del ansdict['question_5_comment']
# first set language to english
response = self.client.get('/q/test:test/1/', {"lang" : "en"})
response = c.post('/q/test:test/1/', ansdict)
self.assertEqual(response.status_code, 200)
self.assertEqual(len(response.context[-1]['errors']), 1)
def test060_successful_questionnaire(self):
"POST complete answers for QuestionSet 1"
c = self.client
ansdict1 = self.ansdict1
runinfo = RunInfo.objects.get(runid='test:test')
runid = runinfo.random = runinfo.runid = '1real'
runinfo.save()
response = c.get('/q/1real/1/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template[0].name, 'questionnaire/questionset.html')
response = c.post('/q/1real/', ansdict1)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], 'http://testserver/q/1real/2/')
"POST complete answers for QuestionSet 2"
c = self.client
ansdict2 = self.ansdict2
response = c.get('/q/1real/2/')
self.assertEqual(response.status_code, 200)
self.assertEqual(response.template[0].name, 'questionnaire/questionset.html')
response = c.post('/q/1real/', ansdict2)
self.assertEqual(response.status_code, 302)
self.assertEqual(response['Location'], 'http://testserver/')
self.assertEqual(RunInfo.objects.filter(runid='1real').count(), 0)
# TODO: The format of these answers seems very strange to me. It was
# simpler before I changed it to get the test to work.
# I'll have to revisit this once I figure out how this is meant to work
# for now it is more important to me that all tests pass
dbvalues = {
'1' : u'["%s"]' % ansdict1['question_1'],
'2' : u'["%s"]' % ansdict1['question_2'],
'3' : u'["%s"]' % ansdict1['question_3'],
'4' : u'["%s"]' % ansdict1['question_4'],
'5' : u'["%s", ["%s"]]' % (ansdict1['question_5'], ansdict1['question_5_comment']),
'6' : u'["%s", ["%s"]]' % (ansdict1['question_6'], ansdict1['question_6_comment']),
'7' : u'[%s]' % ansdict1['question_7'],
'8' : u'%s; %s' % (ansdict1['question_8'], ansdict1['question_8_unit']),
'9' : u'["q9_choice1"]',
'10' : u'[["my freeform"]]',
'11' : u'["q11_choice2", "q11_choice4"]',
'12' : u'["q12_choice1", ["blah"]]',
}
for k, v in dbvalues.items():
ans = Answer.objects.get(runid=runid, subject__id=self.subject_id,
question__number=k)
v = v.replace('\r', '\\r').replace('\n', '\\n')
self.assertEqual(ans.answer, v)
def test070_tags(self):
c = self.client
# the first questionset in questionnaire 2 is always shown,
# but one of its 2 questions is tagged with testtag
with_tags = c.get('/q/test:withtags/1/')
# so we'll get two questions shown if the run is tagged
self.assertEqual(with_tags.status_code, 200)
self.assertEqual(len(with_tags.context['qlist']), 2)
# one question, if the run is not tagged
without_tags = c.get('/q/test:withouttags/1/')
self.assertEqual(without_tags.status_code, 200)
self.assertEqual(len(without_tags.context['qlist']), 1)
# the second questionset is only shown if the run is tagged
with_tags = c.get('/q/test:withtags/2/')
self.assertEqual(with_tags.status_code, 200)
self.assertEqual(len(with_tags.context['qlist']), 1)
# meaning it'll be skipped on the untagged run
without_tags = c.get('/q/test.withouttags/2/')
self.assertEqual(without_tags.status_code, 302) # redirect
# the progress values of the first questionset should reflect
# the fact that in one run there's only one questionset
with_tags = c.get('/q/test:withtags/1/')
without_tags = c.get('/q/test:withouttags/1/')
self.assertEqual(with_tags.context['progress'], 50)
self.assertEqual(without_tags.context['progress'], 100)

View File

@ -0,0 +1,158 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2009-06-04 10:11+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: models.py:16
msgid "Active"
msgstr "Aktiv"
#: models.py:17
msgid "Inactive"
msgstr "Inaktiv"
#: models.py:22
msgid "State"
msgstr "Status"
#: models.py:24
msgid "Surname"
msgstr "Nachname"
#: models.py:26
msgid "Given name"
msgstr "Vorname"
#: models.py:27
msgid "Email"
msgstr "Email"
#: models.py:29
msgid "Gender"
msgstr "Geschlecht"
#: models.py:30
msgid "Unset"
msgstr "Keine Angabe"
#: models.py:31
msgid "Male"
msgstr "Männlich"
#: models.py:32
msgid "Female"
msgstr "Weiblich"
#: models.py:35
msgid "Next Run"
msgstr "Nächste Ausführung"
#: models.py:37
msgid "Form Type"
msgstr "Art des Formulars"
#: models.py:39
msgid "Subject receives emails"
msgstr "Person bekommt Emails"
#: models.py:40
msgid "Subject is sent paper form"
msgstr "Person bekommt Papierformular"
#: models.py:43
msgid "Language"
msgstr "Sprache"
#: qprocessors/choice.py:38 qprocessors/simple.py:66
msgid "You must select an option"
msgstr "Sie müssen eine Option auswählen"
#: qprocessors/choice.py:42 qprocessors/simple.py:70 qprocessors/simple.py:72
#: qprocessors/simple.py:74 qprocessors/simple.py:77
#: qprocessors/timeperiod.py:56
msgid "Field cannot be blank"
msgstr "Dieses Feld muss ausgefüllt werden"
#: qprocessors/choice.py:46
msgid "Invalid option!"
msgstr "Ungültige Angabe"
#: qprocessors/choice.py:107
#, python-format
msgid "You must select at least %d option"
msgid_plural "You must select at least %d options"
msgstr[0] "Sie müssen mindestens %d Option auswählen"
msgstr[1] "Sie müssen mindestens %d Optionen auswählen"
#: qprocessors/custom.py:27
msgid "Processor not defined for this question"
msgstr ""
#: qprocessors/range.py:35
msgid "Out of range"
msgstr ""
#: qprocessors/timeperiod.py:5
msgid "second(s)"
msgstr "Sekunde(n)"
#: qprocessors/timeperiod.py:6
msgid "minute(s)"
msgstr "Minute(n)"
#: qprocessors/timeperiod.py:7
msgid "hour(s)"
msgstr "Stunde(n)"
#: qprocessors/timeperiod.py:8
msgid "day(s)"
msgstr "Tag(e)"
#: qprocessors/timeperiod.py:9
msgid "week(s)"
msgstr "Woche(n)"
#: qprocessors/timeperiod.py:10
msgid "month(s)"
msgstr "Monat(e)"
#: qprocessors/timeperiod.py:11
msgid "year(s)"
msgstr "Jahr(e)"
#: qprocessors/timeperiod.py:42 qprocessors/timeperiod.py:58
msgid "Invalid time period"
msgstr "Ungültiger Zeitraum"
#: qprocessors/timeperiod.py:48
msgid "Time period must be a whole number"
msgstr "Angabe muss aus einer ganzen Zahl bestehen"
#: templates/questionnaire/choice-yesnocomment.html:3
msgid "Yes"
msgstr "Ja"
#: templates/questionnaire/choice-yesnocomment.html:5
msgid "No"
msgstr "Nein"
#: templates/questionnaire/choice-yesnocomment.html:8
msgid "Don't Know"
msgstr "Weiss nicht"
#: templates/questionnaire/questionset.html:72
msgid "Continue"
msgstr "Weiter"

View File

@ -0,0 +1,160 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2009-06-04 10:11+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: models.py:16
msgid "Active"
msgstr ""
#: models.py:17
msgid "Inactive"
msgstr ""
#: models.py:22
msgid "State"
msgstr ""
#: models.py:24
msgid "Surname"
msgstr ""
#: models.py:26
msgid "Given name"
msgstr ""
#: models.py:27
msgid "Email"
msgstr ""
#: models.py:29
msgid "Gender"
msgstr ""
#: models.py:30
msgid "Unset"
msgstr ""
#: models.py:31
msgid "Male"
msgstr ""
#: models.py:32
msgid "Female"
msgstr ""
#: models.py:35
msgid "Next Run"
msgstr ""
#: models.py:37
msgid "Form Type"
msgstr ""
#: models.py:39
msgid "Subject receives emails"
msgstr ""
#: models.py:40
msgid "Subject is sent paper form"
msgstr ""
#: models.py:43
msgid "Language"
msgstr ""
#: qprocessors/choice.py:38 qprocessors/simple.py:66
msgid "You must select an option"
msgstr "Veuillez choisir une option!"
#: qprocessors/choice.py:42 qprocessors/simple.py:70 qprocessors/simple.py:72
#: qprocessors/simple.py:74 qprocessors/simple.py:77
#: qprocessors/timeperiod.py:56
msgid "Field cannot be blank"
msgstr ""
"Merci de donner une réponse à cette question; ce champs ne peut rester vide."
#: qprocessors/choice.py:46
#, fuzzy
msgid "Invalid option!"
msgstr "Option non valable"
#: qprocessors/choice.py:107
#, python-format
msgid "You must select at least %d option"
msgid_plural "You must select at least %d options"
msgstr[0] "Vous devez cocher au moins %d option"
msgstr[1] "Vous devez cocher au moins %d options"
#: qprocessors/custom.py:27
msgid "Processor not defined for this question"
msgstr ""
#: qprocessors/range.py:35
msgid "Out of range"
msgstr ""
#: qprocessors/timeperiod.py:5
msgid "second(s)"
msgstr "seconde(s)"
#: qprocessors/timeperiod.py:6
msgid "minute(s)"
msgstr "minute(s)"
#: qprocessors/timeperiod.py:7
msgid "hour(s)"
msgstr "heure(s)"
#: qprocessors/timeperiod.py:8
msgid "day(s)"
msgstr "jour(s)"
#: qprocessors/timeperiod.py:9
msgid "week(s)"
msgstr "semaine(s)"
#: qprocessors/timeperiod.py:10
msgid "month(s)"
msgstr "mois"
#: qprocessors/timeperiod.py:11
msgid "year(s)"
msgstr "année(s)"
#: qprocessors/timeperiod.py:42 qprocessors/timeperiod.py:58
msgid "Invalid time period"
msgstr "Période non valable"
#: qprocessors/timeperiod.py:48
msgid "Time period must be a whole number"
msgstr "La période doit être un chiffre entier"
#: templates/questionnaire/choice-yesnocomment.html:3
msgid "Yes"
msgstr "Oui"
#: templates/questionnaire/choice-yesnocomment.html:5
msgid "No"
msgstr "Non"
#: templates/questionnaire/choice-yesnocomment.html:8
msgid "Don't Know"
msgstr "Ne sais pas"
#: templates/questionnaire/questionset.html:72
msgid "Continue"
msgstr "Continuer"

View File

@ -0,0 +1,174 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-11-29 15:51+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: models.py:16
msgid "Active"
msgstr "Attivo"
#: models.py:17
msgid "Inactive"
msgstr "Inattivo"
#: models.py:22
msgid "State"
msgstr "Stato"
#: models.py:24
msgid "Surname"
msgstr "Cognome"
#: models.py:26
msgid "Given name"
msgstr "Nome"
#: models.py:27
msgid "Email"
msgstr "Email"
#: models.py:29
msgid "Gender"
msgstr "Sesso"
#: models.py:30
msgid "Unset"
msgstr "Nessun parere"
#: models.py:31
msgid "Male"
msgstr "Maschio"
#: models.py:32
msgid "Female"
msgstr "Femmina"
#: models.py:35
msgid "Next Run"
msgstr "Prossimo"
#: models.py:37
msgid "Form Type"
msgstr "Tipo di domanda"
#: models.py:39
msgid "Subject receives emails"
msgstr "Persona contattata via email"
#: models.py:40
msgid "Subject is sent paper form"
msgstr "Persona contattata via questionario cartaceo"
#: models.py:43
msgid "Language"
msgstr "Lingua"
#: views.py:608 templates/questionnaire/choice-yesnocomment.html:2
msgid "Yes"
msgstr "Si"
#: views.py:608 templates/questionnaire/choice-yesnocomment.html:3
msgid "No"
msgstr "No"
#: views.py:610 templates/questionnaire/choice-yesnocomment.html:5
msgid "Don't Know"
msgstr "Non so"
#: qprocessors/choice.py:39 qprocessors/simple.py:67
msgid "You must select an option"
msgstr "Devi scegliere un'opzione"
#: qprocessors/choice.py:43 qprocessors/simple.py:71 qprocessors/simple.py:73
#: qprocessors/simple.py:75 qprocessors/simple.py:78
#: qprocessors/timeperiod.py:56
msgid "Field cannot be blank"
msgstr "Domanda richiesta"
#: qprocessors/choice.py:48
msgid "Invalid option!"
msgstr "Opzione non valida"
#: qprocessors/choice.py:110
#, python-format
msgid "You must select at least %d option"
msgid_plural "You must select at least %d options"
msgstr[0] "Devi sciegliere almeno %d opzione"
msgstr[1] "Devi sciegliere almeno %d opzioni"
#: qprocessors/custom.py:27
msgid "Processor not defined for this question"
msgstr ""
#: qprocessors/range.py:36
msgid "Out of range"
msgstr ""
#: qprocessors/timeperiod.py:5
msgid "second(s)"
msgstr "secondi"
#: qprocessors/timeperiod.py:6
msgid "minute(s)"
msgstr "minuti"
#: qprocessors/timeperiod.py:7
msgid "hour(s)"
msgstr "ora(e)"
#: qprocessors/timeperiod.py:8
msgid "day(s)"
msgstr "giorno(i)"
#: qprocessors/timeperiod.py:9
msgid "week(s)"
msgstr "settimana(e)"
#: qprocessors/timeperiod.py:10
msgid "month(s)"
msgstr "mese(i)"
#: qprocessors/timeperiod.py:11
msgid "year(s)"
msgstr "anno(i)"
#: qprocessors/timeperiod.py:42 qprocessors/timeperiod.py:58
msgid "Invalid time period"
msgstr "Intervallo di tempo non valido"
#: qprocessors/timeperiod.py:48
msgid "Time period must be a whole number"
msgstr "Angabe muss aus einer ganzen Zahl bestehen"
#: templates/admin/questionnaire/question/change_form.html:10
msgid "Choices"
msgstr ""
#: templates/admin/questionnaire/question/change_form.html:12
msgid "Sortid determines the choice order."
msgstr ""
#: templates/admin/questionnaire/question/change_form.html:13
msgid ""
"Short values will appear in exported data and are \n"
"referenced in 'Additional checks' fields. Numbers, letters \n"
"or single words are valid."
msgstr ""
#: templates/questionnaire/questionset.html:77
msgid "Continue"
msgstr "Continua"

Binary file not shown.

View File

@ -0,0 +1,190 @@
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# Victor Safronivich <vsafronovich@gmail.com>, 2012.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: 2.0\n"
"Report-Msgid-Bugs-To: Victor Safronivich <vsafronovich@gmail.com>\n"
"POT-Creation-Date: 2012-08-24 09:15+0600\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: Victor Safronivich <vsafronovich@gmail.com>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n"
"%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2)\n"
#: .\models.py:16
msgid "Active"
msgstr "Активно"
#: .\models.py:17
msgid "Inactive"
msgstr "Не активно"
#: .\models.py:22
msgid "State"
msgstr "Состояние"
#: .\models.py:24
msgid "Surname"
msgstr "Фамилия"
#: .\models.py:26
msgid "Given name"
msgstr "Имя"
#: .\models.py:27
msgid "Email"
msgstr "Эл. почта"
#: .\models.py:29
msgid "Gender"
msgstr "Пол"
#: .\models.py:30
msgid "Unset"
msgstr "Неважно"
#: .\models.py:31
msgid "Male"
msgstr "Мужской"
#: .\models.py:32
msgid "Female"
msgstr "Женский"
#: .\models.py:35
msgid "Next Run"
msgstr "Следующая попытка"
#: .\models.py:37
msgid "Form Type"
msgstr "Тип формы"
#: .\models.py:39
msgid "Subject receives emails"
msgstr "Тема полученного письма"
#: .\models.py:40
msgid "Subject is sent paper form"
msgstr ""
#: .\models.py:43
msgid "Language"
msgstr "Язык"
#: .\views.py:805 .\templates\questionnaire\choice-yesnocomment.html.py:10
msgid "Yes"
msgstr "Да"
#: .\views.py:805 .\templates\questionnaire\choice-yesnocomment.html.py:18
msgid "No"
msgstr "Нет"
#: .\views.py:807 .\templates\questionnaire\choice-yesnocomment.html.py:27
msgid "Don't Know"
msgstr "Не известно"
#: .\qprocessors\choice.py:38 .\qprocessors\simple.py:68
msgid "You must select an option"
msgstr "Вы должны выбрать ответ"
#: .\qprocessors\choice.py:42 .\qprocessors\simple.py:72
#: .\qprocessors\simple.py:74 .\qprocessors\simple.py:76
#: .\qprocessors\simple.py:79 .\qprocessors\timeperiod.py:56
msgid "Field cannot be blank"
msgstr "Поле не может быть пустым"
#: .\qprocessors\choice.py:47
msgid "Invalid option!"
msgstr "Не правильный ответ!"
#: .\qprocessors\choice.py:108
#, python-format
msgid "You must select at least %d option"
msgid_plural "You must select at least %d options"
msgstr[0] "Вы должны выбрать как минимум %d ответ"
msgstr[1] "Вы должны выбрать как минимум %d ответа"
msgstr[2] "Вы должны выбрать как минимум %d ответов"
#: .\qprocessors\custom.py:27
msgid "Processor not defined for this question"
msgstr "Для этого вопроса не определен обработчик"
#: .\qprocessors\range.py:41
msgid "Out of range"
msgstr "Вне диапазона значений"
#: .\qprocessors\timeperiod.py:5
msgid "second(s)"
msgstr "Секунды"
#: .\qprocessors\timeperiod.py:6
msgid "minute(s)"
msgstr "Минуты"
#: .\qprocessors\timeperiod.py:7
msgid "hour(s)"
msgstr "часов"
#: .\qprocessors\timeperiod.py:8
msgid "day(s)"
msgstr "дней"
#: .\qprocessors\timeperiod.py:9
msgid "week(s)"
msgstr "недель"
#: .\qprocessors\timeperiod.py:10
msgid "month(s)"
msgstr "месяцев"
#: .\qprocessors\timeperiod.py:11
msgid "year(s)"
msgstr "лет"
#: .\qprocessors\timeperiod.py:42 .\qprocessors\timeperiod.py:58
msgid "Invalid time period"
msgstr "Неправильный временной интервал"
#: .\qprocessors\timeperiod.py:48
msgid "Time period must be a whole number"
msgstr "Временной интервал должен быть целым числом"
#: .\templates\admin\questionnaire\question\change_form.html.py:10
msgid "Choices"
msgstr "Варианты ответов"
#: .\templates\admin\questionnaire\question\change_form.html.py:12
msgid "Sortid determines the choice order."
msgstr "Поле обозначает порядок ответов"
#: .\templates\admin\questionnaire\question\change_form.html.py:13
msgid ""
"Short values will appear in exported data and are \n"
"referenced in 'Additional checks' fields. Numbers, letters \n"
"or single words are valid."
msgstr ""
"Краткие значения появятся в данных экспорта. \n"
"Допустимы числа, буквы или простые слова."
#: .\templates\questionnaire\choice-yesnocomment.html.py:35
msgid "comment"
msgstr "комментарий"
#: .\templates\questionnaire\questionset.html.py:58
msgid "edit"
msgstr "редактировать"
#: .\templates\questionnaire\questionset.html.py:95
msgid "Continue"
msgstr "далее"
#: .\templates\questionnaire\questionset.html.py:100
msgid "return to previous page"
msgstr "вернуть на предыдущую страницу"

View File

@ -1,4 +1,3 @@
import string
from django.core.management.base import BaseCommand
from ...models import Landing
@ -7,10 +6,9 @@ class Command(BaseCommand):
help = "make survey nonces with the specified label"
args = "<how_many> <label>"
def handle(self, how_many=1, label="no label yet", **options):
how_many=int(how_many)
while how_many>0:
landing = Landing.objects.create(label=label)
while how_many > 0:
landing = Landing.objects.create(label = label)
print landing.nonce
how_many -= 1

View File

@ -0,0 +1,8 @@
from django.core.management.base import NoArgsCommand
class Command(NoArgsCommand):
def handle_noargs(self, **options):
from ...emails import send_emails
res = send_emails()
if res:
print res

View File

@ -0,0 +1,221 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Subject'
db.create_table('questionnaire_subject', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('state', self.gf('django.db.models.fields.CharField')(default='inactive', max_length=16)),
('surname', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)),
('givenname', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)),
('email', self.gf('django.db.models.fields.EmailField')(max_length=75, null=True, blank=True)),
('gender', self.gf('django.db.models.fields.CharField')(default='unset', max_length=8, blank=True)),
('nextrun', self.gf('django.db.models.fields.DateField')(null=True, blank=True)),
('formtype', self.gf('django.db.models.fields.CharField')(default='email', max_length=16)),
('language', self.gf('django.db.models.fields.CharField')(default='en-us', max_length=2)),
))
db.send_create_signal('questionnaire', ['Subject'])
# Adding model 'Questionnaire'
db.create_table('questionnaire_questionnaire', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('name', self.gf('django.db.models.fields.CharField')(max_length=128)),
('redirect_url', self.gf('django.db.models.fields.CharField')(default='/static/complete.html', max_length=128)),
))
db.send_create_signal('questionnaire', ['Questionnaire'])
# Adding model 'QuestionSet'
db.create_table('questionnaire_questionset', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('questionnaire', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.Questionnaire'])),
('sortid', self.gf('django.db.models.fields.IntegerField')()),
('heading', self.gf('django.db.models.fields.CharField')(max_length=64)),
('checks', self.gf('django.db.models.fields.CharField')(max_length=256, blank=True)),
('text_en', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
))
db.send_create_signal('questionnaire', ['QuestionSet'])
# Adding model 'RunInfo'
db.create_table('questionnaire_runinfo', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('subject', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.Subject'])),
('random', self.gf('django.db.models.fields.CharField')(max_length=32)),
('runid', self.gf('django.db.models.fields.CharField')(max_length=32)),
('questionset', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.QuestionSet'], null=True, blank=True)),
('emailcount', self.gf('django.db.models.fields.IntegerField')(default=0)),
('created', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
('emailsent', self.gf('django.db.models.fields.DateTimeField')(null=True, blank=True)),
('lastemailerror', self.gf('django.db.models.fields.CharField')(max_length=64, null=True, blank=True)),
('state', self.gf('django.db.models.fields.CharField')(max_length=16, null=True, blank=True)),
('cookies', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
('tags', self.gf('django.db.models.fields.TextField')(blank=True)),
('skipped', self.gf('django.db.models.fields.TextField')(blank=True)),
))
db.send_create_signal('questionnaire', ['RunInfo'])
# Adding model 'RunInfoHistory'
db.create_table('questionnaire_runinfohistory', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('subject', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.Subject'])),
('runid', self.gf('django.db.models.fields.CharField')(max_length=32)),
('completed', self.gf('django.db.models.fields.DateField')()),
('tags', self.gf('django.db.models.fields.TextField')(blank=True)),
('skipped', self.gf('django.db.models.fields.TextField')(blank=True)),
('questionnaire', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.Questionnaire'])),
))
db.send_create_signal('questionnaire', ['RunInfoHistory'])
# Adding model 'Question'
db.create_table('questionnaire_question', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('questionset', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.QuestionSet'])),
('number', self.gf('django.db.models.fields.CharField')(max_length=8)),
('sort_id', self.gf('django.db.models.fields.IntegerField')(null=True, blank=True)),
('text_en', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
('type', self.gf('django.db.models.fields.CharField')(max_length=32)),
('extra_en', self.gf('django.db.models.fields.CharField')(max_length=512, null=True, blank=True)),
('checks', self.gf('django.db.models.fields.CharField')(max_length=512, null=True, blank=True)),
('footer_en', self.gf('django.db.models.fields.TextField')(null=True, blank=True)),
))
db.send_create_signal('questionnaire', ['Question'])
# Adding model 'Choice'
db.create_table('questionnaire_choice', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('question', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.Question'])),
('sortid', self.gf('django.db.models.fields.IntegerField')()),
('value', self.gf('django.db.models.fields.CharField')(max_length=64)),
('text_en', self.gf('django.db.models.fields.CharField')(max_length=200, null=True, blank=True)),
('tags', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)),
))
db.send_create_signal('questionnaire', ['Choice'])
# Adding model 'Answer'
db.create_table('questionnaire_answer', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('subject', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.Subject'])),
('question', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.Question'])),
('runid', self.gf('django.db.models.fields.CharField')(max_length=32)),
('answer', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal('questionnaire', ['Answer'])
def backwards(self, orm):
# Deleting model 'Subject'
db.delete_table('questionnaire_subject')
# Deleting model 'Questionnaire'
db.delete_table('questionnaire_questionnaire')
# Deleting model 'QuestionSet'
db.delete_table('questionnaire_questionset')
# Deleting model 'RunInfo'
db.delete_table('questionnaire_runinfo')
# Deleting model 'RunInfoHistory'
db.delete_table('questionnaire_runinfohistory')
# Deleting model 'Question'
db.delete_table('questionnaire_question')
# Deleting model 'Choice'
db.delete_table('questionnaire_choice')
# Deleting model 'Answer'
db.delete_table('questionnaire_answer')
models = {
'questionnaire.answer': {
'Meta': {'object_name': 'Answer'},
'answer': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"})
},
'questionnaire.choice': {
'Meta': {'object_name': 'Choice'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'tags': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'text_en': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
'questionnaire.question': {
'Meta': {'object_name': 'Question'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'extra_en': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'footer_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'number': ('django.db.models.fields.CharField', [], {'max_length': '8'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']"}),
'sort_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '32'})
},
'questionnaire.questionnaire': {
'Meta': {'object_name': 'Questionnaire'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'redirect_url': ('django.db.models.fields.CharField', [], {'default': "'/static/complete.html'", 'max_length': '128'})
},
'questionnaire.questionset': {
'Meta': {'object_name': 'QuestionSet'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
'heading': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
},
'questionnaire.runinfo': {
'Meta': {'object_name': 'RunInfo'},
'cookies': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'emailcount': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'emailsent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lastemailerror': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']", 'null': 'True', 'blank': 'True'}),
'random': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.runinfohistory': {
'Meta': {'object_name': 'RunInfoHistory'},
'completed': ('django.db.models.fields.DateField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.subject': {
'Meta': {'object_name': 'Subject'},
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
'formtype': ('django.db.models.fields.CharField', [], {'default': "'email'", 'max_length': '16'}),
'gender': ('django.db.models.fields.CharField', [], {'default': "'unset'", 'max_length': '8', 'blank': 'True'}),
'givenname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'default': "'en-us'", 'max_length': '2'}),
'nextrun': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'inactive'", 'max_length': '16'}),
'surname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['questionnaire']

View File

@ -0,0 +1,105 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Changing field 'Subject.language'
db.alter_column('questionnaire_subject', 'language', self.gf('django.db.models.fields.CharField')(max_length=5))
def backwards(self, orm):
# Changing field 'Subject.language'
db.alter_column('questionnaire_subject', 'language', self.gf('django.db.models.fields.CharField')(max_length=2))
models = {
'questionnaire.answer': {
'Meta': {'object_name': 'Answer'},
'answer': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"})
},
'questionnaire.choice': {
'Meta': {'object_name': 'Choice'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'tags': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'text_en': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
'questionnaire.question': {
'Meta': {'object_name': 'Question'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'extra_en': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'footer_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'number': ('django.db.models.fields.CharField', [], {'max_length': '8'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']"}),
'sort_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '32'})
},
'questionnaire.questionnaire': {
'Meta': {'object_name': 'Questionnaire'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'redirect_url': ('django.db.models.fields.CharField', [], {'default': "'/static/complete.html'", 'max_length': '128'})
},
'questionnaire.questionset': {
'Meta': {'object_name': 'QuestionSet'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
'heading': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
},
'questionnaire.runinfo': {
'Meta': {'object_name': 'RunInfo'},
'cookies': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'emailcount': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'emailsent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lastemailerror': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']", 'null': 'True', 'blank': 'True'}),
'random': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.runinfohistory': {
'Meta': {'object_name': 'RunInfoHistory'},
'completed': ('django.db.models.fields.DateField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.subject': {
'Meta': {'object_name': 'Subject'},
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
'formtype': ('django.db.models.fields.CharField', [], {'default': "'email'", 'max_length': '16'}),
'gender': ('django.db.models.fields.CharField', [], {'default': "'unset'", 'max_length': '8', 'blank': 'True'}),
'givenname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'default': "'en-us'", 'max_length': '5'}),
'nextrun': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'inactive'", 'max_length': '16'}),
'surname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['questionnaire']

View File

@ -0,0 +1,182 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'GlobalStyles'
db.create_table('questionnaire_globalstyles', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('content', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal('questionnaire', ['GlobalStyles'])
# Adding model 'DBStylesheet'
db.create_table('questionnaire_dbstylesheet', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('inclusion_tag', self.gf('django.db.models.fields.CharField')(max_length=128)),
('content', self.gf('django.db.models.fields.TextField')()),
))
db.send_create_signal('questionnaire', ['DBStylesheet'])
# Adding field 'Question.parse_html'
db.add_column('questionnaire_question', 'parse_html',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Adding field 'Questionnaire.html'
db.add_column('questionnaire_questionnaire', 'html',
self.gf('django.db.models.fields.TextField')(default='', blank=True),
keep_default=False)
# Adding field 'Questionnaire.parse_html'
db.add_column('questionnaire_questionnaire', 'parse_html',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Adding field 'Questionnaire.admin_access_only'
db.add_column('questionnaire_questionnaire', 'admin_access_only',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Changing field 'RunInfoHistory.completed'
db.alter_column('questionnaire_runinfohistory', 'completed', self.gf('django.db.models.fields.DateTimeField')())
# Adding field 'QuestionSet.parse_html'
db.add_column('questionnaire_questionset', 'parse_html',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
def backwards(self, orm):
# Deleting model 'GlobalStyles'
db.delete_table('questionnaire_globalstyles')
# Deleting model 'DBStylesheet'
db.delete_table('questionnaire_dbstylesheet')
# Deleting field 'Question.parse_html'
db.delete_column('questionnaire_question', 'parse_html')
# Deleting field 'Questionnaire.html'
db.delete_column('questionnaire_questionnaire', 'html')
# Deleting field 'Questionnaire.parse_html'
db.delete_column('questionnaire_questionnaire', 'parse_html')
# Deleting field 'Questionnaire.admin_access_only'
db.delete_column('questionnaire_questionnaire', 'admin_access_only')
# Changing field 'RunInfoHistory.completed'
db.alter_column('questionnaire_runinfohistory', 'completed', self.gf('django.db.models.fields.DateField')())
# Deleting field 'QuestionSet.parse_html'
db.delete_column('questionnaire_questionset', 'parse_html')
models = {
'questionnaire.answer': {
'Meta': {'object_name': 'Answer'},
'answer': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"})
},
'questionnaire.choice': {
'Meta': {'object_name': 'Choice'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'tags': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'text_en': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
'questionnaire.dbstylesheet': {
'Meta': {'object_name': 'DBStylesheet'},
'content': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inclusion_tag': ('django.db.models.fields.CharField', [], {'max_length': '128'})
},
'questionnaire.globalstyles': {
'Meta': {'object_name': 'GlobalStyles'},
'content': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'questionnaire.question': {
'Meta': {'object_name': 'Question'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'extra_en': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'footer_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'number': ('django.db.models.fields.CharField', [], {'max_length': '8'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']"}),
'sort_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '32'})
},
'questionnaire.questionnaire': {
'Meta': {'object_name': 'Questionnaire'},
'admin_access_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'html': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'redirect_url': ('django.db.models.fields.CharField', [], {'default': "'/static/complete.html'", 'max_length': '128'})
},
'questionnaire.questionset': {
'Meta': {'object_name': 'QuestionSet'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
'heading': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
},
'questionnaire.runinfo': {
'Meta': {'object_name': 'RunInfo'},
'cookies': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'emailcount': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'emailsent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lastemailerror': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']", 'null': 'True', 'blank': 'True'}),
'random': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.runinfohistory': {
'Meta': {'object_name': 'RunInfoHistory'},
'completed': ('django.db.models.fields.DateTimeField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.subject': {
'Meta': {'object_name': 'Subject'},
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
'formtype': ('django.db.models.fields.CharField', [], {'default': "'email'", 'max_length': '16'}),
'gender': ('django.db.models.fields.CharField', [], {'default': "'unset'", 'max_length': '8', 'blank': 'True'}),
'givenname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'default': "'en-us'", 'max_length': '5'}),
'nextrun': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'inactive'", 'max_length': '16'}),
'surname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['questionnaire']

View File

@ -0,0 +1,143 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding model 'Landing'
db.create_table('questionnaire_landing', (
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
('nonce', self.gf('django.db.models.fields.CharField')(max_length=32, null=True, blank=True)),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='landings', null=True, to=orm['contenttypes.ContentType'])),
('object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True, blank=True)),
('label', self.gf('django.db.models.fields.CharField')(max_length=64, blank=True)),
))
db.send_create_signal('questionnaire', ['Landing'])
def backwards(self, orm):
# Deleting model 'Landing'
db.delete_table('questionnaire_landing')
models = {
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'questionnaire.answer': {
'Meta': {'object_name': 'Answer'},
'answer': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"})
},
'questionnaire.choice': {
'Meta': {'object_name': 'Choice'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'tags': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'text_en': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
'questionnaire.dbstylesheet': {
'Meta': {'object_name': 'DBStylesheet'},
'content': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inclusion_tag': ('django.db.models.fields.CharField', [], {'max_length': '128'})
},
'questionnaire.globalstyles': {
'Meta': {'object_name': 'GlobalStyles'},
'content': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'questionnaire.landing': {
'Meta': {'object_name': 'Landing'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'landings'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'nonce': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
},
'questionnaire.question': {
'Meta': {'object_name': 'Question'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'extra_en': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'footer_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'number': ('django.db.models.fields.CharField', [], {'max_length': '8'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']"}),
'sort_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '32'})
},
'questionnaire.questionnaire': {
'Meta': {'object_name': 'Questionnaire'},
'admin_access_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'html': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'redirect_url': ('django.db.models.fields.CharField', [], {'default': "'/static/complete.html'", 'max_length': '128'})
},
'questionnaire.questionset': {
'Meta': {'object_name': 'QuestionSet'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
'heading': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
},
'questionnaire.runinfo': {
'Meta': {'object_name': 'RunInfo'},
'cookies': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'emailcount': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'emailsent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lastemailerror': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']", 'null': 'True', 'blank': 'True'}),
'random': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.runinfohistory': {
'Meta': {'object_name': 'RunInfoHistory'},
'completed': ('django.db.models.fields.DateTimeField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.subject': {
'Meta': {'object_name': 'Subject'},
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
'formtype': ('django.db.models.fields.CharField', [], {'default': "'email'", 'max_length': '16'}),
'gender': ('django.db.models.fields.CharField', [], {'default': "'unset'", 'max_length': '8', 'blank': 'True'}),
'givenname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'default': "'en-us'", 'max_length': '5'}),
'nextrun': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'inactive'", 'max_length': '16'}),
'surname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['questionnaire']

View File

@ -0,0 +1,149 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import DataMigration
from django.db import models
class Migration(DataMigration):
depends_on = (
("survey", "0001_initial"),
)
needed_by = (
("survey", "0002_auto__del_landing"),
)
def forwards(self, orm):
oldLanding=orm['survey.Landing']
Landing=orm['questionnaire.Landing']
for landing in oldLanding.objects.all():
Landing.objects.create(nonce=landing.nonce, label=landing.label, content_type=landing.content_type, object_id=landing.object_id)
def backwards(self, orm):
pass
models = {
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'questionnaire.answer': {
'Meta': {'object_name': 'Answer'},
'answer': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"})
},
'questionnaire.choice': {
'Meta': {'object_name': 'Choice'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'tags': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'text_en': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
'questionnaire.dbstylesheet': {
'Meta': {'object_name': 'DBStylesheet'},
'content': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inclusion_tag': ('django.db.models.fields.CharField', [], {'max_length': '128'})
},
'questionnaire.globalstyles': {
'Meta': {'object_name': 'GlobalStyles'},
'content': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'questionnaire.landing': {
'Meta': {'object_name': 'Landing'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'landings'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'nonce': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
},
'questionnaire.question': {
'Meta': {'object_name': 'Question'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'extra_en': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'footer_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'number': ('django.db.models.fields.CharField', [], {'max_length': '8'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']"}),
'sort_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '32'})
},
'questionnaire.questionnaire': {
'Meta': {'object_name': 'Questionnaire'},
'admin_access_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'html': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'redirect_url': ('django.db.models.fields.CharField', [], {'default': "'/static/complete.html'", 'max_length': '128'})
},
'questionnaire.questionset': {
'Meta': {'object_name': 'QuestionSet'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
'heading': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
},
'questionnaire.runinfo': {
'Meta': {'object_name': 'RunInfo'},
'cookies': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'emailcount': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'emailsent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'lastemailerror': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']", 'null': 'True', 'blank': 'True'}),
'random': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.runinfohistory': {
'Meta': {'object_name': 'RunInfoHistory'},
'completed': ('django.db.models.fields.DateTimeField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.subject': {
'Meta': {'object_name': 'Subject'},
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
'formtype': ('django.db.models.fields.CharField', [], {'default': "'email'", 'max_length': '16'}),
'gender': ('django.db.models.fields.CharField', [], {'default': "'unset'", 'max_length': '8', 'blank': 'True'}),
'givenname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'default': "'en-us'", 'max_length': '5'}),
'nextrun': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'inactive'", 'max_length': '16'}),
'surname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'})
},
'survey.landing': {
'Meta': {'object_name': 'Landing'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'nonce': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['survey', 'questionnaire']
symmetrical = True

View File

@ -0,0 +1,157 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'RunInfoHistory.landing'
db.add_column('questionnaire_runinfohistory', 'landing',
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.Landing'], null=True, blank=True),
keep_default=False)
# Adding field 'RunInfo.landing'
db.add_column('questionnaire_runinfo', 'landing',
self.gf('django.db.models.fields.related.ForeignKey')(to=orm['questionnaire.Landing'], null=True, blank=True),
keep_default=False)
# Adding field 'Landing.questionnaire'
db.add_column('questionnaire_landing', 'questionnaire',
self.gf('django.db.models.fields.related.ForeignKey')(blank=True, related_name='landings', null=True, to=orm['questionnaire.Questionnaire']),
keep_default=False)
def backwards(self, orm):
# Deleting field 'RunInfoHistory.landing'
db.delete_column('questionnaire_runinfohistory', 'landing_id')
# Deleting field 'RunInfo.landing'
db.delete_column('questionnaire_runinfo', 'landing_id')
# Deleting field 'Landing.questionnaire'
db.delete_column('questionnaire_landing', 'questionnaire_id')
models = {
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'questionnaire.answer': {
'Meta': {'object_name': 'Answer'},
'answer': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"})
},
'questionnaire.choice': {
'Meta': {'object_name': 'Choice'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'tags': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'text_en': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
'questionnaire.dbstylesheet': {
'Meta': {'object_name': 'DBStylesheet'},
'content': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inclusion_tag': ('django.db.models.fields.CharField', [], {'max_length': '128'})
},
'questionnaire.globalstyles': {
'Meta': {'object_name': 'GlobalStyles'},
'content': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'questionnaire.landing': {
'Meta': {'object_name': 'Landing'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'landings'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'nonce': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'landings'", 'null': 'True', 'to': "orm['questionnaire.Questionnaire']"})
},
'questionnaire.question': {
'Meta': {'object_name': 'Question'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'extra_en': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'footer_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'number': ('django.db.models.fields.CharField', [], {'max_length': '8'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']"}),
'sort_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '32'})
},
'questionnaire.questionnaire': {
'Meta': {'object_name': 'Questionnaire'},
'admin_access_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'html': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'redirect_url': ('django.db.models.fields.CharField', [], {'default': "'/static/complete.html'", 'max_length': '128'})
},
'questionnaire.questionset': {
'Meta': {'object_name': 'QuestionSet'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
'heading': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
},
'questionnaire.runinfo': {
'Meta': {'object_name': 'RunInfo'},
'cookies': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'emailcount': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'emailsent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'landing': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Landing']", 'null': 'True', 'blank': 'True'}),
'lastemailerror': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']", 'null': 'True', 'blank': 'True'}),
'random': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.runinfohistory': {
'Meta': {'object_name': 'RunInfoHistory'},
'completed': ('django.db.models.fields.DateTimeField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'landing': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Landing']", 'null': 'True', 'blank': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.subject': {
'Meta': {'object_name': 'Subject'},
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
'formtype': ('django.db.models.fields.CharField', [], {'default': "'email'", 'max_length': '16'}),
'gender': ('django.db.models.fields.CharField', [], {'default': "'unset'", 'max_length': '8', 'blank': 'True'}),
'givenname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'default': "'en-us'", 'max_length': '5'}),
'nextrun': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'inactive'", 'max_length': '16'}),
'surname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['questionnaire']

View File

@ -0,0 +1,151 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
def forwards(self, orm):
# Adding field 'Subject.anonymous'
db.add_column('questionnaire_subject', 'anonymous',
self.gf('django.db.models.fields.BooleanField')(default=False),
keep_default=False)
# Adding field 'Subject.ip_address'
db.add_column('questionnaire_subject', 'ip_address',
self.gf('django.db.models.fields.GenericIPAddressField')(max_length=39, null=True, blank=True),
keep_default=False)
def backwards(self, orm):
# Deleting field 'Subject.anonymous'
db.delete_column('questionnaire_subject', 'anonymous')
# Deleting field 'Subject.ip_address'
db.delete_column('questionnaire_subject', 'ip_address')
models = {
'contenttypes.contenttype': {
'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
},
'questionnaire.answer': {
'Meta': {'object_name': 'Answer'},
'answer': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"})
},
'questionnaire.choice': {
'Meta': {'object_name': 'Choice'},
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'question': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Question']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'tags': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'text_en': ('django.db.models.fields.CharField', [], {'max_length': '200', 'null': 'True', 'blank': 'True'}),
'value': ('django.db.models.fields.CharField', [], {'max_length': '64'})
},
'questionnaire.dbstylesheet': {
'Meta': {'object_name': 'DBStylesheet'},
'content': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'inclusion_tag': ('django.db.models.fields.CharField', [], {'max_length': '128'})
},
'questionnaire.globalstyles': {
'Meta': {'object_name': 'GlobalStyles'},
'content': ('django.db.models.fields.TextField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
},
'questionnaire.landing': {
'Meta': {'object_name': 'Landing'},
'content_type': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'landings'", 'null': 'True', 'to': "orm['contenttypes.ContentType']"}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'label': ('django.db.models.fields.CharField', [], {'max_length': '64', 'blank': 'True'}),
'nonce': ('django.db.models.fields.CharField', [], {'max_length': '32', 'null': 'True', 'blank': 'True'}),
'object_id': ('django.db.models.fields.PositiveIntegerField', [], {'null': 'True', 'blank': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'landings'", 'null': 'True', 'to': "orm['questionnaire.Questionnaire']"})
},
'questionnaire.question': {
'Meta': {'object_name': 'Question'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'extra_en': ('django.db.models.fields.CharField', [], {'max_length': '512', 'null': 'True', 'blank': 'True'}),
'footer_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'number': ('django.db.models.fields.CharField', [], {'max_length': '8'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']"}),
'sort_id': ('django.db.models.fields.IntegerField', [], {'null': 'True', 'blank': 'True'}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'type': ('django.db.models.fields.CharField', [], {'max_length': '32'})
},
'questionnaire.questionnaire': {
'Meta': {'object_name': 'Questionnaire'},
'admin_access_only': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'html': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'name': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'redirect_url': ('django.db.models.fields.CharField', [], {'default': "'/static/complete.html'", 'max_length': '128'})
},
'questionnaire.questionset': {
'Meta': {'object_name': 'QuestionSet'},
'checks': ('django.db.models.fields.CharField', [], {'max_length': '256', 'blank': 'True'}),
'heading': ('django.db.models.fields.CharField', [], {'max_length': '64'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'parse_html': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'sortid': ('django.db.models.fields.IntegerField', [], {}),
'text_en': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'})
},
'questionnaire.runinfo': {
'Meta': {'object_name': 'RunInfo'},
'cookies': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}),
'created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
'emailcount': ('django.db.models.fields.IntegerField', [], {'default': '0'}),
'emailsent': ('django.db.models.fields.DateTimeField', [], {'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'landing': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Landing']", 'null': 'True', 'blank': 'True'}),
'lastemailerror': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'questionset': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.QuestionSet']", 'null': 'True', 'blank': 'True'}),
'random': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'max_length': '16', 'null': 'True', 'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.runinfohistory': {
'Meta': {'object_name': 'RunInfoHistory'},
'completed': ('django.db.models.fields.DateTimeField', [], {}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'landing': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Landing']", 'null': 'True', 'blank': 'True'}),
'questionnaire': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Questionnaire']"}),
'runid': ('django.db.models.fields.CharField', [], {'max_length': '32'}),
'skipped': ('django.db.models.fields.TextField', [], {'blank': 'True'}),
'subject': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['questionnaire.Subject']"}),
'tags': ('django.db.models.fields.TextField', [], {'blank': 'True'})
},
'questionnaire.subject': {
'Meta': {'object_name': 'Subject'},
'anonymous': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'null': 'True', 'blank': 'True'}),
'formtype': ('django.db.models.fields.CharField', [], {'default': "'email'", 'max_length': '16'}),
'gender': ('django.db.models.fields.CharField', [], {'default': "'unset'", 'max_length': '8', 'blank': 'True'}),
'givenname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'}),
'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
'ip_address': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39', 'null': 'True', 'blank': 'True'}),
'language': ('django.db.models.fields.CharField', [], {'default': "'en-us'", 'max_length': '5'}),
'nextrun': ('django.db.models.fields.DateField', [], {'null': 'True', 'blank': 'True'}),
'state': ('django.db.models.fields.CharField', [], {'default': "'inactive'", 'max_length': '16'}),
'surname': ('django.db.models.fields.CharField', [], {'max_length': '64', 'null': 'True', 'blank': 'True'})
}
}
complete_apps = ['questionnaire']

View File

547
questionnaire/models.py Normal file
View File

@ -0,0 +1,547 @@
import hashlib
import json
import re
import uuid
from datetime import datetime
from transmeta import TransMeta
from django.conf import settings
from django.contrib.contenttypes.generic import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.signals import post_save
from django.utils.translation import ugettext_lazy as _
from . import QuestionChoices
from .utils import split_numal
from .parsers import parse_checks, ParseException
_numre = re.compile("(\d+)([a-z]+)", re.I)
class Subject(models.Model):
STATE_CHOICES = [
("active", _("Active")),
("inactive", _("Inactive")),
# Can be changed from elsewhere with
# Subject.STATE_CHOICES[:] = [ ('blah', 'Blah') ]
]
state = models.CharField(max_length=16, default="inactive",
choices = STATE_CHOICES, verbose_name=_('State'))
anonymous = models.BooleanField(default=False)
ip_address = models.GenericIPAddressField(null=True, blank=True)
surname = models.CharField(max_length=64, blank=True, null=True,
verbose_name=_('Surname'))
givenname = models.CharField(max_length=64, blank=True, null=True,
verbose_name=_('Given name'))
email = models.EmailField(null=True, blank=True, verbose_name=_('Email'))
gender = models.CharField(max_length=8, default="unset", blank=True,
verbose_name=_('Gender'),
choices = ( ("unset", _("Unset")),
("male", _("Male")),
("female", _("Female")),
)
)
nextrun = models.DateField(verbose_name=_('Next Run'), blank=True, null=True)
formtype = models.CharField(max_length=16, default='email',
verbose_name = _('Form Type'),
choices = (
("email", _("Subject receives emails")),
("paperform", _("Subject is sent paper form"),))
)
language = models.CharField(max_length=5, default=settings.LANGUAGE_CODE,
verbose_name = _('Language'), choices = settings.LANGUAGES)
def __unicode__(self):
if self.anonymous:
return self.ip_address
else:
return u'%s, %s (%s)' % (self.surname, self.givenname, self.email)
def next_runid(self):
"Return the string form of the runid for the upcoming run"
return str(self.nextrun.year)
def last_run(self):
"Returns the last completed run or None"
try:
query = RunInfoHistory.objects.filter(subject=self)
return query.order_by('-completed')[0]
except IndexError:
return None
def history(self):
return RunInfoHistory.objects.filter(subject=self).order_by('runid')
def pending(self):
return RunInfo.objects.filter(subject=self).order_by('runid')
''' class Meta:
index_together = [
["givenname", "surname"],
]
'''
class GlobalStyles(models.Model):
content = models.TextField()
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)
def __unicode__(self):
return self.name
def questionsets(self):
if not hasattr(self, "__qscache"):
self.__qscache = \
QuestionSet.objects.filter(questionnaire=self).order_by('sortid')
return self.__qscache
def questions(self):
questions = []
for questionset in self.questionsets():
questions += questionset.questions()
return questions
class Meta:
permissions = (
("export", "Can export questionnaire answers"),
("management", "Management Tools")
)
class Landing(models.Model):
# defines an entry point to a Feedback session
nonce = models.CharField(max_length=32, null=True,blank=True)
content_type = models.ForeignKey(ContentType, null=True,blank=True, related_name='landings')
object_id = models.PositiveIntegerField(null=True,blank=True)
content_object = GenericForeignKey('content_type', 'object_id')
label = models.CharField(max_length=64, blank=True)
questionnaire = models.ForeignKey(Questionnaire, null=True, blank=True, related_name='landings')
def _hash(self):
return uuid.uuid4().hex
def __str__(self):
return self.label
def config_landing(sender, instance, created, **kwargs):
if created:
instance.nonce=instance._hash()
instance.save()
post_save.connect(config_landing,sender=Landing)
class DBStylesheet(models.Model):
#Questionnaire max length of name is 128; Questionset max length of heading
#is 64, and Question associative information is id which is less than 128
#in length
inclusion_tag = models.CharField(max_length=128)
content = models.TextField()
def __unicode__(self):
return self.inclusion_tag
class QuestionSet(models.Model):
__metaclass__ = TransMeta
"Which questions to display on a question page"
questionnaire = models.ForeignKey(Questionnaire)
sortid = models.IntegerField() # used to decide which order to display in
heading = models.CharField(max_length=64)
checks = models.CharField(max_length=256, blank=True,
help_text = """Current options are 'femaleonly' or 'maleonly' and shownif="QuestionNumber,Answer" which takes the same format as <tt>requiredif</tt> for questions.""")
text = models.TextField(u'Text', help_text="HTML or Text")
parse_html = models.BooleanField("Render html in heading?", null=False, default=False)
def questions(self):
if not hasattr(self, "__qcache"):
def numeric_number(val):
matches = re.findall(r'^\d+', val)
return int(matches[0]) if matches else 0
questions_with_sort_id = sorted(Question.objects.filter(questionset=self.id).exclude(sort_id__isnull=True), key=lambda q: q.sort_id)
questions_with_out_sort_id = sorted(Question.objects.filter(questionset=self.id, sort_id__isnull=True), key=lambda q: (numeric_number(q.number), q.number))
self.__qcache = questions_with_sort_id + questions_with_out_sort_id
return self.__qcache
def sorted_questions(self):
questions = self.questions()
return sorted(questions, key = lambda question : (question.sort_id, question.number))
def next(self):
qs = self.questionnaire.questionsets()
retnext = False
for q in qs:
if retnext:
return q
if q == self:
retnext = True
return None
def prev(self):
qs = self.questionnaire.questionsets()
last = None
for q in qs:
if q == self:
return last
last = q
def is_last(self):
try:
return self.questionnaire.questionsets()[-1] == self
except NameError:
# should only occur if not yet saved
return True
def is_first(self):
try:
return self.questionnaire.questionsets()[0] == self
except NameError:
# should only occur if not yet saved
return True
def __unicode__(self):
return u'%s: %s' % (self.questionnaire.name, self.heading)
class Meta:
translate = ('text',)
'''index_together = [
["questionnaire", "sortid"],
["sortid",]
]
'''
class RunInfo(models.Model):
"Store the active/waiting questionnaire runs here"
subject = models.ForeignKey(Subject)
random = models.CharField(max_length=32) # probably a randomized md5sum
runid = models.CharField(max_length=32)
landing = models.ForeignKey(Landing, null=True, blank=True)
# questionset should be set to the first QuestionSet initially, and to null on completion
# ... although the RunInfo entry should be deleted then anyway.
questionset = models.ForeignKey(QuestionSet, blank=True, null=True) # or straight int?
emailcount = models.IntegerField(default=0)
created = models.DateTimeField(auto_now_add=True)
emailsent = models.DateTimeField(null=True, blank=True)
lastemailerror = models.CharField(max_length=64, null=True, blank=True)
state = models.CharField(max_length=16, null=True, blank=True)
cookies = models.TextField(null=True, blank=True)
tags = models.TextField(
blank=True,
help_text=u"Tags active on this run, separated by commas"
)
skipped = models.TextField(
blank=True,
help_text=u"A comma sepearted list of questions to skip"
)
def save(self, **kwargs):
self.random = (self.random or '').lower()
super(RunInfo, self).save(**kwargs)
def add_tags(self, tags):
for tag in tags:
if self.tags:
self.tags += ','
self.tags += tag
def remove_tags(self, tags):
if not self.tags:
return
current_tags = self.tags.split(',')
for tag in tags:
try:
current_tags.remove(tag)
except ValueError:
pass
self.tags = ",".join(current_tags)
def set_cookie(self, key, value):
"runinfo.set_cookie(key, value). If value is None, delete cookie"
key = key.lower().strip()
cookies = self.get_cookiedict()
if type(value) not in (int, float, str, unicode, type(None)):
raise Exception("Can only store cookies of type integer or string")
if value is None:
if key in cookies:
del cookies[key]
else:
if type(value) in ('int', 'float'):
value=str(value)
cookies[key] = value
cstr = json.dumps(cookies)
self.cookies=cstr
self.save()
self.__cookiecache = cookies
def get_cookie(self, key, default=None):
if not self.cookies:
return default
d = self.get_cookiedict()
return d.get(key.lower().strip(), default)
def get_cookiedict(self):
if not self.cookies:
return {}
if not hasattr(self, '__cookiecache'):
self.__cookiecache = json.loads(self.cookies)
return self.__cookiecache
def __unicode__(self):
return "%s: %s, %s" % (self.runid, self.subject.surname, self.subject.givenname)
class Meta:
verbose_name_plural = 'Run Info'
'''index_together = [
["random"],
]
'''
class RunInfoHistory(models.Model):
subject = models.ForeignKey(Subject)
runid = models.CharField(max_length=32)
completed = models.DateTimeField()
landing = models.ForeignKey(Landing, null=True, blank=True)
tags = models.TextField(
blank=True,
help_text=u"Tags used on this run, separated by commas"
)
skipped = models.TextField(
blank=True,
help_text=u"A comma sepearted list of questions skipped by this run"
)
questionnaire = models.ForeignKey(Questionnaire)
def __unicode__(self):
return "%s: %s on %s" % (self.runid, self.subject, self.completed)
def answers(self):
"Returns the query for the answers."
return Answer.objects.filter(subject=self.subject, runid=self.runid)
class Meta:
verbose_name_plural = 'Run Info History'
class Question(models.Model):
__metaclass__ = TransMeta
questionset = models.ForeignKey(QuestionSet)
number = models.CharField(max_length=8, help_text=
"eg. <tt>1</tt>, <tt>2a</tt>, <tt>2b</tt>, <tt>3c</tt><br /> "
"Number is also used for ordering questions.")
sort_id = models.IntegerField(null=True, blank=True, help_text="Questions within a questionset are sorted by sort order first, question number second")
text = models.TextField(blank=True, verbose_name=_("Text"))
type = models.CharField(u"Type of question", max_length=32,
choices = QuestionChoices,
help_text = u"Determines the means of answering the question. " \
"An open question gives the user a single-line textfield, " \
"multiple-choice gives the user a number of choices he/she can " \
"choose from. If a question is multiple-choice, enter the choices " \
"this user can choose from below'.")
extra = models.CharField(u"Extra information", max_length=512, blank=True, null=True, help_text=u"Extra information (use on question type)")
checks = models.CharField(u"Additional checks", max_length=512, blank=True,
null=True, help_text="Additional checks to be performed for this "
"value (space separated) <br /><br />"
"For text fields, <tt>required</tt> is a valid check.<br />"
"For yes/no choice, <tt>required</tt>, <tt>required-yes</tt>, "
"and <tt>required-no</tt> are valid.<br /><br />"
"If this question is required only if another question's answer is "
'something specific, use <tt>requiredif="QuestionNumber,Value"</tt> '
'or <tt>requiredif="QuestionNumber,!Value"</tt> for anything but '
"a specific value. "
"You may also combine tests appearing in <tt>requiredif</tt> "
"by joining them with the words <tt>and</tt> or <tt>or</tt>, "
'eg. <tt>requiredif="Q1,A or Q2,B"</tt>')
footer = models.TextField(u"Footer", help_text="Footer rendered below the question", blank=True)
parse_html = models.BooleanField("Render html in Footer?", null=False, default=False)
def questionnaire(self):
return self.questionset.questionnaire
def getcheckdict(self):
"""getcheckdict returns a dictionary of the values in self.checks"""
if(hasattr(self, '__checkdict_cached')):
return self.__checkdict_cached
try:
self.__checkdict_cached = d = parse_checks(self.sameas().checks or '')
except ParseException:
raise Exception("Error Parsing Checks for Question %s: %s" % (
self.number, self.sameas().checks))
return d
def __unicode__(self):
return u'{%s} (%s) %s' % (unicode(self.questionset), self.number, self.text)
def sameas(self):
if self.type == 'sameas':
try:
kwargs = {}
for check, value in parse_checks(self.checks):
if check == 'sameasid':
kwargs['id'] = value
break
elif check == 'sameas':
kwargs['number'] = value
kwargs['questionset__questionnaire'] = self.questionset.questionnaire
break
self.__sameas = res = getattr(self, "__sameas", Question.objects.get(**kwargs))
return res
except Question.DoesNotExist:
return Question(type='comment') # replace with something benign
return self
def display_number(self):
"Return either the number alone or the non-number part of the question number indented"
m = _numre.match(self.number)
if m:
sub = m.group(2)
return "&nbsp;&nbsp;&nbsp;" + sub
return self.number
def choices(self):
if self.type == 'sameas':
return self.sameas().choices()
res = None
if 'samechoicesas' in parse_checks(self.checks):
number_to_grab_from = parse_checks(self.checks)['samechoicesas']
choicesource = Question.objects.get(number=number_to_grab_from)
if not choicesource == None:
res = Choice.objects.filter(question=choicesource).order_by('sortid')
else:
res = Choice.objects.filter(question=self).order_by('sortid')
return res
def is_custom(self):
return "custom" == self.sameas().type
def get_type(self):
"Get the type name, treating sameas and custom specially"
t = self.sameas().type
if t == 'custom':
cd = self.sameas().getcheckdict()
if 'type' not in cd:
raise Exception("When using custom types, you must have type=<name> in the additional checks field")
return cd.get('type')
return t
def questioninclude(self):
return "questionnaire/" + self.get_type() + ".html"
@property
def is_comment(self):
return self.type == 'comment'
# def __cmp__(a, b):
# anum, astr = split_numal(a.number)
# bnum, bstr = split_numal(b.number)
# cmpnum = cmp(anum, bnum)
# return cmpnum or cmp(astr, bstr)
def get_value_for_run_question(self, runid):
runanswer = Answer.objects.filter(runid=runid,question=self)
if len(runanswer) > 0:
return runanswer[0].answer
else:
return None
class Meta:
translate = ('text', 'extra', 'footer')
'''index_together = [
["number", "questionset"],
]
'''
class Choice(models.Model):
__metaclass__ = TransMeta
question = models.ForeignKey(Question)
sortid = models.IntegerField()
value = models.CharField(u"Short Value", max_length=64)
text = models.CharField(u"Choice Text", max_length=200)
tags = models.CharField(u"Tags", max_length=64, blank=True)
def __unicode__(self):
return u'(%s) %d. %s' % (self.question.number, self.sortid, self.text)
class Meta:
translate = ('text',)
'''index_together = [
['value'],
]
'''
class Answer(models.Model):
subject = models.ForeignKey(Subject, help_text = u'The user who supplied this answer')
question = models.ForeignKey(Question, help_text = u"The question that this is an answer to")
runid = models.CharField(u'RunID', help_text = u"The RunID (ie. year)", max_length=32)
answer = models.TextField()
def __unicode__(self):
return "Answer(%s: %s, %s)" % (self.question.number, self.subject.surname, self.subject.givenname)
def split_answer(self):
"""
Decode stored answer value and return as a list of choices.
Any freeform value will be returned in a list as the last item.
Calling code should be tolerant of freeform answers outside
of additional [] if data has been stored in plain text format
"""
try:
return json.loads(self.answer)
except ValueError:
# this was likely saved as plain text, try to guess what the
# value(s) were
if 'multiple' in self.question.type:
return self.answer.split('; ')
else:
return [self.answer]
def check_answer(self):
"Confirm that the supplied answer matches what we expect"
return True
def save(self, runinfo=None, **kwargs):
self._update_tags(runinfo)
super(Answer, self).save(**kwargs)
def _update_tags(self, runinfo):
if not runinfo:
return
tags_to_add = []
for choice in self.question.choices():
tags = choice.tags
if not tags:
continue
tags = tags.split(',')
runinfo.remove_tags(tags)
for split_answer in self.split_answer():
if unicode(split_answer) == choice.value:
tags_to_add.extend(tags)
runinfo.add_tags(tags_to_add)
runinfo.save()
'''class Meta:
index_together = [
['subject', 'runid'],
['subject', 'runid', 'id'],
]
'''

View File

@ -0,0 +1,5 @@
#!/usr/bin/python
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

View File

@ -0,0 +1,8 @@
from django.contrib import admin
from .models import Page
class PageAdmin(admin.ModelAdmin):
list_display = ('slug', 'title',)
admin.site.register(Page, PageAdmin)

View File

@ -0,0 +1,21 @@
from django.db import models
from django.core.urlresolvers import reverse
from transmeta import TransMeta
class Page(models.Model):
__metaclass__ = TransMeta
slug = models.SlugField(unique=True, primary_key=True)
title = models.CharField(u"Title", max_length=256)
body = models.TextField(u"Body")
public = models.BooleanField(default=True)
def __unicode__(self):
return u"Page[%s]" % self.slug
def get_absolute_url(self):
return reverse('questionnaire.page.views.page', kwargs={'page_to_render':self.slug})
class Meta:
translate = ('title','body',)

View File

@ -0,0 +1,42 @@
# Create your views here.
from django.shortcuts import render_to_response
from django.conf import settings
from django.template import RequestContext
from django import http
from django.utils import translation
from .models import Page
def page(request, page_to_render):
try:
p = Page.objects.get(slug=page_to_render, public=True)
except Page.DoesNotExist:
return render_to_response("pages/{}.html".format(page_to_render),
{ "request" : request,},
context_instance = RequestContext(request)
)
return render_to_response("page.html",
{ "request" : request, "page" : p, },
context_instance = RequestContext(request)
)
def langpage(request, lang, page_to_trans):
translation.activate(lang)
return page(request, page_to_trans)
def set_language(request):
next = request.REQUEST.get('next', None)
if not next:
next = request.META.get('HTTP_REFERER', None)
if not next:
next = '/'
response = http.HttpResponseRedirect(next)
if request.method == 'GET':
lang_code = request.GET.get('language', None)
if lang_code and translation.check_for_language(lang_code):
if hasattr(request, 'session'):
request.session['django_language'] = lang_code
else:
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, lang_code)
return response

142
questionnaire/parsers.py Normal file
View File

@ -0,0 +1,142 @@
#!/usr/bin/python
__all__ = ('parse_checks', 'BooleanParser')
try: from pyparsing import *
except ImportError: from utils.pyparsing import *
def __make_parser():
key = Word(alphas, alphanums+"_-")
value = Word(alphanums + "-.,_=<>!@$%^&*[]{}:;|/'") | QuotedString('"')
return Dict(ZeroOrMore(Group( key + Optional( Suppress("=") + value, default=True ) ) ))
__checkparser = __make_parser()
def parse_checks(string):
"""
from parsers import parse_checks
>>> parse_checks('dependent=5a,no dependent="5a && 4a" dog="Roaming Rover" name=Robert foo bar')
([(['dependent', '5a,no'], {}), (['dependent', '5a && 4a'], {}), (['dog', 'Roaming Rover'], {}), (['name', 'Robert'], {}), (['foo', True], {}), (['bar', True], {})], {'dependent': [('5a,no', 0), ('5a && 4a', 1)], 'foo': [(True, 4)], 'bar': [(True, 5)], 'dog': [('Roaming Rover', 2)], 'name': [('Robert', 3)]})
"""
return __checkparser.parseString(string, parseAll=True)
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-
# - Boolean Expression Parser -
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-
class BoolOperand(object):
def __init__(self, t):
self.args = t[0][0::2]
def __str__(self):
sep = " %s " % self.reprsymbol
return "(" + sep.join(map(str, self.args)) + ")"
class BoolAnd(BoolOperand):
reprsymbol = '&&'
def __nonzero__(self):
for a in self.args:
if not bool(a):
return False
return True
class BoolOr(BoolOperand):
reprsymbol = '||'
def __nonzero__(self):
for a in self.args:
if bool(a):
return True
return False
class BoolNot(BoolOperand):
def __init__(self,t):
self.arg = t[0][1]
def __str__(self):
return "!" + str(self.arg)
def __nonzero__(self):
return not bool(self.arg)
class Checker(object):
"Simple wrapper to call a specific function, passing in args and kwargs each time"
def __init__(self, func, expr, *args, **kwargs):
self.func = func
self.expr = expr
self.args = args
self.kwargs = kwargs
def __nonzero__(self):
return self.func(self.expr, *self.args, **self.kwargs)
def __hash__(self):
return hash(self.expr)
def __unicode__(self):
try: fname=self.func.func_name
except: fname="TestExpr"
return "%s('%s')" % (fname, self.expr)
__str__ = __unicode__
class BooleanParser(object):
"""Simple boolean parser
>>> def foo(x):
... if x == '1': return True
... return False
...
>>> foo('1')
True
>>> foo('0')
False
>>> p = BooleanParser(foo)
>>> p.parse('1 and 0')
False
>>> p.parse('1 and 1')
True
>>> p.parse('1 or 1')
True
>>> p.parse('0 or 1')
True
>>> p.parse('0 or 0')
False
>>> p.parse('(0 or 0) and 1')
False
>>> p.parse('(0 or 0) and (1)')
False
>>> p.parse('(0 or 1) and (1)')
True
>>> p.parse('(0 or 0) or (1)')
True
"""
def __init__(self, func, *args, **kwargs): # treats kwarg boolOperand specially!
self.args = args
self.kwargs = kwargs
self.func = func
if "boolOperand" in kwargs:
boolOperand = kwargs["boolOperand"]
del kwargs["boolOperand"]
else:
boolOperand = Word(alphanums + "-.,_=<>!@$%^&*[]{}:;|/\\")
boolOperand = boolOperand.setParseAction(self._check)
self.boolExpr = operatorPrecedence( boolOperand,
[
("not ", 1, opAssoc.RIGHT, BoolNot),
("or", 2, opAssoc.LEFT, BoolOr),
("and", 2, opAssoc.LEFT, BoolAnd),
])
def _check(self, string, location, tokens):
checker = Checker(self.func, tokens[0], *self.args, **self.kwargs)
tokens[0] = checker
def parse(self, code):
if not code or not code.strip():
return False
return bool(self.boolExpr.parseString(code)[0])
def toString(self, code):
return str(self.boolExpr.parseString(code)[0])
if __name__ == '__main__':
import doctest
doctest.testmod()

68
questionnaire/profiler.py Normal file
View File

@ -0,0 +1,68 @@
import hotshot
import os
import time
from django.conf import settings
from django.db import connection
try:
PROFILE_LOG_BASE = settings.PROFILE_LOG_BASE
except:
PROFILE_LOG_BASE = "/tmp"
def profile(log_file):
"""Profile some callable.
This decorator uses the hotshot profiler to profile some callable (like
a view function or method) and dumps the profile data somewhere sensible
for later processing and examination.
It takes one argument, the profile log name. If it's a relative path, it
places it under the PROFILE_LOG_BASE. It also inserts a time stamp into the
file name, such that 'my_view.prof' become 'my_view-20100211T170321.prof',
where the time stamp is in UTC. This makes it easy to run and compare
multiple trials.
"""
if not os.path.isabs(log_file):
log_file = os.path.join(PROFILE_LOG_BASE, log_file)
def _outer(f):
def _inner(*args, **kwargs):
# Add a timestamp to the profile output when the callable
# is actually called.
(base, ext) = os.path.splitext(log_file)
base = base + "-" + time.strftime("%Y%m%dT%H%M%S", time.gmtime())
final_log_file = base + ext
prof = hotshot.Profile(final_log_file)
try:
ret = prof.runcall(f, *args, **kwargs)
finally:
prof.close()
return ret
return _inner
return _outer
def timethis(fn):
def wrapper(*args, **kwargs):
start = time.time()
result = fn(*args, **kwargs)
print fn.__name__, 'took', time.time() - start
return result
return wrapper
from pprint import pprint
def sqlprint(fn):
def wrapper(*args, **kwargs):
connection.queries = list()
result = fn(*args, **kwargs)
print fn.__name__, 'issued'
pprint(connection.queries)
return result
return wrapper

View File

@ -0,0 +1,10 @@
# add all the question types to QuestionChoices before anything else
from .. import add_type
from . import simple # store value as returned
from . import choice # multiple choice, do checks
from . import range_or_number # range of numbers
from . import timeperiod # time periods
from . import custom # backwards compatibility support
add_type('custom', 'Custom field')

View File

@ -0,0 +1,219 @@
from json import dumps
import ast
from django.utils.translation import ugettext as _, ungettext
from .. import add_type, question_proc, answer_proc, AnswerException
from ..utils import get_runid_from_request
@question_proc('choice', 'choice-freeform', 'dropdown')
def question_choice(request, question):
choices = []
jstriggers = []
cd = question.getcheckdict()
key = "question_%s" % question.number
key2 = "question_%s_comment" % question.number
val = None
possibledbvalue = question.get_value_for_run_question(get_runid_from_request(request))
if key in request.POST:
val = request.POST[key]
elif not possibledbvalue == None:
valueaslist = ast.literal_eval(possibledbvalue)
val = valueaslist[0]
else:
if 'default' in cd:
val = cd['default']
for choice in question.choices():
choices.append( ( choice.value == val, choice, ) )
if question.type == 'choice-freeform':
jstriggers.append('%s_comment' % question.number)
return {
'choices' : choices,
'sel_entry' : val == '_entry_',
'qvalue' : val or '',
'required' : True,
'comment' : request.POST.get(key2, ""),
'jstriggers': jstriggers,
}
@answer_proc('choice', 'choice-freeform', 'dropdown')
def process_choice(question, answer):
opt = answer['ANSWER'] or ''
if not opt:
raise AnswerException(_(u'You must select an option'))
if opt == '_entry_' and question.type == 'choice-freeform':
opt = answer.get('comment','')
if not opt:
raise AnswerException(_(u'Field cannot be blank'))
return dumps([[opt]])
else:
valid = [c.value for c in question.choices()]
if opt not in valid:
raise AnswerException(_(u'Invalid option!'))
return dumps([opt])
add_type('choice', 'Choice [radio]')
add_type('choice-freeform', 'Choice with a freeform option [radio]')
add_type('dropdown', 'Dropdown choice [select]')
@question_proc('choice-multiple', 'choice-multiple-freeform', 'choice-multiple-values')
def question_multiple(request, question):
key = "question_%s" % question.number
choices = []
jstriggers = []
counter = 0
qvalues = []
cd = question.getcheckdict()
defaults = cd.get('default','').split(',')
possibledbvalue = question.get_value_for_run_question(get_runid_from_request(request))
possiblelist = []
if not possibledbvalue == None:
possiblelist = ast.literal_eval(possibledbvalue)
prev_vals = {}
if question.type == 'choice-multiple-values':
pl = []
for choice_value, prev_value in possiblelist:
pl.append(choice_value)
prev_vals[choice_value] = str(prev_value)
possiblelist = pl
# print 'possible value is ', possibledbvalue, ', possiblelist is ', possiblelist
for choice in question.choices():
counter += 1
key = "question_%s_multiple_%d" % (question.number, choice.sortid)
if question.type == "choice-multiple-values":
jstriggers.append("q%s_%s_box" % (question.number, choice.value))
# so that the number box will be activated when item is checked
#try database first and only after that fall back to post choices
# print 'choice multiple checking for match for choice ', choice
checked = ' checked'
prev_value = ''
qvalue = "%s_%s" % (question.number, choice.value)
if key in request.POST or \
(request.method == 'GET' and choice.value in defaults):
qvalues.append(qvalue)
value_key = "question_%s_%s_value" % (question.number, choice.value)
if value_key in request.POST:
prev_value = request.POST[value_key]
elif choice.value in possiblelist:
qvalues.append(qvalue)
# so that this choice being checked will trigger anything that depends on it -
# for choice-multiple-values right now
if choice.value in prev_vals.keys():
prev_value = prev_vals[choice.value]
else:
checked = ''
# bug: you can have one item checked from database and another from POST data
choices.append( (choice, key, checked, prev_value,) )
extracount = int(cd.get('extracount', 0))
if not extracount and question.type == 'choice-multiple-freeform':
extracount = 1
extras = []
for x in range(1, extracount+1):
key = "question_%s_more%d" % (question.number, x)
if key in request.POST:
extras.append( (key, request.POST[key],) )
else:
extras.append( (key, '',) )
# right now does not retrieve extra fields from database
return {
"choices": choices,
"extras": extras,
"type": question.type,
"template" : "questionnaire/choice-multiple-freeform.html",
"required" : cd.get("required", False) and cd.get("required") != "0",
"jstriggers": jstriggers,
"qvalues": qvalues
}
@answer_proc('choice-multiple', 'choice-multiple-freeform', 'choice-multiple-values')
def process_multiple(question, answer):
multiple = []
multiple_freeform = []
#this is the same thing as a minimum count so use it for that..
requiredcount = 0
required = question.getcheckdict().get('required', 0)
if required:
try:
requiredcount = int(required)
except ValueError:
requiredcount = 1
if requiredcount and requiredcount > question.choices().count():
requiredcount = question.choices().count()
#added support for a max number of choices
maxcount = 9999
maxdict = question.getcheckdict().get('max', 0)
if maxdict:
try:
maxcount = int(maxdict)
except ValueError:
pass #leave maxcount at 9999
if maxcount and maxcount > question.choices().count():
maxcount = question.choices().count()
for k, v in answer.items():
if k.startswith('multiple') and not k.endswith('value'):
multiple.append(v)
if k.startswith('more') and len(v.strip()) > 0:
multiple_freeform.append(v)
if question.type == 'choice-multiple-values':
# check for associated values for choice-multiple-values
total = 0
floats = False
notnumbers = False
for k, v in answer.items():
if k.endswith('value'):
choice_text = k.rsplit("_", 2)[0]
if choice_text in multiple:
val = v
multiple.remove(choice_text)
try:
val = int(v)
total += val
except ValueError: # not an int
try:
val = float(v)
floats = True
except ValueError: # not a float or int
notnumbers = True
multiple.append([choice_text, val])
if floats:
raise AnswerException(ungettext(u"Please enter a whole number - no decimal places", u"Please enter whole numbers - no decimal places", len(multiple)))
if notnumbers:
raise AnswerException(ungettext(u"Value must be a number (with no decimal places)", u"All values must be numbers (with no decimal places)", len(multiple)))
if len(multiple) > 0 and total != 100:
raise AnswerException(ungettext(u"Did you mean 100% for one choice? Please enter 100 or add other choices.", u"Values must add up to 100.", len(multiple)))
if len(multiple) + len(multiple_freeform) < requiredcount:
raise AnswerException(ungettext(u"You must select at least %d option",
u"You must select at least %d options",
requiredcount) % requiredcount)
if len(multiple) + len(multiple_freeform) > maxcount:
raise AnswerException(ungettext(u"You must select at most %d options",
u"You must select at most %d options",
maxcount) % maxcount)
multiple.sort()
if multiple_freeform:
multiple.append(multiple_freeform)
return dumps(multiple)
add_type('choice-multiple', 'Multiple-Choice, Multiple-Answers [checkbox]')
add_type('choice-multiple-freeform', 'Multiple-Choice, Multiple-Answers, plus freeform [checkbox, input]')
add_type('choice-multiple-values', 'Multiple-Choice, Multiple-Answers [checkboxes], plus value box [input] for each selected answer')

View File

@ -0,0 +1,28 @@
#
# Custom type exists for backwards compatibility. All custom types should now
# exist in the drop down list of the management interface.
#
from django.utils.translation import ugettext as _
from .. import add_type, question_proc, answer_proc, AnswerException
from .. import Processors, QuestionProcessors
@question_proc('custom')
def question_custom(request, question):
cd = question.getcheckdict()
_type = cd['type']
d = {}
if _type in QuestionProcessors:
d = QuestionProcessors[_type](request, question)
if 'template' not in d:
d['template'] = 'questionnaire/%s.html' % _type
return d
@answer_proc('custom')
def process_custom(question, answer):
cd = question.getcheckdict()
_type = cd['type']
if _type in Processors:
return Processors[_type](question, answer)
raise AnswerException(_(u"Processor not defined for this question"))

View File

@ -0,0 +1,107 @@
import ast
from json import dumps
from django.conf import settings
from django.utils.translation import ugettext as _
from ..utils import get_runid_from_request
from .. import add_type, question_proc, answer_proc, AnswerException
@question_proc('range', 'number')
def question_range_or_number(request, question):
cd = question.getcheckdict()
rmin, rmax = parse_range(cd)
rstep = parse_step(cd)
runit = cd.get('unit', '')
#try loading current from database before just setting to min
possibledbvalue = question.get_value_for_run_question(get_runid_from_request(request))
#you can't eval none nor can you eval empty
if not possibledbvalue == None and len(possibledbvalue) > 0:
valueaslist = ast.literal_eval(possibledbvalue)
current = valueaslist[0]
else:
current = request.POST.get('question_%s' % question.number, rmin)
jsinclude = []
if question.type == 'range':
jsinclude = [settings.STATIC_URL+'range.js']
return {
'required' : True,
'type': question.type,
'rmin' : rmin,
'rmax' : rmax,
'rstep' : rstep,
'runit' : runit,
'current' : current,
'jsinclude' : jsinclude
}
@answer_proc('range', 'number')
def process_range_or_number(question, answer):
cd = question.getcheckdict()
rmin, rmax = parse_range(cd)
rstep = parse_step(cd)
convert = range_type(rmin, rmax, rstep)
ans = answer['ANSWER']
if not ans:
required = question.getcheckdict().get('required', 0)
if required:
raise AnswerException(_(u"Field cannot be blank"))
else:
return []
try:
ans = convert(ans)
except:
raise AnswerException(_(u"Could not convert the number"))
if ans > convert(rmax) or ans < convert(rmin):
raise AnswerException(_(u"Out of range"))
return dumps([ans])
add_type('range', 'Range of numbers [select]')
add_type('number', 'Number [input]')
def parse_range(checkdict):
"Given a checkdict for a range widget return the min and max string values."
Range = checkdict.get('range', '1-5')
try:
rmin, rmax = Range.split('-', 1)
except ValueError:
rmin, rmax = '1', '5'
return rmin, rmax
def parse_step(checkdict):
"Given a checkdict for a range widget return the step as string value."
return checkdict.get('step', '1')
def range_type(rmin, rmax, step):
"""Given the min, max and step value return float or int depending on
the number of digits after 0.
"""
if any((digits(rmin), digits(rmax), digits(step))):
return float
else:
return int
def digits(number):
"Given a number as string return the number of digits after 0."
if '.' in number or ',' in number:
if '.' in number:
return len(number.split('.')[1])
else:
return len(number.split(',')[1])
else:
return 0

View File

@ -0,0 +1,134 @@
import ast
from json import dumps
from django.utils.translation import ugettext as _
from .. import add_type, question_proc, answer_proc, AnswerException
from ..utils import get_runid_from_request
#true if either 'required' or if 'requiredif' is satisfied
#def is_required
@question_proc('choice-yesno', 'choice-yesnocomment', 'choice-yesnodontknow')
def question_yesno(request, question):
key = "question_%s" % question.number
key2 = "question_%s_comment" % question.number
val = request.POST.get(key, None)
cmt = request.POST.get(key2, '')
qtype = question.get_type()
cd = question.getcheckdict()
jstriggers = []
if qtype == 'choice-yesnocomment':
hascomment = True
else:
hascomment = False
if qtype == 'choice-yesnodontknow' or 'dontknow' in cd:
hasdontknow = True
else:
hasdontknow = False
#try the database before reverting to default
possiblevalue = question.get_value_for_run_question(get_runid_from_request(request))
if not possiblevalue == None:
#save process always listifies the answer so we unlistify it to put it back in the field
valueaslist = ast.literal_eval(possiblevalue)
if len(valueaslist) > 0:
val = valueaslist[0]
if not val:
if cd.get('default', None):
val = cd['default']
checks = ''
if hascomment:
if cd.get('required-yes'):
jstriggers = ['%s_comment' % question.number]
checks = ' checks="dep_check(\'%s,yes\')"' % question.number
elif cd.get('required-no'):
checks = ' checks="dep_check(\'%s,no\')"' % question.number
elif cd.get('required-dontknow'):
checks = ' checks="dep_check(\'%s,dontknow\')"' % question.number
return {
'required': True,
'checks': checks,
'value': val,
'qvalue': val,
'hascomment': hascomment,
'hasdontknow': hasdontknow,
'comment': cmt,
'jstriggers': jstriggers,
'template': 'questionnaire/choice-yesnocomment.html',
}
@question_proc('open', 'open-textfield')
def question_open(request, question):
key = "question_%s" % question.number
value = question.getcheckdict().get('default', '')
if key in request.POST:
value = request.POST[key]
else:
#also try to get it from the database so we can handle back/forward in which post has been cleared
possiblevalue = question.get_value_for_run_question(get_runid_from_request(request))
if not possiblevalue == None:
#save process always listifies the answer so we unlistify it to put it back in the field
valueaslist = ast.literal_eval(possiblevalue)
if len(valueaslist) > 0:
value = valueaslist[0]
return {
'required': question.getcheckdict().get('required', False),
'value': value,
}
@answer_proc('open', 'open-textfield', 'choice-yesno', 'choice-yesnocomment', 'choice-yesnodontknow')
def process_simple(question, ansdict):
# print 'process_simple has question, ansdict ', question, ',', ansdict
checkdict = question.getcheckdict()
ans = ansdict['ANSWER'] or ''
qtype = question.get_type()
if qtype.startswith('choice-yesno'):
if ans not in ('yes', 'no', 'dontknow'):
raise AnswerException(_(u'You must select an option'))
if qtype == 'choice-yesnocomment' \
and len(ansdict.get('comment', '').strip()) == 0:
if checkdict.get('required', False):
raise AnswerException(_(u'Field cannot be blank'))
if checkdict.get('required-yes', False) and ans == 'yes':
raise AnswerException(_(u'Field cannot be blank'))
if checkdict.get('required-no', False) and ans == 'no':
raise AnswerException(_(u'Field cannot be blank'))
else:
#the key here is to note that requiredif has already been evaluated or we wouldn't have reached this point, so we don't have to recheck
if not ans.strip() and (checkdict.get('required', False) or checkdict.get('requiredif', False)):
raise AnswerException(_(u'Field cannot be blank'))
maxwords = checkdict.get('maxwords', False)
if maxwords:
maxwords = int(maxwords)
answords = len(ans.split())
if answords > maxwords:
raise AnswerException(_(u'Answer is ' + str(answords) + ' words. Please shorten answer to ' + str(maxwords) + ' words or less'))
if ansdict.has_key('comment') and len(ansdict['comment']) > 0:
return dumps([ans, [ansdict['comment']]])
if ans:
return dumps([ans])
return dumps([])
add_type('open', 'Open Answer, single line [input]')
add_type('open-textfield', 'Open Answer, multi-line [textarea]')
add_type('choice-yesno', 'Yes/No Choice [radio]')
add_type('choice-yesnocomment', 'Yes/No Choice with optional comment [radio, input]')
add_type('choice-yesnodontknow', 'Yes/No/Don\'t know Choice [radio]')
@answer_proc('comment')
def process_comment(question, answer):
pass
add_type('comment', 'Comment Only')

View File

@ -0,0 +1,62 @@
from django.utils.translation import ugettext as _, ugettext_lazy
from .. import add_type, question_proc, answer_proc, AnswerException
perioddict = {
"second" : ugettext_lazy("second(s)"),
"minute" : ugettext_lazy("minute(s)"),
"hour" : ugettext_lazy("hour(s)"),
"day" : ugettext_lazy("day(s)"),
"week" : ugettext_lazy("week(s)"),
"month" : ugettext_lazy("month(s)"),
"year" : ugettext_lazy("year(s)"),
}
@question_proc('timeperiod')
def question_timeperiod(request, question):
cd = question.getcheckdict()
if "units" in cd:
units = cd["units"].split(',')
else:
units = ["day","week","month","year"]
timeperiods = []
if not units:
units = ["day","week","month","year"]
key1 = "question_%s" % question.number
key2 = "question_%s_unit" % question.number
value = request.POST.get(key1, '')
unitselected = request.POST.get(key2, units[0])
for x in units:
if x in perioddict:
timeperiods.append( (x, unicode(perioddict[x]), unitselected==x) )
return {
"required" : "required" in cd,
"timeperiods" : timeperiods,
"value" : value,
}
@answer_proc('timeperiod')
def process_timeperiod(question, answer):
if not answer['ANSWER'] or not answer.has_key('unit'):
raise AnswerException(_(u"Invalid time period"))
period = answer['ANSWER'].strip()
if period:
try:
period = str(int(period))
except ValueError:
raise AnswerException(_(u"Time period must be a whole number"))
unit = answer['unit']
checkdict = question.getcheckdict()
if checkdict and 'units' in checkdict:
units = checkdict['units'].split(',')
else:
units = ('day', 'hour', 'week', 'month', 'year')
if not period and "required" in checkdict:
raise AnswerException(_(u'Field cannot be blank'))
if unit not in units:
raise AnswerException(_(u"Invalid time period"))
return "%s; %s" % (period, unit)
add_type('timeperiod', 'Time Period [input, select]')

View File

@ -0,0 +1,75 @@
# Per request cache middleware
# Provides a simple cache dictionary only existing for the request's lifetime
# The middleware provides a threadsafe LocMemCache which can be used just
# like any other django cache facility
from functools import wraps
from threading import currentThread
from django.core.cache.backends.locmem import LocMemCache
_request_cache = {}
_installed_middleware = False
def get_request_cache():
assert _installed_middleware, 'RequestCacheMiddleware not loaded'
return _request_cache[currentThread()]
def clear_request_cache():
_request_cache[currentThread()].clear()
class RequestCache(LocMemCache):
def __init__(self):
name = 'locmemcache@%i' % hash(currentThread())
params = dict()
super(RequestCache, self).__init__(name, params)
class RequestCacheMiddleware(object):
def __init__(self):
global _installed_middleware
_installed_middleware = True
def process_request(self, request):
cache = _request_cache.get(currentThread()) or RequestCache()
_request_cache[currentThread()] = cache
cache.clear()
class request_cache(object):
""" A decorator for use around functions that should be cached for the current
request. Use like this:
@request_cache()
def cached(name):
print "My name is %s and I'm cached" % name
@request_cache(keyfn=lambda p: p['id'])
def cached(param):
print "My id is %s" % p['id']
If no keyfn is provided the decorator expects the args to be hashable.
"""
def __init__(self, keyfn=None):
self.keyfn = keyfn or (lambda *args: hash(args))
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
cache = get_request_cache()
cachekey = self.keyfn(*args)
cacheval = cache.get(cachekey, 'expired')
if not cacheval == 'expired':
return cacheval
result = func(*args, **kwargs)
cache.set(cachekey, result)
return result
return wrapper

View File

@ -0,0 +1,356 @@
html,body{margin:0;padding:0;}
h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,cite,code,del,dfn,em,img,q,s,samp,small,strike,strong,sub,sup,tt,var,dd,dl,dt,li,ol,ul,fieldset,form,label,legend,button,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;font-weight:normal;font-style:normal;font-size:100%;line-height:1;font-family:inherit;}
table{border-collapse:collapse;border-spacing:0;}
ol,ul{list-style:none;}
q:before,q:after,blockquote:before,blockquote:after{content:"";}
html{overflow-y:scroll;font-size:100%;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;}
a:focus{outline:thin dotted;}
a:hover,a:active{outline:0;}
article,aside,details,figcaption,figure,footer,header,hgroup,nav,section{display:block;}
audio,canvas,video{display:inline-block;*display:inline;*zoom:1;}
audio:not([controls]){display:none;}
sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline;}
sup{top:-0.5em;}
sub{bottom:-0.25em;}
img{border:0;-ms-interpolation-mode:bicubic;}
button,input,select,textarea{font-size:100%;margin:0;vertical-align:baseline;*vertical-align:middle;}
button,input{line-height:normal;*overflow:visible;}
button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0;}
button,input[type="button"],input[type="reset"],input[type="submit"]{cursor:pointer;-webkit-appearance:button;}
input[type="search"]{-webkit-appearance:textfield;-webkit-box-sizing:content-box;-moz-box-sizing:content-box;box-sizing:content-box;}
input[type="search"]::-webkit-search-decoration{-webkit-appearance:none;}
textarea{overflow:auto;vertical-align:top;}
body{background-color:#ffffff;margin:0;font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:18px;color:#404040;}
.container{width:940px;margin-left:auto;margin-right:auto;zoom:1;}.container:before,.container:after{display:table;content:"";zoom:1;}
.container:after{clear:both;}
.container-fluid{position:relative;min-width:940px;padding-left:20px;padding-right:20px;zoom:1;}.container-fluid:before,.container-fluid:after{display:table;content:"";zoom:1;}
.container-fluid:after{clear:both;}
.container-fluid>.sidebar{position:absolute;top:0;left:20px;width:220px;}
.container-fluid>.content{margin-left:240px;}
a{color:#0069d6;text-decoration:none;line-height:inherit;font-weight:inherit;}a:hover{color:#00438a;text-decoration:underline;}
.pull-right{float:right;}
.pull-left{float:left;}
.hide{display:none;}
.show{display:block;}
.row{zoom:1;margin-left:-20px;}.row:before,.row:after{display:table;content:"";zoom:1;}
.row:after{clear:both;}
.row>[class*="span"]{display:inline;float:left;margin-left:20px;}
.span1{width:40px;}
.span2{width:100px;}
.span3{width:160px;}
.span4{width:220px;}
.span5{width:280px;}
.span6{width:340px;}
.span7{width:400px;}
.span8{width:460px;}
.span9{width:520px;}
.span10{width:580px;}
.span11{width:640px;}
.span12{width:700px;}
.span13{width:760px;}
.span14{width:820px;}
.span15{width:880px;}
.span16{width:940px;}
.span17{width:1000px;}
.span18{width:1060px;}
.span19{width:1120px;}
.span20{width:1180px;}
.span21{width:1240px;}
.span22{width:1300px;}
.span23{width:1360px;}
.span24{width:1420px;}
.row>.offset1{margin-left:80px;}
.row>.offset2{margin-left:140px;}
.row>.offset3{margin-left:200px;}
.row>.offset4{margin-left:260px;}
.row>.offset5{margin-left:320px;}
.row>.offset6{margin-left:380px;}
.row>.offset7{margin-left:440px;}
.row>.offset8{margin-left:500px;}
.row>.offset9{margin-left:560px;}
.row>.offset10{margin-left:620px;}
.row>.offset11{margin-left:680px;}
.row>.offset12{margin-left:740px;}
.span-one-third{width:300px;}
.span-two-thirds{width:620px;}
.row>.offset-one-third{margin-left:340px;}
.row>.offset-two-thirds{margin-left:660px;}
p{font-size:13px;font-weight:normal;line-height:18px;margin-bottom:9px;}p small{font-size:11px;color:#bfbfbf;}
h1,h2,h3,h4,h5,h6{font-weight:bold;color:#404040;}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small{color:#bfbfbf;}
h1{margin-bottom:18px;font-size:30px;line-height:36px;}h1 small{font-size:18px;}
h2{font-size:24px;line-height:36px;}h2 small{font-size:14px;}
h3,h4,h5,h6{line-height:36px;}
h3{font-size:18px;}h3 small{font-size:14px;}
h4{font-size:16px;}h4 small{font-size:12px;}
h5{font-size:14px;}
h6{font-size:13px;color:#bfbfbf;text-transform:uppercase;}
ul,ol{margin:0 0 18px 25px;}
ul ul,ul ol,ol ol,ol ul{margin-bottom:0;}
ul{list-style:disc;}
ol{list-style:decimal;}
li{line-height:18px;color:#808080;}
ul.unstyled{list-style:none;margin-left:0;}
dl{margin-bottom:18px;}dl dt,dl dd{line-height:18px;}
dl dt{font-weight:bold;}
dl dd{margin-left:9px;}
hr{margin:20px 0 19px;border:0;border-bottom:1px solid #eee;}
strong{font-style:inherit;font-weight:bold;}
em{font-style:italic;font-weight:inherit;line-height:inherit;}
.muted{color:#bfbfbf;}
blockquote{margin-bottom:18px;border-left:5px solid #eee;padding-left:15px;}blockquote p{font-size:14px;font-weight:300;line-height:18px;margin-bottom:0;}
blockquote small{display:block;font-size:12px;font-weight:300;line-height:18px;color:#bfbfbf;}blockquote small:before{content:'\2014 \00A0';}
address{display:block;line-height:18px;margin-bottom:18px;}
code,pre{padding:0 3px 2px;font-family:Monaco, Andale Mono, Courier New, monospace;font-size:12px;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
code{background-color:#fee9cc;color:rgba(0, 0, 0, 0.75);padding:1px 3px;}
pre{background-color:#f5f5f5;display:block;padding:8.5px;margin:0 0 18px;line-height:18px;font-size:12px;border:1px solid #ccc;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;white-space:pre;white-space:pre-wrap;word-wrap:break-word;}
form{margin-bottom:18px;}
fieldset{margin-bottom:18px;padding-top:18px;}fieldset legend{display:block;padding-left:150px;font-size:19.5px;line-height:1;color:#404040;*padding:0 0 5px 145px;*line-height:1.5;}
form .clearfix{margin-bottom:18px;zoom:1;}form .clearfix:before,form .clearfix:after{display:table;content:"";zoom:1;}
form .clearfix:after{clear:both;}
label,input,select,textarea{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:13px;font-weight:normal;line-height:normal;}
label{padding-top:6px;font-size:13px;line-height:18px;float:left;width:130px;text-align:right;color:#404040;}
form .input{margin-left:150px;}
input[type=checkbox],input[type=radio]{cursor:pointer;}
input,textarea,select,.uneditable-input{display:inline-block;width:210px;height:18px;padding:4px;font-size:13px;line-height:18px;color:#808080;border:1px solid #ccc;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;}
select{padding:initial;}
input[type=checkbox],input[type=radio]{width:auto;height:auto;padding:0;margin:3px 0;*margin-top:0;line-height:normal;border:none;}
input[type=file]{background-color:#ffffff;padding:initial;border:initial;line-height:initial;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
input[type=button],input[type=reset],input[type=submit]{width:auto;height:auto;}
select,input[type=file]{height:27px;*height:auto;line-height:27px;*margin-top:4px;}
select[multiple]{height:inherit;background-color:#ffffff;}
textarea{height:auto;}
.uneditable-input{background-color:#ffffff;display:block;border-color:#eee;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.025);cursor:not-allowed;}
:-moz-placeholder{color:#bfbfbf;}
::-webkit-input-placeholder{color:#bfbfbf;}
input,textarea{-webkit-transition:border linear 0.2s,box-shadow linear 0.2s;-moz-transition:border linear 0.2s,box-shadow linear 0.2s;-ms-transition:border linear 0.2s,box-shadow linear 0.2s;-o-transition:border linear 0.2s,box-shadow linear 0.2s;transition:border linear 0.2s,box-shadow linear 0.2s;-webkit-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);-moz-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1);}
input:focus,textarea:focus{outline:0;border-color:rgba(82, 168, 236, 0.8);-webkit-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);-moz-box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);box-shadow:inset 0 1px 3px rgba(0, 0, 0, 0.1),0 0 8px rgba(82, 168, 236, 0.6);}
input[type=file]:focus,input[type=checkbox]:focus,select:focus{-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;outline:1px dotted #666;}
form .clearfix.error>label,form .clearfix.error .help-block,form .clearfix.error .help-inline{color:#b94a48;}
form .clearfix.error input,form .clearfix.error textarea{color:#b94a48;border-color:#ee5f5b;}form .clearfix.error input:focus,form .clearfix.error textarea:focus{border-color:#e9322d;-webkit-box-shadow:0 0 6px #f8b9b7;-moz-box-shadow:0 0 6px #f8b9b7;box-shadow:0 0 6px #f8b9b7;}
form .clearfix.error .input-prepend .add-on,form .clearfix.error .input-append .add-on{color:#b94a48;background-color:#fce6e6;border-color:#b94a48;}
form .clearfix.warning>label,form .clearfix.warning .help-block,form .clearfix.warning .help-inline{color:#c09853;}
form .clearfix.warning input,form .clearfix.warning textarea{color:#c09853;border-color:#ccae64;}form .clearfix.warning input:focus,form .clearfix.warning textarea:focus{border-color:#be9a3f;-webkit-box-shadow:0 0 6px #e5d6b1;-moz-box-shadow:0 0 6px #e5d6b1;box-shadow:0 0 6px #e5d6b1;}
form .clearfix.warning .input-prepend .add-on,form .clearfix.warning .input-append .add-on{color:#c09853;background-color:#d2b877;border-color:#c09853;}
form .clearfix.success>label,form .clearfix.success .help-block,form .clearfix.success .help-inline{color:#468847;}
form .clearfix.success input,form .clearfix.success textarea{color:#468847;border-color:#57a957;}form .clearfix.success input:focus,form .clearfix.success textarea:focus{border-color:#458845;-webkit-box-shadow:0 0 6px #9acc9a;-moz-box-shadow:0 0 6px #9acc9a;box-shadow:0 0 6px #9acc9a;}
form .clearfix.success .input-prepend .add-on,form .clearfix.success .input-append .add-on{color:#468847;background-color:#bcddbc;border-color:#468847;}
.input-mini,input.mini,textarea.mini,select.mini{width:60px;}
.input-small,input.small,textarea.small,select.small{width:90px;}
.input-medium,input.medium,textarea.medium,select.medium{width:150px;}
.input-large,input.large,textarea.large,select.large{width:210px;}
.input-xlarge,input.xlarge,textarea.xlarge,select.xlarge{width:270px;}
.input-xxlarge,input.xxlarge,textarea.xxlarge,select.xxlarge{width:530px;}
textarea.xxlarge{overflow-y:auto;}
input.span1,textarea.span1{display:inline-block;float:none;width:30px;margin-left:0;}
input.span2,textarea.span2{display:inline-block;float:none;width:90px;margin-left:0;}
input.span3,textarea.span3{display:inline-block;float:none;width:150px;margin-left:0;}
input.span4,textarea.span4{display:inline-block;float:none;width:210px;margin-left:0;}
input.span5,textarea.span5{display:inline-block;float:none;width:270px;margin-left:0;}
input.span6,textarea.span6{display:inline-block;float:none;width:330px;margin-left:0;}
input.span7,textarea.span7{display:inline-block;float:none;width:390px;margin-left:0;}
input.span8,textarea.span8{display:inline-block;float:none;width:450px;margin-left:0;}
input.span9,textarea.span9{display:inline-block;float:none;width:510px;margin-left:0;}
input.span10,textarea.span10{display:inline-block;float:none;width:570px;margin-left:0;}
input.span11,textarea.span11{display:inline-block;float:none;width:630px;margin-left:0;}
input.span12,textarea.span12{display:inline-block;float:none;width:690px;margin-left:0;}
input.span13,textarea.span13{display:inline-block;float:none;width:750px;margin-left:0;}
input.span14,textarea.span14{display:inline-block;float:none;width:810px;margin-left:0;}
input.span15,textarea.span15{display:inline-block;float:none;width:870px;margin-left:0;}
input.span16,textarea.span16{display:inline-block;float:none;width:930px;margin-left:0;}
input[disabled],select[disabled],textarea[disabled],input[readonly],select[readonly],textarea[readonly]{background-color:#f5f5f5;border-color:#ddd;cursor:not-allowed;}
.actions{background:#f5f5f5;margin-top:18px;margin-bottom:18px;padding:17px 20px 18px 150px;border-top:1px solid #ddd;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;}.actions .secondary-action{float:right;}.actions .secondary-action a{line-height:30px;}.actions .secondary-action a:hover{text-decoration:underline;}
.help-inline,.help-block{font-size:13px;line-height:18px;color:#bfbfbf;}
.help-inline{padding-left:5px;*position:relative;*top:-5px;}
.help-block{display:block;max-width:600px;}
.inline-inputs{color:#808080;}.inline-inputs span{padding:0 2px 0 1px;}
.input-prepend input,.input-append input{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;}
.input-prepend .add-on,.input-append .add-on{position:relative;background:#f5f5f5;border:1px solid #ccc;z-index:2;float:left;display:block;width:auto;min-width:16px;height:18px;padding:4px 4px 4px 5px;margin-right:-1px;font-weight:normal;line-height:18px;color:#bfbfbf;text-align:center;text-shadow:0 1px 0 #ffffff;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.input-prepend .active,.input-append .active{background:#a9dba9;border-color:#46a546;}
.input-prepend .add-on{*margin-top:1px;}
.input-append input{float:left;-webkit-border-radius:3px 0 0 3px;-moz-border-radius:3px 0 0 3px;border-radius:3px 0 0 3px;}
.input-append .add-on{-webkit-border-radius:0 3px 3px 0;-moz-border-radius:0 3px 3px 0;border-radius:0 3px 3px 0;margin-right:0;margin-left:-1px;}
.inputs-list{margin:0 0 5px;width:100%;}.inputs-list li{display:block;padding:0;width:100%;}
.inputs-list label{display:block;float:none;width:auto;padding:0;margin-left:20px;line-height:18px;text-align:left;white-space:normal;}.inputs-list label strong{color:#808080;}
.inputs-list label small{font-size:11px;font-weight:normal;}
.inputs-list .inputs-list{margin-left:25px;margin-bottom:10px;padding-top:0;}
.inputs-list:first-child{padding-top:6px;}
.inputs-list li+li{padding-top:2px;}
.inputs-list input[type=radio],.inputs-list input[type=checkbox]{margin-bottom:0;margin-left:-20px;float:left;}
.form-stacked{padding-left:20px;}.form-stacked fieldset{padding-top:9px;}
.form-stacked legend{padding-left:0;}
.form-stacked label{display:block;float:none;width:auto;font-weight:bold;text-align:left;line-height:20px;padding-top:0;}
.form-stacked .clearfix{margin-bottom:9px;}.form-stacked .clearfix div.input{margin-left:0;}
.form-stacked .inputs-list{margin-bottom:0;}.form-stacked .inputs-list li{padding-top:0;}.form-stacked .inputs-list li label{font-weight:normal;padding-top:0;}
.form-stacked div.clearfix.error{padding-top:10px;padding-bottom:10px;padding-left:10px;margin-top:0;margin-left:-10px;}
.form-stacked .actions{margin-left:-20px;padding-left:20px;}
table{width:100%;margin-bottom:18px;padding:0;font-size:13px;border-collapse:collapse;}table th,table td{padding:10px 10px 9px;line-height:18px;text-align:left;}
table th{padding-top:9px;font-weight:bold;vertical-align:middle;}
table td{vertical-align:top;border-top:1px solid #ddd;}
table tbody th{border-top:1px solid #ddd;vertical-align:top;}
.condensed-table th,.condensed-table td{padding:5px 5px 4px;}
.bordered-table{border:1px solid #ddd;border-collapse:separate;*border-collapse:collapse;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}.bordered-table th+th,.bordered-table td+td,.bordered-table th+td{border-left:1px solid #ddd;}
.bordered-table thead tr:first-child th:first-child,.bordered-table tbody tr:first-child td:first-child{-webkit-border-radius:4px 0 0 0;-moz-border-radius:4px 0 0 0;border-radius:4px 0 0 0;}
.bordered-table thead tr:first-child th:last-child,.bordered-table tbody tr:first-child td:last-child{-webkit-border-radius:0 4px 0 0;-moz-border-radius:0 4px 0 0;border-radius:0 4px 0 0;}
.bordered-table tbody tr:last-child td:first-child{-webkit-border-radius:0 0 0 4px;-moz-border-radius:0 0 0 4px;border-radius:0 0 0 4px;}
.bordered-table tbody tr:last-child td:last-child{-webkit-border-radius:0 0 4px 0;-moz-border-radius:0 0 4px 0;border-radius:0 0 4px 0;}
table .span1{width:20px;}
table .span2{width:60px;}
table .span3{width:100px;}
table .span4{width:140px;}
table .span5{width:180px;}
table .span6{width:220px;}
table .span7{width:260px;}
table .span8{width:300px;}
table .span9{width:340px;}
table .span10{width:380px;}
table .span11{width:420px;}
table .span12{width:460px;}
table .span13{width:500px;}
table .span14{width:540px;}
table .span15{width:580px;}
table .span16{width:620px;}
.zebra-striped tbody tr:nth-child(odd) td,.zebra-striped tbody tr:nth-child(odd) th{background-color:#f9f9f9;}
.zebra-striped tbody tr:hover td,.zebra-striped tbody tr:hover th{background-color:#f5f5f5;}
table .header{cursor:pointer;}table .header:after{content:"";float:right;margin-top:7px;border-width:0 4px 4px;border-style:solid;border-color:#000 transparent;visibility:hidden;}
table .headerSortUp,table .headerSortDown{background-color:rgba(141, 192, 219, 0.25);text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);}
table .header:hover:after{visibility:visible;}
table .headerSortDown:after,table .headerSortDown:hover:after{visibility:visible;filter:alpha(opacity=60);-khtml-opacity:0.6;-moz-opacity:0.6;opacity:0.6;}
table .headerSortUp:after{border-bottom:none;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #000;visibility:visible;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;filter:alpha(opacity=60);-khtml-opacity:0.6;-moz-opacity:0.6;opacity:0.6;}
table .blue{color:#049cdb;border-bottom-color:#049cdb;}
table .headerSortUp.blue,table .headerSortDown.blue{background-color:#ade6fe;}
table .green{color:#46a546;border-bottom-color:#46a546;}
table .headerSortUp.green,table .headerSortDown.green{background-color:#cdeacd;}
table .red{color:#9d261d;border-bottom-color:#9d261d;}
table .headerSortUp.red,table .headerSortDown.red{background-color:#f4c8c5;}
table .yellow{color:#ffc40d;border-bottom-color:#ffc40d;}
table .headerSortUp.yellow,table .headerSortDown.yellow{background-color:#fff6d9;}
table .orange{color:#f89406;border-bottom-color:#f89406;}
table .headerSortUp.orange,table .headerSortDown.orange{background-color:#fee9cc;}
table .purple{color:#7a43b6;border-bottom-color:#7a43b6;}
table .headerSortUp.purple,table .headerSortDown.purple{background-color:#e2d5f0;}
.topbar{height:40px;position:fixed;top:0;left:0;right:0;z-index:10000;overflow:visible;}.topbar a{color:#bfbfbf;text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);}
.topbar h3 a:hover,.topbar .brand:hover,.topbar ul .active>a{background-color:#333;background-color:rgba(255, 255, 255, 0.05);color:#ffffff;text-decoration:none;}
.topbar h3{position:relative;}
.topbar h3 a,.topbar .brand{float:left;display:block;padding:8px 20px 12px;margin-left:-20px;color:#ffffff;font-size:20px;font-weight:200;line-height:1;}
.topbar p{margin:0;line-height:40px;}.topbar p a:hover{background-color:transparent;color:#ffffff;}
.topbar form{float:left;margin:5px 0 0 0;position:relative;filter:alpha(opacity=100);-khtml-opacity:1;-moz-opacity:1;opacity:1;}
.topbar form.pull-right{float:right;}
.topbar input{background-color:#444;background-color:rgba(255, 255, 255, 0.3);font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:normal;font-weight:13px;line-height:1;padding:4px 9px;color:#ffffff;color:rgba(255, 255, 255, 0.75);border:1px solid #111;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);-moz-box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);box-shadow:inset 0 1px 2px rgba(0, 0, 0, 0.1),0 1px 0px rgba(255, 255, 255, 0.25);-webkit-transition:none;-moz-transition:none;-ms-transition:none;-o-transition:none;transition:none;}.topbar input:-moz-placeholder{color:#e6e6e6;}
.topbar input::-webkit-input-placeholder{color:#e6e6e6;}
.topbar input:hover{background-color:#bfbfbf;background-color:rgba(255, 255, 255, 0.5);color:#ffffff;}
.topbar input:focus,.topbar input.focused{outline:0;background-color:#ffffff;color:#404040;text-shadow:0 1px 0 #ffffff;border:0;padding:5px 10px;-webkit-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);-moz-box-shadow:0 0 3px rgba(0, 0, 0, 0.15);box-shadow:0 0 3px rgba(0, 0, 0, 0.15);}
.topbar-inner,.topbar .fill{background-color:#222;background-color:#222222;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#333333), to(#222222));background-image:-moz-linear-gradient(top, #333333, #222222);background-image:-ms-linear-gradient(top, #333333, #222222);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #333333), color-stop(100%, #222222));background-image:-webkit-linear-gradient(top, #333333, #222222);background-image:-o-linear-gradient(top, #333333, #222222);background-image:linear-gradient(top, #333333, #222222);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#333333', endColorstr='#222222', GradientType=0);-webkit-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);-moz-box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);box-shadow:0 1px 3px rgba(0, 0, 0, 0.25),inset 0 -1px 0 rgba(0, 0, 0, 0.1);}
.topbar div>ul,.nav{display:block;float:left;margin:0 10px 0 0;position:relative;left:0;}.topbar div>ul>li,.nav>li{display:block;float:left;}
.topbar div>ul a,.nav a{display:block;float:none;padding:10px 10px 11px;line-height:19px;text-decoration:none;}.topbar div>ul a:hover,.nav a:hover{color:#ffffff;text-decoration:none;}
.topbar div>ul .active>a,.nav .active>a{background-color:#222;background-color:rgba(0, 0, 0, 0.5);}
.topbar div>ul.secondary-nav,.nav.secondary-nav{float:right;margin-left:10px;margin-right:0;}.topbar div>ul.secondary-nav .menu-dropdown,.nav.secondary-nav .menu-dropdown,.topbar div>ul.secondary-nav .dropdown-menu,.nav.secondary-nav .dropdown-menu{right:0;border:0;}
.topbar div>ul a.menu:hover,.nav a.menu:hover,.topbar div>ul li.open .menu,.nav li.open .menu,.topbar div>ul .dropdown-toggle:hover,.nav .dropdown-toggle:hover,.topbar div>ul .dropdown.open .dropdown-toggle,.nav .dropdown.open .dropdown-toggle{background:#444;background:rgba(255, 255, 255, 0.05);}
.topbar div>ul .menu-dropdown,.nav .menu-dropdown,.topbar div>ul .dropdown-menu,.nav .dropdown-menu{background-color:#333;}.topbar div>ul .menu-dropdown a.menu,.nav .menu-dropdown a.menu,.topbar div>ul .dropdown-menu a.menu,.nav .dropdown-menu a.menu,.topbar div>ul .menu-dropdown .dropdown-toggle,.nav .menu-dropdown .dropdown-toggle,.topbar div>ul .dropdown-menu .dropdown-toggle,.nav .dropdown-menu .dropdown-toggle{color:#ffffff;}.topbar div>ul .menu-dropdown a.menu.open,.nav .menu-dropdown a.menu.open,.topbar div>ul .dropdown-menu a.menu.open,.nav .dropdown-menu a.menu.open,.topbar div>ul .menu-dropdown .dropdown-toggle.open,.nav .menu-dropdown .dropdown-toggle.open,.topbar div>ul .dropdown-menu .dropdown-toggle.open,.nav .dropdown-menu .dropdown-toggle.open{background:#444;background:rgba(255, 255, 255, 0.05);}
.topbar div>ul .menu-dropdown li a,.nav .menu-dropdown li a,.topbar div>ul .dropdown-menu li a,.nav .dropdown-menu li a{color:#999;text-shadow:0 1px 0 rgba(0, 0, 0, 0.5);}.topbar div>ul .menu-dropdown li a:hover,.nav .menu-dropdown li a:hover,.topbar div>ul .dropdown-menu li a:hover,.nav .dropdown-menu li a:hover{background-color:#191919;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#292929), to(#191919));background-image:-moz-linear-gradient(top, #292929, #191919);background-image:-ms-linear-gradient(top, #292929, #191919);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #292929), color-stop(100%, #191919));background-image:-webkit-linear-gradient(top, #292929, #191919);background-image:-o-linear-gradient(top, #292929, #191919);background-image:linear-gradient(top, #292929, #191919);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#292929', endColorstr='#191919', GradientType=0);color:#ffffff;}
.topbar div>ul .menu-dropdown .active a,.nav .menu-dropdown .active a,.topbar div>ul .dropdown-menu .active a,.nav .dropdown-menu .active a{color:#ffffff;}
.topbar div>ul .menu-dropdown .divider,.nav .menu-dropdown .divider,.topbar div>ul .dropdown-menu .divider,.nav .dropdown-menu .divider{background-color:#222;border-color:#444;}
.topbar ul .menu-dropdown li a,.topbar ul .dropdown-menu li a{padding:4px 15px;}
li.menu,.dropdown{position:relative;}
a.menu:after,.dropdown-toggle:after{width:0;height:0;display:inline-block;content:"&darr;";text-indent:-99999px;vertical-align:top;margin-top:8px;margin-left:4px;border-left:4px solid transparent;border-right:4px solid transparent;border-top:4px solid #ffffff;filter:alpha(opacity=50);-khtml-opacity:0.5;-moz-opacity:0.5;opacity:0.5;}
.menu-dropdown,.dropdown-menu{background-color:#ffffff;float:left;display:none;position:absolute;top:40px;z-index:900;min-width:160px;max-width:220px;_width:160px;margin-left:0;margin-right:0;padding:6px 0;zoom:1;border-color:#999;border-color:rgba(0, 0, 0, 0.2);border-style:solid;border-width:0 1px 1px;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:0 2px 4px rgba(0, 0, 0, 0.2);-moz-box-shadow:0 2px 4px rgba(0, 0, 0, 0.2);box-shadow:0 2px 4px rgba(0, 0, 0, 0.2);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.menu-dropdown li,.dropdown-menu li{float:none;display:block;background-color:none;}
.menu-dropdown .divider,.dropdown-menu .divider{height:1px;margin:5px 0;overflow:hidden;background-color:#eee;border-bottom:1px solid #ffffff;}
.topbar .dropdown-menu a,.dropdown-menu a{display:block;padding:4px 15px;clear:both;font-weight:normal;line-height:18px;color:#808080;text-shadow:0 1px 0 #ffffff;}.topbar .dropdown-menu a:hover,.dropdown-menu a:hover,.topbar .dropdown-menu a.hover,.dropdown-menu a.hover{background-color:#dddddd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#eeeeee), to(#dddddd));background-image:-moz-linear-gradient(top, #eeeeee, #dddddd);background-image:-ms-linear-gradient(top, #eeeeee, #dddddd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #eeeeee), color-stop(100%, #dddddd));background-image:-webkit-linear-gradient(top, #eeeeee, #dddddd);background-image:-o-linear-gradient(top, #eeeeee, #dddddd);background-image:linear-gradient(top, #eeeeee, #dddddd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#eeeeee', endColorstr='#dddddd', GradientType=0);color:#404040;text-decoration:none;-webkit-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.025),inset 0 -1px rgba(0, 0, 0, 0.025);-moz-box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.025),inset 0 -1px rgba(0, 0, 0, 0.025);box-shadow:inset 0 1px 0 rgba(0, 0, 0, 0.025),inset 0 -1px rgba(0, 0, 0, 0.025);}
.open .menu,.dropdown.open .menu,.open .dropdown-toggle,.dropdown.open .dropdown-toggle{color:#ffffff;background:#ccc;background:rgba(0, 0, 0, 0.3);}
.open .menu-dropdown,.dropdown.open .menu-dropdown,.open .dropdown-menu,.dropdown.open .dropdown-menu{display:block;}
.tabs,.pills{margin:0 0 18px;padding:0;list-style:none;zoom:1;}.tabs:before,.pills:before,.tabs:after,.pills:after{display:table;content:"";zoom:1;}
.tabs:after,.pills:after{clear:both;}
.tabs>li,.pills>li{float:left;}.tabs>li>a,.pills>li>a{display:block;}
.tabs{border-color:#ddd;border-style:solid;border-width:0 0 1px;}.tabs>li{position:relative;margin-bottom:-1px;}.tabs>li>a{padding:0 15px;margin-right:2px;line-height:34px;border:1px solid transparent;-webkit-border-radius:4px 4px 0 0;-moz-border-radius:4px 4px 0 0;border-radius:4px 4px 0 0;}.tabs>li>a:hover{text-decoration:none;background-color:#eee;border-color:#eee #eee #ddd;}
.tabs .active>a,.tabs .active>a:hover{color:#808080;background-color:#ffffff;border:1px solid #ddd;border-bottom-color:transparent;cursor:default;}
.tabs .menu-dropdown,.tabs .dropdown-menu{top:35px;border-width:1px;-webkit-border-radius:0 6px 6px 6px;-moz-border-radius:0 6px 6px 6px;border-radius:0 6px 6px 6px;}
.tabs a.menu:after,.tabs .dropdown-toggle:after{border-top-color:#999;margin-top:15px;margin-left:5px;}
.tabs li.open.menu .menu,.tabs .open.dropdown .dropdown-toggle{border-color:#999;}
.tabs li.open a.menu:after,.tabs .dropdown.open .dropdown-toggle:after{border-top-color:#555;}
.pills a{margin:5px 3px 5px 0;padding:0 15px;line-height:30px;text-shadow:0 1px 1px #ffffff;-webkit-border-radius:15px;-moz-border-radius:15px;border-radius:15px;}.pills a:hover{color:#ffffff;text-decoration:none;text-shadow:0 1px 1px rgba(0, 0, 0, 0.25);background-color:#00438a;}
.pills .active a{color:#ffffff;text-shadow:0 1px 1px rgba(0, 0, 0, 0.25);background-color:#0069d6;}
.pills-vertical>li{float:none;}
.tab-content>.tab-pane,.pill-content>.pill-pane,.tab-content>div,.pill-content>div{display:none;}
.tab-content>.active,.pill-content>.active{display:block;}
.breadcrumb{padding:7px 14px;margin:0 0 18px;background-color:#f5f5f5;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#ffffff), to(#f5f5f5));background-image:-moz-linear-gradient(top, #ffffff, #f5f5f5);background-image:-ms-linear-gradient(top, #ffffff, #f5f5f5);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #ffffff), color-stop(100%, #f5f5f5));background-image:-webkit-linear-gradient(top, #ffffff, #f5f5f5);background-image:-o-linear-gradient(top, #ffffff, #f5f5f5);background-image:linear-gradient(top, #ffffff, #f5f5f5);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#f5f5f5', GradientType=0);border:1px solid #ddd;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;}.breadcrumb li{display:inline;text-shadow:0 1px 0 #ffffff;}
.breadcrumb .divider{padding:0 5px;color:#bfbfbf;}
.breadcrumb .active a{color:#404040;}
.hero-unit{background-color:#f5f5f5;margin-bottom:30px;padding:60px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}.hero-unit h1{margin-bottom:0;font-size:60px;line-height:1;letter-spacing:-1px;}
.hero-unit p{font-size:18px;font-weight:200;line-height:27px;}
footer{margin-top:17px;padding-top:17px;border-top:1px solid #eee;}
.page-header{margin-bottom:17px;border-bottom:1px solid #ddd;-webkit-box-shadow:0 1px 0 rgba(255, 255, 255, 0.5);-moz-box-shadow:0 1px 0 rgba(255, 255, 255, 0.5);box-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}.page-header h1{margin-bottom:8px;}
.btn.danger,.alert-message.danger,.btn.danger:hover,.alert-message.danger:hover,.btn.error,.alert-message.error,.btn.error:hover,.alert-message.error:hover,.btn.success,.alert-message.success,.btn.success:hover,.alert-message.success:hover,.btn.info,.alert-message.info,.btn.info:hover,.alert-message.info:hover{color:#ffffff;}
.btn .close,.alert-message .close{font-family:Arial,sans-serif;line-height:18px;}
.btn.danger,.alert-message.danger,.btn.error,.alert-message.error{background-color:#c43c35;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#ee5f5b), to(#c43c35));background-image:-moz-linear-gradient(top, #ee5f5b, #c43c35);background-image:-ms-linear-gradient(top, #ee5f5b, #c43c35);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #ee5f5b), color-stop(100%, #c43c35));background-image:-webkit-linear-gradient(top, #ee5f5b, #c43c35);background-image:-o-linear-gradient(top, #ee5f5b, #c43c35);background-image:linear-gradient(top, #ee5f5b, #c43c35);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ee5f5b', endColorstr='#c43c35', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#c43c35 #c43c35 #882a25;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);}
.btn.success,.alert-message.success{background-color:#57a957;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#62c462), to(#57a957));background-image:-moz-linear-gradient(top, #62c462, #57a957);background-image:-ms-linear-gradient(top, #62c462, #57a957);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #62c462), color-stop(100%, #57a957));background-image:-webkit-linear-gradient(top, #62c462, #57a957);background-image:-o-linear-gradient(top, #62c462, #57a957);background-image:linear-gradient(top, #62c462, #57a957);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#62c462', endColorstr='#57a957', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#57a957 #57a957 #3d773d;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);}
.btn.info,.alert-message.info{background-color:#339bb9;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#5bc0de), to(#339bb9));background-image:-moz-linear-gradient(top, #5bc0de, #339bb9);background-image:-ms-linear-gradient(top, #5bc0de, #339bb9);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #5bc0de), color-stop(100%, #339bb9));background-image:-webkit-linear-gradient(top, #5bc0de, #339bb9);background-image:-o-linear-gradient(top, #5bc0de, #339bb9);background-image:linear-gradient(top, #5bc0de, #339bb9);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#5bc0de', endColorstr='#339bb9', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#339bb9 #339bb9 #22697d;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);}
.btn{cursor:pointer;display:inline-block;background-color:#e6e6e6;background-repeat:no-repeat;background-image:-webkit-gradient(linear, 0 0, 0 100%, from(#ffffff), color-stop(25%, #ffffff), to(#e6e6e6));background-image:-webkit-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-moz-linear-gradient(top, #ffffff, #ffffff 25%, #e6e6e6);background-image:-ms-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:-o-linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);background-image:linear-gradient(#ffffff, #ffffff 25%, #e6e6e6);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffff', endColorstr='#e6e6e6', GradientType=0);padding:5px 14px 6px;text-shadow:0 1px 1px rgba(255, 255, 255, 0.75);color:#333;font-size:13px;line-height:normal;border:1px solid #ccc;border-bottom-color:#bbb;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.2),0 1px 2px rgba(0, 0, 0, 0.05);-webkit-transition:0.1s linear all;-moz-transition:0.1s linear all;-ms-transition:0.1s linear all;-o-transition:0.1s linear all;transition:0.1s linear all;}.btn:hover{background-position:0 -15px;color:#333;text-decoration:none;}
.btn:focus{outline:1px dotted #666;}
.btn.primary{color:#ffffff;background-color:#0064cd;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#049cdb), to(#0064cd));background-image:-moz-linear-gradient(top, #049cdb, #0064cd);background-image:-ms-linear-gradient(top, #049cdb, #0064cd);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #049cdb), color-stop(100%, #0064cd));background-image:-webkit-linear-gradient(top, #049cdb, #0064cd);background-image:-o-linear-gradient(top, #049cdb, #0064cd);background-image:linear-gradient(top, #049cdb, #0064cd);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#049cdb', endColorstr='#0064cd', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#0064cd #0064cd #003f81;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);}
.btn.active,.btn:active{-webkit-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:inset 0 2px 4px rgba(0, 0, 0, 0.25),0 1px 2px rgba(0, 0, 0, 0.05);}
.btn.disabled{cursor:default;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=65);-khtml-opacity:0.65;-moz-opacity:0.65;opacity:0.65;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
.btn[disabled]{cursor:default;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);filter:alpha(opacity=65);-khtml-opacity:0.65;-moz-opacity:0.65;opacity:0.65;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}
.btn.large{font-size:15px;line-height:normal;padding:9px 14px 9px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;}
.btn.small{padding:7px 9px 7px;font-size:11px;}
:root .alert-message,:root .btn{border-radius:0 \0;}
button.btn::-moz-focus-inner,input[type=submit].btn::-moz-focus-inner{padding:0;border:0;}
.close{float:right;color:#000000;font-size:20px;font-weight:bold;line-height:13.5px;text-shadow:0 1px 0 #ffffff;filter:alpha(opacity=25);-khtml-opacity:0.25;-moz-opacity:0.25;opacity:0.25;}.close:hover{color:#000000;text-decoration:none;filter:alpha(opacity=40);-khtml-opacity:0.4;-moz-opacity:0.4;opacity:0.4;}
.alert-message{position:relative;padding:7px 15px;margin-bottom:18px;color:#404040;background-color:#eedc94;background-repeat:repeat-x;background-image:-khtml-gradient(linear, left top, left bottom, from(#fceec1), to(#eedc94));background-image:-moz-linear-gradient(top, #fceec1, #eedc94);background-image:-ms-linear-gradient(top, #fceec1, #eedc94);background-image:-webkit-gradient(linear, left top, left bottom, color-stop(0%, #fceec1), color-stop(100%, #eedc94));background-image:-webkit-linear-gradient(top, #fceec1, #eedc94);background-image:-o-linear-gradient(top, #fceec1, #eedc94);background-image:linear-gradient(top, #fceec1, #eedc94);filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#fceec1', endColorstr='#eedc94', GradientType=0);text-shadow:0 -1px 0 rgba(0, 0, 0, 0.25);border-color:#eedc94 #eedc94 #e4c652;border-color:rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);border-width:1px;border-style:solid;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.25);-moz-box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.25);box-shadow:inset 0 1px 0 rgba(255, 255, 255, 0.25);}.alert-message .close{margin-top:1px;*margin-top:0;}
.alert-message a{font-weight:bold;color:#404040;}
.alert-message.danger p a,.alert-message.error p a,.alert-message.success p a,.alert-message.info p a{color:#ffffff;}
.alert-message h5{line-height:18px;}
.alert-message p{margin-bottom:0;}
.alert-message div{margin-top:5px;margin-bottom:2px;line-height:28px;}
.alert-message .btn{-webkit-box-shadow:0 1px 0 rgba(255, 255, 255, 0.25);-moz-box-shadow:0 1px 0 rgba(255, 255, 255, 0.25);box-shadow:0 1px 0 rgba(255, 255, 255, 0.25);}
.alert-message.block-message{background-image:none;background-color:#fdf5d9;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);padding:14px;border-color:#fceec1;-webkit-box-shadow:none;-moz-box-shadow:none;box-shadow:none;}.alert-message.block-message ul,.alert-message.block-message p{margin-right:30px;}
.alert-message.block-message ul{margin-bottom:0;}
.alert-message.block-message li{color:#404040;}
.alert-message.block-message .alert-actions{margin-top:5px;}
.alert-message.block-message.error,.alert-message.block-message.success,.alert-message.block-message.info{color:#404040;text-shadow:0 1px 0 rgba(255, 255, 255, 0.5);}
.alert-message.block-message.error{background-color:#fddfde;border-color:#fbc7c6;}
.alert-message.block-message.success{background-color:#d1eed1;border-color:#bfe7bf;}
.alert-message.block-message.info{background-color:#ddf4fb;border-color:#c6edf9;}
.alert-message.block-message.danger p a,.alert-message.block-message.error p a,.alert-message.block-message.success p a,.alert-message.block-message.info p a{color:#404040;}
.pagination{height:36px;margin:18px 0;}.pagination ul{float:left;margin:0;border:1px solid #ddd;border:1px solid rgba(0, 0, 0, 0.15);-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;-webkit-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);-moz-box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);box-shadow:0 1px 2px rgba(0, 0, 0, 0.05);}
.pagination li{display:inline;}
.pagination a{float:left;padding:0 14px;line-height:34px;border-right:1px solid;border-right-color:#ddd;border-right-color:rgba(0, 0, 0, 0.15);*border-right-color:#ddd;text-decoration:none;}
.pagination a:hover,.pagination .active a{background-color:#c7eefe;}
.pagination .disabled a,.pagination .disabled a:hover{background-color:transparent;color:#bfbfbf;}
.pagination .next a{border:0;}
.well{background-color:#f5f5f5;margin-bottom:20px;padding:19px;min-height:20px;border:1px solid #eee;border:1px solid rgba(0, 0, 0, 0.05);-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);-moz-box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);box-shadow:inset 0 1px 1px rgba(0, 0, 0, 0.05);}.well blockquote{border-color:#ddd;border-color:rgba(0, 0, 0, 0.15);}
.modal-backdrop{background-color:#000000;position:fixed;top:0;left:0;right:0;bottom:0;z-index:10000;}.modal-backdrop.fade{opacity:0;}
.modal-backdrop,.modal-backdrop.fade.in{filter:alpha(opacity=80);-khtml-opacity:0.8;-moz-opacity:0.8;opacity:0.8;}
.modal{position:fixed;top:50%;left:50%;z-index:11000;width:560px;margin:-250px 0 0 -280px;background-color:#ffffff;border:1px solid #999;border:1px solid rgba(0, 0, 0, 0.3);*border:1px solid #999;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.modal .close{margin-top:7px;}
.modal.fade{-webkit-transition:opacity .3s linear, top .3s ease-out;-moz-transition:opacity .3s linear, top .3s ease-out;-ms-transition:opacity .3s linear, top .3s ease-out;-o-transition:opacity .3s linear, top .3s ease-out;transition:opacity .3s linear, top .3s ease-out;top:-25%;}
.modal.fade.in{top:50%;}
.modal-header{border-bottom:1px solid #eee;padding:5px 15px;}
.modal-body{padding:15px;}
.modal-body form{margin-bottom:0;}
.modal-footer{background-color:#f5f5f5;padding:14px 15px 15px;border-top:1px solid #ddd;-webkit-border-radius:0 0 6px 6px;-moz-border-radius:0 0 6px 6px;border-radius:0 0 6px 6px;-webkit-box-shadow:inset 0 1px 0 #ffffff;-moz-box-shadow:inset 0 1px 0 #ffffff;box-shadow:inset 0 1px 0 #ffffff;zoom:1;margin-bottom:0;}.modal-footer:before,.modal-footer:after{display:table;content:"";zoom:1;}
.modal-footer:after{clear:both;}
.modal-footer .btn{float:right;margin-left:5px;}
.modal .popover,.modal .twipsy{z-index:12000;}
.twipsy{display:block;position:absolute;visibility:visible;padding:5px;font-size:11px;z-index:1000;filter:alpha(opacity=80);-khtml-opacity:0.8;-moz-opacity:0.8;opacity:0.8;}.twipsy.fade.in{filter:alpha(opacity=80);-khtml-opacity:0.8;-moz-opacity:0.8;opacity:0.8;}
.twipsy.above .twipsy-arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
.twipsy.left .twipsy-arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
.twipsy.below .twipsy-arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
.twipsy.right .twipsy-arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
.twipsy-inner{padding:3px 8px;background-color:#000000;color:white;text-align:center;max-width:200px;text-decoration:none;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;}
.twipsy-arrow{position:absolute;width:0;height:0;}
.popover{position:absolute;top:0;left:0;z-index:1000;padding:5px;display:none;}.popover.above .arrow{bottom:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-top:5px solid #000000;}
.popover.right .arrow{top:50%;left:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-right:5px solid #000000;}
.popover.below .arrow{top:0;left:50%;margin-left:-5px;border-left:5px solid transparent;border-right:5px solid transparent;border-bottom:5px solid #000000;}
.popover.left .arrow{top:50%;right:0;margin-top:-5px;border-top:5px solid transparent;border-bottom:5px solid transparent;border-left:5px solid #000000;}
.popover .arrow{position:absolute;width:0;height:0;}
.popover .inner{background:#000000;background:rgba(0, 0, 0, 0.8);padding:3px;overflow:hidden;width:280px;-webkit-border-radius:6px;-moz-border-radius:6px;border-radius:6px;-webkit-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);-moz-box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);box-shadow:0 3px 7px rgba(0, 0, 0, 0.3);}
.popover .title{background-color:#f5f5f5;padding:9px 15px;line-height:1;-webkit-border-radius:3px 3px 0 0;-moz-border-radius:3px 3px 0 0;border-radius:3px 3px 0 0;border-bottom:1px solid #eee;}
.popover .content{background-color:#ffffff;padding:14px;-webkit-border-radius:0 0 3px 3px;-moz-border-radius:0 0 3px 3px;border-radius:0 0 3px 3px;-webkit-background-clip:padding-box;-moz-background-clip:padding-box;background-clip:padding-box;}.popover .content p,.popover .content ul,.popover .content ol{margin-bottom:0;}
.fade{-webkit-transition:opacity 0.15s linear;-moz-transition:opacity 0.15s linear;-ms-transition:opacity 0.15s linear;-o-transition:opacity 0.15s linear;transition:opacity 0.15s linear;opacity:0;}.fade.in{opacity:1;}
.label{padding:1px 3px 2px;font-size:9.75px;font-weight:bold;color:#ffffff;text-transform:uppercase;white-space:nowrap;background-color:#bfbfbf;-webkit-border-radius:3px;-moz-border-radius:3px;border-radius:3px;text-shadow:none;}.label.important{background-color:#c43c35;}
.label.warning{background-color:#f89406;}
.label.success{background-color:#46a546;}
.label.notice{background-color:#62cffc;}
.media-grid{margin-left:-20px;margin-bottom:0;zoom:1;}.media-grid:before,.media-grid:after{display:table;content:"";zoom:1;}
.media-grid:after{clear:both;}
.media-grid li{display:inline;}
.media-grid a{float:left;padding:4px;margin:0 0 18px 20px;border:1px solid #ddd;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px;-webkit-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);-moz-box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);box-shadow:0 1px 1px rgba(0, 0, 0, 0.075);}.media-grid a img{display:block;}
.media-grid a:hover{border-color:#0069d6;-webkit-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);-moz-box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);box-shadow:0 1px 4px rgba(0, 105, 214, 0.25);}

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,17 @@
(function($){
$(document).ready(function() {
var request = {
type: 'GET',
url: progress_url + '?nocache=' + new Date().getTime(),
dataType: 'json',
success: function(data) {
var progress = data.progress;
var bar = $('#progress_bar').find('.ui-progress');
bar.animate({'width': progress.toString() + '%'});
}
};
$.ajax(request);
});
})(jQuery);

View File

@ -0,0 +1,114 @@
/*
Copyright (c) 2010 Ivan Vanderbyl
Originally found at http://ivan.ly/ui
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
/* Bar which is placed behind the progress */
.ui-progress-bar {
/* Usual setup stuff */
position: relative;
top: 27px;
height: 18px;
width: 200px;
margin: 0 0 0 auto;
/* Pad right so we don't cover the borders when fully progressed */
padding-right: 2px;
/* For browser that don't support gradients, we'll set a blanket background colour */
background-color: #abb2bc;
/* Rounds the ends, we specify an excessive amount to make sure they are completely rounded */
/* Adjust to your liking, and don't forget to adjust to the same amount in .ui-progress */
/*border-radius: 4px;
-moz-border-radius: 4px;
-webkit-border-radius: 4px;*/
/* Webkit background gradient */
background: -webkit-gradient(linear, left bottom, left top, color-stop(0, #b6bcc6), color-stop(1, #9da5b0));
/* Mozilla background gradient */
background: -moz-linear-gradient(#9da5b0 0%, #b6bcc6 100%);
/* Give it the inset look by adding some shadows and highlights */
-webkit-box-shadow: inset 0px 1px 2px 0px rgba(0, 0, 0, 0.5), 0px 1px 0px 0px #FFF;
-moz-box-shadow: inset 0px 1px 2px 0px rgba(0, 0, 0, 0.5), 0px 1px 0px 0px #FFF;
box-shadow: inset 0px 1px 2px 0px rgba(0, 0, 0, 0.5), 0px 1px 0px 0px #FFF;
}
/* Progress part of the progress bar */
.ui-progress {
/* Usual setup stuff */
position: relative;
display: block;
overflow: hidden;
/* Height should be 2px less than .ui-progress-bar so as to not cover borders and give it a look of being inset */
height: 16px;
/* Rounds the ends, we specify an excessive amount to make sure they are completely rounded */
/* Adjust to your liking, and don't forget to adjust to the same amount in .ui-progress-bar */
/*-moz-border-radius: 4px;
-webkit-border-radius: 4px;
border-radius: 4px;*/
/* Set the background size so the stripes work correctly */
-webkit-background-size: 44px 44px; /* Webkit */
/* For browser that don't support gradients, we'll set a blanket background colour */
background-color: #74d04c;
/* Webkit background stripes and gradient */
background: -webkit-gradient(linear, 0 0, 44 44,
color-stop(0.00, rgba(255,255,255,0.17)),
color-stop(0.25, rgba(255,255,255,0.17)),
color-stop(0.26, rgba(255,255,255,0)),
color-stop(0.50, rgba(255,255,255,0)),
color-stop(0.51, rgba(255,255,255,0.17)),
color-stop(0.75, rgba(255,255,255,0.17)),
color-stop(0.76, rgba(255,255,255,0)),
color-stop(1.00, rgba(255,255,255,0))
), -webkit-gradient(linear, left bottom, left top, color-stop(0, #74d04c), color-stop(1, #9bdd62));
/* Mozilla (Firefox etc) background stripes */
/* Note: Mozilla's support for gradients is more true to the original design, allowing gradients at 30 degrees, as apposed to 45 degress in webkit. */
background: -moz-repeating-linear-gradient(top left -30deg,
rgba(255,255,255,0.17),
rgba(255,255,255,0.17) 15px,
rgba(255,255,255,0) 15px,
rgba(255,255,255,0) 30px
), -moz-linear-gradient(#9bdd62 0%, #74d04c 100%);
/* Webkit embossing */
-webkit-box-shadow: inset 0px 1px 0px 0px #dbf383, inset 0px -1px 1px #58c43a;
/* Mozilla embossing */
-moz-box-shadow: inset 0px 1px 0px 0px #dbf383, inset 0px -1px 1px #58c43a;
/* IE9 and Opera embossing */
box-shadow: inset 0px 1px 0px 0px #dbf383, inset 0px -1px 1px #58c43a;
/* Give it a higher contrast outline */
border: 1px solid #4c8932;
}
/* Progress indicator text */
.ui-progress span.ui-label {
display: none;
}

View File

@ -0,0 +1,137 @@
/* question numbers are always bold, the whole question if it is required */
.required, .qnumber {
font-weight: bold;
}
.questionset-title {
/* margin right is there to make space for the progressbar */
margin: 0 220px 25px 0;
}
.questionset-text {
margin-bottom: 25px;
}
.questionset-text p{
font-size: 1.3em;
line-height: 1.3em;
}
.question-text {
font-size: 1.3em;
line-height: 1.3em;
}
div.input {
margin-left: 35px;
}
.answer {
margin: 5px 0 25px 0;
padding: 0;
border-top: 1px solid #dadada;
}
.answer label {
font-size: 14px;
}
/* ensure that all inputs have the same distance to the question */
div.input input[type="text"], div.input textarea, div.input select {
margin: 10px 0 0 0;
}
div.input span.add-on {
margin: 8px 0 0 0;
border-right: none;
}
ul.inputs-list input[type="text"] {
margin: 5px 0;
}
div.error {
margin: 10px 0;
}
/* move the back link left of the submit button */
.back-link {
margin-top: -66px;
margin-left: 25px;
position: absolute;
}
.questionset-submit {
margin-top: 60px;
}
#languages {
float: right;
margin: 15px 0px 0px 0px;
}
.content {
background-color: #fff;
padding: 20px;
margin: 0px -20px; /* negative indent the amount of the padding to maintain the grid system */
-webkit-border-radius: 0 0 6px 6px;
-moz-border-radius: 0 0 6px 6px;
border-radius: 0 0 6px 6px;
-webkit-box-shadow: 0 1px 2px rgba(0,0,0,.15);
-moz-box-shadow: 0 1px 2px rgba(0,0,0,.15);
box-shadow: 0 1px 2px rgba(0,0,0,.15);
}
.page-header {
background-color: #f5f5f5;
padding: 20px 20px 10px;
margin: -20px -20px 20px;
}
.rangeinput div.input {
display: inline;
float: left;
}
.rangeinput label {
text-align: left;
max-width: 40px;
margin-left: 10px;
padding-top: 4px;
}
.rangeinput {
margin-top: 5px;
}
.rangeinput span.help-block {
clear: left;
margin-left: 35px;
}
span.choice-multiple-values-text {
display: inline-block;
max-width: 40%;
}
input.sending {
text-shadow: 0px 0px 5px white !important;
-webkit-transition: 0.3s linear all !important;
-moz-transition: 0.3s linear all !important;
-o-transition: 0.3s linear all !important;
transition: 0.3s linear all !important;
}
.questionset-submit input:disabled {
-webkit-transition: 0.3s linear all !important;
-moz-transition: 0.3s linear all !important;
-o-transition: 0.3s linear all !important;
transition: 0.3s linear all !important;
}
.hoverable {
color: #79E2E8;
}

View File

@ -0,0 +1,147 @@
var qvalues = new Array(); // used as dictionary
var qtriggers = new Array();
function dep_check(expr) {
var exprs = expr.split(",",2);
var qnum = exprs[0];
var value = exprs[1];
var qvalue = qvalues[qnum];
if(value.substring(0,1) == "!") {
var multiple_option = qvalues[qnum+'_'+value.substring(1)];
if(multiple_option != undefined)
return !multiple_option;
value = value.substring(1);
return qvalue != value;
}
if(value.substring(0,1) == "<") {
qvalue = parseInt(qvalue);
if(value.substring(1,2) == "=") {
value = parseInt(value.substring(2));
return qvalue <= value;
}
value = parseInt(value.substring(1));
return qvalue < value;
}
if(value.substring(0,1) == ">") {
qvalue = parseInt(qvalue);
if(value.substring(1,2) == "=") {
value = parseInt(value.substring(2));
return qvalue >= value;
}
value = parseInt(value.substring(1));
return qvalue > value;
}
var multiple_option = qvalues[qnum+'_'+value];
if(multiple_option != undefined) {
return multiple_option;
}
if(qvalues[qnum] == value) {
return true;
}
return false;
}
function getChecksAttr(obj) {
return obj.getAttribute('checks');
}
function statusChanged(obj, res) {
if(obj.tagName == 'DIV') {
obj.style.display = !res ? 'none' : 'block';
return;
}
if(obj.tagName == 'SPAN') {
obj.style.visibility = !res ? 'hidden': 'visible';
}
//obj.style.backgroundColor = !res ? "#eee" : "#fff";
obj.disabled = !res;
}
function valchanged(qnum, value) {
qvalues[qnum] = value;
// qnum may be 'X_Y' for option Y of multiple choice question X
qnum = qnum.split('_')[0];
for (var t in qtriggers) {
t = qtriggers[t];
checks = getChecksAttr(t);
var res = eval(checks);
statusChanged(t, res)
}
}
function addtrigger(elemid) {
var elem = document.getElementById(elemid);
if(!elem) {
alert("addtrigger: Element with id "+elemid+" not found.");
return;
}
qtriggers[qtriggers.length] = elem;
}
function alignValueBoxes(question) {
var question = document.getElementById("qc_" + question);
var choice_boxes = question.getElementsByClassName("choice-multiple-values-text");
var max_width = 0;
for (i = 0 ; i < choice_boxes.length ; i++) {
box_width = choice_boxes[i].offsetWidth;
if (box_width > max_width)
max_width = box_width;
}
// add padding to right of narrower boxes
for (i = 0 ; i < choice_boxes.length ; i++) {
box_width = choice_boxes[i].offsetWidth;
padding = max_width - box_width;
if (padding > 0)
choice_boxes[i].style.paddingRight = padding + "px";
}
}
/*
- disable the submit button once it's been clicked
- do it for a total of 5 seconds by which time the page should've been sent
- oscillate the sending class which does some fancy css transition trickery
*/
(function($){
$(document).ready(function() {
// Ensure that the submit button is enabled to prevent a disabled
// button when user clicks the back button on their browser
$('.questionset-submit input').removeAttr('disabled');
$('#qform').submit(function() {
var input = $('.questionset-submit input');
var interval = 400; // ms
var duration = 10000; // 10s
var disable = function(){
input.attr('disabled', 'disabled');
input.toggleClass('sending', false);
};
var enable = function(){
$('body').css({'cursor':'auto'});
input.removeAttr('disabled');
};
var step = 0;
var animate = function() {
// re-enable the button after the duration
if (interval * step > duration) {
clearInterval(id);
enable();
}
step += 1;
input.toggleClass('sending');
};
// start animating before disabling as it looks nicer
animate();
disable();
// id is availabe in the animate method. js closures ftw!
var id = setInterval(animate, interval);
});
});
$(window).unload(function() {});
})(jQuery);

View File

@ -0,0 +1,59 @@
(function($){$(document).ready(function() {
// true if the native html5 range input type is supported by the browser
var range_support = (function(){
var el=document.createElement("input");
el.setAttribute("type", "range");
return el.type=="range";
})();
// if range is not supported the input will be a simple text field
// in which case the following function ensures that the values
// in this text field are within the constraints of min, max, step
var normalize_value = function(input) {
var input = $(input);
var min = parseFloat(input.attr('min'));
var max = parseFloat(input.attr('max'));
var step = parseFloat(input.attr('step'));
var val = parseFloat(input.attr('value'));
if (val > max) val = max;
if (val < min) val = min;
// returns the number of digits after the decimal point
var digits = function(value){
var str = value.toString();
if (str.indexOf('.') == -1 && str.indexOf(',') == -1)
return 0;
if (str.indexOf('.') > -1)
return str.split('.')[1].length;
else
return str.split(',')[1].length;
};
// rounds the number to the next step
var round = function(val, step) {
return Math.round(val * (1 / step)) / (1 / step);
};
// round the number and fix the digits
return round(val, step).toFixed(digits(val));
};
if (range_support) {
$('.rangeinput input').change(function() {
var label = $(this).parent().parent().find('label');
var unit = label.attr('data-unit');
label.html($(this).val() + unit);
});
$('.rangeinput input').trigger('change');
} else {
$('.rangeinput input').change(function() {
$(this).val(normalize_value(this));
});
}
});})(jQuery);

View File

@ -0,0 +1,17 @@
{% extends "admin/change_form.html" %}
{% load i18n questionnaire %}
{% block object-tools %}
{% if original %}
<a href="{{ original|qtesturl }}">Show on Site</a>
{% endif %}
{% endblock %}
{% block after_field_sets %}
<h2>{% trans "Choices" %}</h2>
<ul>
<li>{% blocktrans %}Sortid determines the choice order.{% endblocktrans %}</li>
<li>{% blocktrans %}Short values will appear in exported data and are
referenced in 'Additional checks' fields. Numbers, letters
or single words are valid.{% endblocktrans %}</li>
</ul>
{% endblock %}

View File

@ -0,0 +1,37 @@
{% extends "admin/change_list.html" %}
{% block result_list %}
<script language="javascript">
function togglehide(id) {
obj = document.getElementById("questionnaire-" + id);
head = document.getElementById("qhead-" + id);
if(obj) {
if(obj.style.display == 'none') {
obj.style.display = 'block';
head.innerHTML = '&uarr;'
} else {
obj.style.display = 'none';
head.innerHTML = '&darr;'
}
}
return false;
}
</script>
{% for questionnaire in questionnaires %}
<H2 onClick="togglehide('{{ questionnaire.id }}');">
<span id="qhead-{{ questionnaire.id }}">&uarr;</span>{{ questionnaire.name }}
</H2>
<div id="questionnaire-{{ questionnaire.id }}" style="margin-bottom: 2em;">
{% for questionset in questionnaire.questionsets %}
<H4>QuestionSet: <a href="/admin/questionnaire/questionset/{{ questionset.id }}/"><font color="#111">{{ questionset.heading }} ({{ questionset.sortid }})</font></a></H4>
{% for q in questionset.sorted_questions %}
<a href="{{ q.id }}/">{{ q.number }}. {{ q.text }}</a> [{{ q.type }}] (id={{ q.id }})<br />
{% endfor %}
&rarr; <a href="add/?questionset={{ questionset.id }}">Add Question to <tt>{{ questionset.heading }}</tt></a>
{% endfor %}
<br /><br />&rarr; <a href="/admin/questionnaire/questionset/add/?questionnaire={{ questionnaire.id }}">Add QuestionSet to <tt>{{ questionnaire.name }}</tt></a>
</div>
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,14 @@
{% extends "admin/change_form.html" %}
{% block after_field_sets %}
{% for x in original.pending %}
{% if forloop.first %}<h3>Pending:</h3><ul>{% endif %}
<li><b>{{ x.runid }}: created {{ x.created }}, last email sent {{ x.emailsent }}</b></li>
{% if forloop.last %}</ul>{% endif %}
{% endfor %}
{% for x in original.history %}
{% if forloop.first %}<h3>History:</h3><ul>{% endif %}
<li><b>{{ x.runid }}: completed {{ x.completed }}</b></li>
{% if forloop.last %}</ul>{% endif %}
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,57 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>{% block title %}Questionnaire{% endblock title %}</title>
<link rel="stylesheet" href="/static/bootstrap/bootstrap.min.css" type="text/css"/>
<link rel="stylesheet" href="/static/questionnaire.css"></script>
<style type="text/css">
{% block styleextra %}
{% endblock %}
</style>
{% block headextra %}
{% endblock %}
<!--[if lt IE 9]>
<script src="http://html5shim.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
</head>
<body>
<div class="container">
<div class="content">
<div id="languages">
{% block language %}
{% for lang in LANGUAGES %}
{% if not forloop.first %} | {% endif %}
<a href="/setlang/?lang={{ lang.0 }}&next={{ request.path }}">{{ lang.1 }}</a>
{% endfor %}
{% endblock language %}
</div>
<div class="page-header">
<h1>Sample Django Questionnaire</h1>
</div>
<div class="row">
<div class="span1">&nbsp;</div>
<div class="span14">
{% block content %}
{% block questionnaire %}
<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 %}
{% endblock %}
</div>
<div class="span1">&nbsp;</div>
</div>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,12 @@
{% extends "base-questionnaire.html" %}
{% block title %}
{{ block.super }} - {{ page.title }}
{% endblock %}
{% block questionnaire %}
{{ page.body }}
{% if user.is_authenticated %}
<a href="/admin/page/page/{{ page.slug }}/">(edit)</a>
{% endif %}
{% endblock %}

View File

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

View File

@ -0,0 +1,14 @@
{% extends 'basedocumentation.html' %}
{% load url from future %}
{% block title %}Tell us Stuff!{% endblock %}
{% block doccontent %}
<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>
{% endblock %}

View File

@ -0,0 +1,23 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<ul class="inputs-list">
{% for sel, choice in qdict.choices %}
<li>
<label>
<input onClick="valchanged('{{ question.number }}', this.value)" type="radio" id="{{ question.number }}_{{ forloop.counter }}" name="question_{{ question.number }}" value="{{ choice.value }}" {% if sel %} checked {% endif %}>
<span>{{ choice.text }}</span>
</label>
</li>
{% endfor %}
</ul>
</div>
<div class="input">
<input onClick="valchanged('{{ question.number }}', '_entry_');" type="radio" id="{{ question.number }}_entry" name="question_{{ question.number }}" value="_entry_" {% if qdict.sel_entry %} checked {% endif %}>
{% if question.extra %}
<span class="extra-block">{{ question.extra }}</span>
{% endif %}
<input id="{{ question.number }}_comment" checks="dep_check('{{ question.number }},_entry_')" type="input" name="question_{{ question.number }}_comment" value="{{ qdict.comment }}">
</div>
</div>

View File

@ -0,0 +1,48 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<ul class="inputs-list">
{% for choice, key, checked, prev_value in qdict.choices %}
<li>
<!-- <label> -->
<span class="{{ qdict.type }}-text">
<input onClick="valchanged('{{ question.number }}_{{ choice.value }}', this.checked);" type="checkbox" id="{{ key }}" name="{{ key }}" value="{{ choice.value }}" {{ checked }}>
{{ choice.text }}
</span>
<!-- limits text width if we're in a choice-multiple-values control - allows proper alignment of value boxes -->
{% if qdict.type == 'choice-multiple-values' %}
<span checks="dep_check('{{ question.number }},{{ choice.value }}')" id="q{{ question.number }}_{{ choice.value }}_box">
<input type="text" value="{{ prev_value }}" maxlength="4" style="margin-left: 40px; width: 4em"
id="question_{{ question.number }}_{{ choice.value }}_value" name="question_{{ question.number }}_{{ choice.value }}_value">
&#37; <!-- percentage sign: all choice-multiple-values currently represent percentages and must add up to 100% -->
</span>
{% endif %}
<!-- </label> -->
</li>
{% endfor %}
{% if qdict.type == 'choice-multiple-values' %}
<script type="text/javascript">
alignValueBoxes("{{ question.number }}");
</script>
{% endif %}
{% if question.extra %}
<li>
<span class="extra-block">{{ question.extra }}</span>
</li>
{% endif %}
{% if qdict.extras %}
{% for key, value in qdict.extras %}
<li>
{% if not forloop.last or not forloop.first %}
<b>{{ forloop.counter }}.</b>
{% endif %}
<input type="text" name="{{ key }}" size="50" value="{{ value }}">
</li>
{% endfor %}
{% endif %}
</ul>
</div>
</div>

View File

@ -0,0 +1,43 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<ul class="inputs-list">
<!-- yes -->
<li>
<label>
<input type="radio" id="{{ question.number }}_yes" name="question_{{ question.number }}" value="yes" onClick="valchanged('{{ question.number }}', this.value);" {% ifequal qdict.value "yes" %} checked{% endifequal %}>
<span>{% trans "Yes" %}</span>
</label>
</li>
<!-- no -->
<li>
<label>
<input type="radio" id="{{ question.number }}_no" name="question_{{ question.number }}" value="no" onClick="valchanged('{{ question.number }}', this.value);" {% ifequal qdict.value "no" %} checked {% endifequal %}>
<span>{% trans "No" %}</span>
</label>
</li>
<!-- don't know -->
{% if qdict.hasdontknow %}
<li>
<label>
<input type="radio" id="{{ question.number }}_dontknow" name="question_{{ question.number }}" value="dontknow" onClick="valchanged('{{ question.number }}', this.value);" {% ifequal qdict.value "dontknow" %} checked {% endifequal %}>
<span>{% trans "Don't Know" %}</span>
</label>
</li>
{% endif %}
<!-- comment -->
{% if qdict.hascomment %}
<li>
{% if question.extra %}
<span class="extra-block">{{ question.extra }}</span><br />
{% endif %}
<input type="text" id="{{ question.number }}_comment" name="question_{{ question.number }}_comment" value="{{ qdict.comment }}" size="50" {{ qdict.checks|safe }} placeholder="{% trans 'comment' %}">
</li>
{% endif %}
</ul>
</div>
</div>

View File

@ -0,0 +1,20 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<ul class="inputs-list">
{% for sel, choice in qdict.choices %}
<li>
<label>
<input type="radio" id="{{ question.number }}_{{ forloop.counter }}" name="question_{{ question.number }}" onClick="valchanged('{{ question.number }}', this.value)" value="{{ choice.value }}"{% if sel %} checked {% endif %}>
<span>{{ choice.text }}</span>
</label>
</li>
{% endfor %}
{% if question.extra %}
<li>
<span class="help-block">{{ question.extra }}</span>
</li>
{% endif %}
</ul>
</div>
</div>

View File

@ -0,0 +1,10 @@
{% extends "base-questionnaire.html" %}
{% block questionnaire %}
<h2>
Merci vielmals! Die Umfragung ist fertig! Sie bekommen eine neue Einladung nächstes Jahr.
</h2>
<h4>
Wenn Sie Ihre E-Mail Adresse in der Zwischenzeit ändern, teilen Sie dies uns bitte mit.
</h4>
{% endblock %}

View File

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

View File

@ -0,0 +1 @@
{% include question.checks %}

View File

@ -0,0 +1,11 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<select id="{{ question.number }}" name="question_{{ question.number }}" onChange="valchanged('{{ question.number }}', this.value)">
<option value="">{% if question.extra %}{{question.extra}}{% else %}--{% endif %}</option>
{% for sel, choice in qdict.choices %}
<option value="{{ choice.value }}"{% if sel %} selected{% endif %}>{{ choice.text }}</option>
{% endfor %}
</select>
</div>
</div>

View File

@ -0,0 +1,9 @@
<div class="clearfix rangeinput">
<div class="input">
<input name="question_{{ question.number }}" type="{{ question.type }}" min="{{ qdict.rmin }}" max="{{ qdict.rmax }}" step="{{ qdict.rstep }}" value="{{ qdict.current }}">
</div>
<label data-unit="{{ qdict.runit }}">{{ qdict.runit }}</label>
{% if question.extra %}
<span class="help-block">{{ question.extra }}</span>
{% endif %}
</div>

View File

@ -0,0 +1,9 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<textarea class="span8" name="question_{{ question.number }}" cols="60" rows="10">{{ qdict.value }}</textarea>
{% if question.extra %}
<span class="help-block">{{ question.extra }}</span>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,10 @@
{% load i18n %}
<div class="clearfix">
<div class="input">
<input type="text" class="span8" size="60" id="{{ question.number }}" name="question_{{ question.number }}" value="{{ qdict.value }}">
{% if question.extra %}
<span class="help-block">{{ question.extra }}</span>
{% endif %}
</div>
</div>

View File

@ -0,0 +1,178 @@
{% extends "base-questionnaire.html" %}
{% load questionnaire i18n %}
{% load static %}
{% load dynamicStyleTags %}
{% load landings %}
{% block headextra %}
<script type="text/javascript" src="{% static 'jquery-1.7.1.min.js' %}"></script>
<script type="text/javascript" src="{% static 'questionset.js' %}"></script>
<link rel="stylesheet" href="{% static 'progressbar.css' %}"/>
{% if questionsetstylesheet|getAssociatedStylesheets %}
<style type="text/css">
{{ questionsetstylesheet|getAssociatedStylesheets|safe }}
</style>
{% endif %}
{% for x in jsinclude %}
<script type="text/javascript" src="{{ x }}"></script>
{% endfor %}
{% for x in cssinclude %}
<link rel="stylesheet" href="{{ x }}" type="text/css" />
{% endfor %}
{% if async_progress %}
<script type="text/javascript">
var progress_url = "{{ async_url }}";
</script>
<script type="text/javascript" src="{% static 'progress.js' %}"></script>
{% endif %}
{% endblock %}
{% block language %}
{% for lang in LANGUAGES %}{% if not forloop.first %} |{% endif %}
<a href="{{request.path}}?lang={{ lang.0 }}">{{ lang.1 }}</a>
{% endfor %}
{% endblock %}
{% block questionnaire %}
{% if progress %}
{% if questionset.questionnaire.name|add:"Progress"|getAssociatedStylesheets %}
<style type="text/css">
{{ questionset.questionnaire.name|add:"Progress"|getAssociatedStylesheets|safe }}
</style>
{% else %}
<style type="text/css">
{{ "CommonProgressStyles"|getAssociatedStylesheets|safe }}
</style>
{% endif %}
<div id="progress_bar" class="ui-progress-bar ui-container">
<div class="ui-progress" style="width: {{progress}}%;">
<span class="ui-label"><b class="value">{{progress}}%</b></span>
</div>
</div>
{% endif %}
<h2 class="questionset-title">
{% if questionset.heading %}
{% if questionset.parse_html %}
{% render_with_landing questionset.heading|safe %}
{% else %}
{{ questionset.heading }}
{% endif %}
{% endif %}
</h2>
<div class="questionset-text">
{% if questionset.text %}
{% if questionset.parse_html %}
{% render_with_landing questionset.text|safe %}
{% else %}
{{ questionset.text }}
{% endif %}
{% endif %}
</div>
<form name="qform" id="qform" action="{{ request.path }}" method="POST">
{% csrf_token %}
<input type="hidden" name="questionset_id" value="{{ questionset.id }}">
{% for question, qdict in qlist %}
{% if question.id|getAssociatedStylesheets %}
<style type="text/css">
{{question.id|getAssociatedStylesheets|safe}}
</style>
{% endif %}
{% with errors|dictget:question.number as error %}
<div class="question type_{{ qdict.qtype }} {% if error %} error prepend-top{% endif %}{{ qdict.qnum_class }}{{ qdict.qalpha_class }}" id="qc_{{ question.number }}" {{qdict.checkstring|safe}} style="{{qdict.css_style}}">
{% if request.user.is_staff %}
<span class="pull-right">
<a href="/admin/questionnaire/question/{{ question.id }}/">
({% trans "edit" %} {{ question.number }})
</a>
</span>
{% endif %}
{% if qdict.custom %}
{% if error %}
<div class="error">
{{ error }}
</div>
{% endif %}
{% include qdict.template %}
{% else %}
<div class="question-text {% if qdict.required %}required{% endif %}">
<span class="qnumber">{{ question.display_number|safe }}.</span>
{% if question.parse_html %}
{{ question.text|safe }}
{% else %}
{{ question.text }}
{% endif %}
</div>
<div class="answer">
{% if error %}
<div class="alert-message block-message error input"><strong>{{ error }}</strong></div>
{% endif %}
{% include qdict.template %}
</div>
{% endif %}
</div> <!-- /question container -->
{% if question.footer %}
<div class="question-footer" id="qc_{{ question.number }}_footer" {{qdict.footerchecks|safe}}>
{% if question.parse_html %}
{{ question.footer|safe }}
{% else %}
{{ question.footer }}
{% endif %}
<div class="clearfix"></div>
</div>
{% endif %}
{% endwith %}
{% endfor %}
<div style="text-align: center;" class="well questionset-submit">
<input class="btn large primary" name="submit" type="submit" value="{% trans "Continue" %}">
</div>
{% if questionset.prev %}
<a class="back-link pull-left" href="{{ prev_url }}">{% trans "return to previous page" %}</a>
{% endif %}
</form>
{% if debug_questionnaire %}
Previous Answers:
{% for answer in current_answers %}
{% if forloop.first %}
<ul>
{% endif %}
<li>{{ answer.question }}: {{ answer.answer }}</li>
{% if forloop.last %}
</ul>
{% endif %}
{% endfor %}
{% endif %}
<script type="text/javascript">
{% for trigger in triggers %}
addtrigger("{{trigger}}");
{% endfor %}
{% for k,v in qvalues.items %}
qvalues['{{ k|escapejs }}'] = '{{ v|escapejs }}';
{% endfor %}
for(key in qvalues) {
valchanged(key, qvalues[key]);
}
</script>
{% endblock %}

View File

@ -0,0 +1 @@
{% extends "questionnaire/number.html" %}

View File

@ -0,0 +1,13 @@
<div class="clearfix">
<div class="input">
<input type="text" class="span2" name="question_{{ question.number }}" size="10" value="{{ qdict.value }}">
<select class="span2" name="question_{{ question.number }}_unit">
{% for val, desc, sel in qdict.timeperiods %}
<option value="{{ val }}"{% if sel %} selected="selected"{% endif %}>{{ desc }}</option>
{% endfor %}
</select>
{% if question.extra %}
<span class="help-block">{{ question.extra }}</span>
{% endif %}
</div>
</div>

View File

View File

@ -0,0 +1,15 @@
#!/usr/bin/python
from django import template
from ..models import DBStylesheet
register = template.Library()
@register.filter(name="getAssociatedStylesheets")
def getAssociatedStylesheets(inclusionTag):
if DBStylesheet.objects.filter(inclusion_tag=inclusionTag).exists():
return DBStylesheet.objects.get(inclusion_tag=inclusionTag).content
else:
return None

View File

@ -0,0 +1,12 @@
import django.template
from django.template import Template, Context
register = django.template.Library()
@register.simple_tag(takes_context=True)
def render_with_landing(context, text):
block_context = Context({'landing_object' : context['runinfo'].landing.content_object})
if text:
template = Template(text)
return template.render(block_context)
else:
return ''

View File

@ -0,0 +1,29 @@
#!/usr/bin/python
from django import template
from django.core.urlresolvers import reverse
register = template.Library()
@register.filter(name="dictget")
def dictget(thedict, key):
"{{ dictionary|dictget:variableholdingkey }}"
return thedict.get(key, None)
@register.filter(name="spanclass")
def spanclass(string):
l = 2 + len(string.strip()) // 6
if l <= 4:
return "span-4"
if l <= 7:
return "span-7"
if l < 10:
return "span-10"
return "span-%d" % l
@register.filter(name="qtesturl")
def qtesturl(question):
qset = question.questionset
return reverse("questionset",
args=("test:%s" % qset.questionnaire.id,
qset.sortid))

62
questionnaire/tests.py Normal file
View File

@ -0,0 +1,62 @@
from django.test import TestCase
from .dependency_checker import check_actual_answers_against_expression, explode_answer_into_list
from .models import Question
class QuestionSetTests(TestCase):
def test_exploding_answer_into_list(self):
answer = ['1A']
self.assertEqual(['1A'], explode_answer_into_list(answer))
answer = ['1A', ['foobar']]
self.assertEqual(['1A', 'foobar'], explode_answer_into_list(answer))
answer = ['1A', ['foobar', 'barfoo']]
self.assertEqual(['1A', 'foobar', 'barfoo'], explode_answer_into_list(answer))
def test_dependencies_for_multiple_choice_question(self):
check_question = Question()
self.assertTrue(check_actual_answers_against_expression('3B', ['3B', '3E'], check_question))
self.assertTrue(check_actual_answers_against_expression('3E', ['3B', '3E'], check_question))
def test_dependencies_for_multiple_choice_question_false(self):
check_question = Question()
self.assertFalse(check_actual_answers_against_expression('3C', ['3B', '3E'], check_question))
self.assertFalse(check_actual_answers_against_expression('3F', ['3B', '3E'], check_question))
def test_dependencies_for_multiple_choice_question_negation(self):
check_question = Question()
self.assertTrue(check_actual_answers_against_expression('!3C', ['3B', '3E'], check_question))
self.assertTrue(check_actual_answers_against_expression('!3A', ['3B', '3E'], check_question))
def test_dependencies_for_multiple_choice_question_negation_false(self):
check_question = Question()
self.assertFalse(check_actual_answers_against_expression('!3C', ['3C', '3E'], check_question))
self.assertFalse(check_actual_answers_against_expression('!3E', ['3C', '3E'], check_question))
def test_dependencies_for_single_choice_question(self):
check_question = Question()
self.assertTrue(check_actual_answers_against_expression('3B', '3B', check_question))
self.assertFalse(check_actual_answers_against_expression('3C', '3B', check_question))
self.assertTrue(check_actual_answers_against_expression('!3C', '3B', check_question))
self.assertFalse(check_actual_answers_against_expression('!3C', '', check_question))
self.assertFalse(check_actual_answers_against_expression('!3C', [''], check_question))
def test_dependencies_for_numeric_checks(self):
check_question = Question()
self.assertTrue(check_actual_answers_against_expression('>5.6', '6', check_question))
self.assertFalse(check_actual_answers_against_expression('>5.6', '3.6', check_question))
self.assertFalse(check_actual_answers_against_expression('>5.6', '5.6', check_question))
self.assertTrue(check_actual_answers_against_expression('>=5.6', '6', check_question))
self.assertFalse(check_actual_answers_against_expression('>=5.6', '3.6', check_question))
self.assertTrue(check_actual_answers_against_expression('>=5.6', '5.6', check_question))
self.assertTrue(check_actual_answers_against_expression('<5.6', '4.6', check_question))
self.assertFalse(check_actual_answers_against_expression('<5.6', '8.6', check_question))
self.assertFalse(check_actual_answers_against_expression('<5.6', '5.6', check_question))
self.assertTrue(check_actual_answers_against_expression('<=5.6', '3.6', check_question))
self.assertFalse(check_actual_answers_against_expression('<=5.6', '9.6', check_question))
self.assertTrue(check_actual_answers_against_expression('<=5.6', '5.6', check_question))

44
questionnaire/urls.py Normal file
View File

@ -0,0 +1,44 @@
# vim: set fileencoding=utf-8
from django.conf.urls import *
from .views import *
from .page.views import page, langpage
urlpatterns = patterns(
'',
url(r'^$',
questionnaire, name='questionnaire_noargs'),
url(r'^csv/(?P<qid>\d+)/$',
export_csv, name='export_csv'),
url(r'^(?P<runcode>[^/]+)/progress/$',
get_async_progress, name='progress'),
url(r'^take/(?P<questionnaire_id>[0-9]+)/$', generate_run),
url(r'^$', page, {'page_to_render' : 'index'}),
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'^q/manage/csv/(\d+)/',
export_csv, name="export_csv"),
)
if not use_session:
urlpatterns += patterns(
'',
url(r'^(?P<runcode>[^/]+)/$',
questionnaire, name='questionnaire'),
url(r'^(?P<runcode>[^/]+)/(?P<qs>[-]{0,1}\d+)/prev/$',
redirect_to_prev_questionnaire,
name='redirect_to_prev_questionnaire'),
)
else:
urlpatterns += patterns(
'',
url(r'^$',
questionnaire, name='questionnaire'),
url(r'^prev/$',
redirect_to_prev_questionnaire,
name='redirect_to_prev_questionnaire')
)

60
questionnaire/utils.py Normal file
View File

@ -0,0 +1,60 @@
#!/usr/bin/python
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, bstr)
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):
request_string = str(request)
string_chunks = request_string.split('/')
return string_chunks[2]
def get_sortid_from_request(request):
request_string = str(request)
string_chunks = request_string.split('/')
if len(string_chunks) > 3:
return string_chunks[3]
#not enough string chunks to get a sortid
return None
if __name__ == "__main__":
import doctest
doctest.testmod()

View File

@ -0,0 +1,20 @@
#!/usr/bin/python
from .models import RunInfoHistory, Answer
def get_completed_answers_for_questions(questionnaire_id, question_list):
completed_questionnaire_runs = RunInfoHistory.objects.filter(questionnaire__id=questionnaire_id)
completed_answers = []
for run in completed_questionnaire_runs:
specific_answers = Answer.objects.filter(runid=run.runid, question_id__in=question_list)
answer_set = []
for answer in specific_answers:
if answer.answer != '[]':
answer_set.append([int(answer.question_id), answer.answer])
if len(answer_set) > 0:
completed_answers.append(answer_set)
return completed_answers
if __name__ == "__main__":
import doctest
doctest.testmod()

1088
questionnaire/views.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -24,6 +24,7 @@ django-celery==3.0.9
django-ckeditor==4.5.1
#django-email-change==0.2.3
git+git://github.com/novagile/django-email-change.git@0d7cb91b987a0505a9c4bde126d452233dea266d
django-compat==1.0.10
django-endless-pagination==2.0
django-extensions==0.9
django-jsonfield==0.9.10
@ -38,6 +39,7 @@ django-selectable==0.7.0
django-smtp-ssl==1.0
django-storages==1.1.6
django-tastypie==0.9.11
django-transmeta==0.7.3
feedparser==5.1.2
freebase==1.0.8
#gitenberg.metadata==0.1.6

View File

@ -8,6 +8,9 @@ from regluit.payment.parameters import PAYMENT_HOST_PAYPAL, PAYMENT_HOST_AMAZON
PROJECT_DIR = dirname(dirname(realpath(__file__)))
LANGUAGE_CODE = 'en-us'
LANGUAGES = (
('en', 'English'),
)
LOCAL_TEST = False
WISHED_LANGS = ('en','fr','es','de','el','pt','it','ru','cs','ja','zh','nl','ut','ar','la','id','ca','fa','sv','sl','ko','tr')
@ -72,6 +75,8 @@ STATICFILES_DIRS = (
# Put strings here, like "/home/html/static" or "C:/www/django/static".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
join(PROJECT_DIR,'questionnaire/static/'),
)
# List of finder classes that know how to find static files in
@ -79,7 +84,7 @@ STATICFILES_DIRS = (
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
# 'django.contrib.staticfiles.finders.DefaultStorageFinder',
'django.contrib.staticfiles.finders.DefaultStorageFinder',
)
# Make this unique, and don't share it with anybody.
@ -112,6 +117,8 @@ MIDDLEWARE_CLASSES = (
'django.contrib.messages.middleware.MessageMiddleware',
'maintenancemode.middleware.MaintenanceModeMiddleware',
'regluit.core.auth.SocialAuthExceptionMiddlewareWithoutMessages',
'django.middleware.locale.LocaleMiddleware',
'regluit.questionnaire.request_cache.RequestCacheMiddleware',
)
ROOT_URLCONF = 'regluit.urls'
@ -122,6 +129,7 @@ TEMPLATE_DIRS = (
# Don't forget to use absolute paths, not relative paths.
join(PROJECT_DIR, "frontend", "templates"),
join(PROJECT_DIR, "frontend", "templates", "registration"),
join(PROJECT_DIR, "questionnaire", "templates"),
)
INSTALLED_APPS = (
@ -143,7 +151,6 @@ INSTALLED_APPS = (
'regluit.marc',
'regluit.payment',
'regluit.utils',
'regluit.survey',
'registration',
'social.apps.django_app.default',
'tastypie',
@ -164,6 +171,10 @@ INSTALLED_APPS = (
'regluit.booxtream',
'regluit.pyepub',
'regluit.libraryauth',
'transmeta',
'regluit.questionnaire',
'regluit.questionnaire.page',
'regluit.survey',
)
# A sample logging configuration. The only tangible logging
@ -468,3 +479,6 @@ SOUTH_MIGRATION_MODULES = {
MOBIGEN_URL = "https://docker.gluejar.com:5001/mobigen"
MOBIGEN_USER_ID = "admin"
MOBIGEN_PASSWORD = "CXq5FSEQFgXtP_s"
QUESTIONNAIRE_USE_SESSION = True
QUESTIONNAIRE_DEBUG = True

View File

@ -6,7 +6,9 @@ from django.db import models
class Migration(SchemaMigration):
needed_by = (
("questionnaire", "0005_move_nonces"),
)
def forwards(self, orm):
# Adding model 'Landing'
db.create_table('survey_landing', (

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
import datetime
from south.db import db
from south.v2 import SchemaMigration
from django.db import models
class Migration(SchemaMigration):
depends_on = (
("questionnaire", "0005_move_nonces"),
)
def forwards(self, orm):
# Deleting model 'Landing'
db.delete_table('survey_landing')
def backwards(self, orm):
# Adding model 'Landing'
db.create_table('survey_landing', (
('nonce', self.gf('django.db.models.fields.CharField')(max_length=32, null=True)),
('object_id', self.gf('django.db.models.fields.PositiveIntegerField')(null=True)),
('label', self.gf('django.db.models.fields.CharField')(max_length=64, null=True)),
('content_type', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['contenttypes.ContentType'], null=True)),
('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
))
db.send_create_signal('survey', ['Landing'])
models = {
}
complete_apps = ['survey']

View File

@ -7,24 +7,3 @@ from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models.signals import post_save
class Landing(models.Model):
# defines an entry point to a Feedback session
nonce = models.CharField(max_length=32, null=True,blank=True)
content_type = models.ForeignKey(ContentType, null=True,blank=True)
object_id = models.PositiveIntegerField(null=True,blank=True)
content_object = GenericForeignKey('content_type', 'object_id')
label = models.CharField(max_length=64, blank=True)
def _hash(self):
return uuid.uuid4().hex
def __str__(self):
return self.label
def config_landing(sender, instance, created, **kwargs):
if created:
instance.nonce=instance._hash()
instance.save()
post_save.connect(config_landing,sender=Landing)

View File

@ -19,12 +19,14 @@ urlpatterns = patterns('',
url(r'', include('regluit.libraryauth.urls')),
url(r'', include('regluit.marc.urls')),
url(r'^bisac/', include('regluit.bisac.urls')),
url(r'^survey/', include('regluit.survey.urls')),
url(r'^selectable/', include('selectable.urls')),
url(r'^admin/', include(admin_site.urls)),
url(r'^comments/', include('django.contrib.comments.urls')),
url(r'^notification/', include(notification.urls)),
url(r'^ckeditor/', include('ckeditor.urls')),
# questionnaire urls
url(r'^survey/', include('regluit.questionnaire.urls')),
)
urlpatterns += patterns('django.contrib.sitemaps.views',