Initial Checkin of Seantis Questionnaire

EmailTemplateFixes
Robert Thomson 2009-05-17 13:34:55 +02:00
commit 5c3d9ca1a0
93 changed files with 8256 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*.pyc
*.mo
*~
*.sqlite
*.swp
local_settings.py

0
example/__init__.py Normal file
View File

File diff suppressed because one or more lines are too long

14
example/manage.py Executable file
View File

@ -0,0 +1,14 @@
#!/usr/bin/env python
from django.core.management import execute_manager
import os, os.path, sys
sys.path.insert(0, os.path.abspath("../"))
try:
import settings # Assumed to be in the same directory.
except ImportError:
import sys
sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__)
sys.exit(1)
if __name__ == "__main__":
execute_manager(settings)

0
example/page/__init__.py Normal file
View File

8
example/page/admin.py Normal 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)

22
example/page/models.py Normal file
View File

@ -0,0 +1,22 @@
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(max_length=256)
body = models.TextField()
public = models.BooleanField(default=True)
def __unicode__(self):
return u"Page[%s]" % self.slug
def get_absolute_url(self):
return reverse('page.views.page', kwargs={'page':self.slug})
class Meta:
pass
translate = ('title','body',)

33
example/page/views.py Normal file
View File

@ -0,0 +1,33 @@
# Create your views here.
from django.shortcuts import render_to_response, get_object_or_404
from django.template import RequestContext
from django import http
from django.utils import translation
from models import Page
def page(request, page):
p = get_object_or_404(Page, slug=page)
if not p.public:
raise Http404
return render_to_response("page.html", { "request" : request, "page":p, }, context_instance = RequestContext(request) )
def langpage(request, lang, page):
translation.activate_language(lang)
return page(request, page)
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

3601
example/pyparsing.py Normal file

File diff suppressed because it is too large Load Diff

96
example/settings.py Normal file
View File

@ -0,0 +1,96 @@
# Django settings for example project.
import os.path
DEBUG = True
TEMPLATE_DEBUG = DEBUG
ADMINS = (
# ('Your Name', 'your_email@domain.com'),
)
MANAGERS = ADMINS
DATABASE_ENGINE = 'sqlite3' # 'postgresql_psycopg2', 'postgresql', 'mysql', 'sqlite3' or 'oracle'.
DATABASE_NAME = 'example.sqlite' # Or path to database file if using sqlite3.
DATABASE_USER = '' # Not used with sqlite3.
DATABASE_PASSWORD = '' # Not used with sqlite3.
DATABASE_HOST = '' # Set to empty string for localhost. Not used with sqlite3.
DATABASE_PORT = '' # Set to empty string for default. Not used with sqlite3.
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# If running in a Windows environment this must be set to the same as your
# system time zone.
TIME_ZONE = 'Europe/Berlin'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'en'
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# Absolute path to the directory that holds media.
# Example: "/home/media/media.lawrence.com/"
MEDIA_ROOT = os.path.abspath('../questionnaire/media/')
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash if there is a path component (optional in other cases).
# Examples: "http://media.lawrence.com", "http://example.com/media/"
MEDIA_URL = '/media/'
# URL prefix for admin media -- CSS, JavaScript and images. Make sure to use a
# trailing slash.
# Examples: "http://foo.com/media/", "/media/".
ADMIN_MEDIA_PREFIX = '/media/admin/'
# Make this unique, and don't share it with anybody.
SECRET_KEY = 'j69g6-&t0l43f06iq=+u!ni)9n)g!ygy4dk-dgdbrbdx7%9l*6'
# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = (
'django.template.loaders.filesystem.load_template_source',
'django.template.loaders.app_directories.load_template_source',
# 'django.template.loaders.eggs.load_template_source',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.middleware.locale.LocaleMiddleware',
)
ROOT_URLCONF = 'example.urls'
TEMPLATE_DIRS = (
# Put strings here, like "/home/html/django_templates" or "C:/www/django/templates".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
os.path.abspath("../questionnaire/templates/"),
os.path.abspath("./templates/"),
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.admin',
'django.contrib.markup',
'transmeta',
'questionnaire',
'page',
)
LANGUAGES = (
('en', 'English'),
('de', 'Deutsch'),
)
try: from local_settings import *
except: pass

View File

@ -0,0 +1,59 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<!-- This django based questionnaire application was developed by
Robert Thomson for Seantis GmbH of Lucerne. With much thanks to
Leonie Grüter for all the testing, bug reports and suggestions! -->
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>{% block title %}Questionnaire{% endblock title %}</title>
<link rel="stylesheet" href="/media/blueprint/screen.css" type="text/css" media="screen, projection" />
<link rel="stylesheet" href="/media/blueprint/print.css" type="text/css" media="print" />
<!--[if IE]><link rel="stylesheet" href="/media/blueprint/ie.css" type="text/css" media="screen, projection"><![endif]-->
<style type="text/css">
body { background-color: #F1F1F1; }
#wrapper {
background-color: #fff;
margin-top: 2em;
margin-left: auto;
margin-right: auto;
width: 800px;
padding: 1em;
border: 1px solid #ccc;
}
.container { border-bottom: 1px solid #DFDFDF; }
.aleven { background-color: #F1F1F1; }
hr { height: 1px; }
{% block styleextra %}{% endblock %}
</style>
{% block headextra %}{% endblock %}
</head>
<body>
<div id="wrapper">
<div style="float: right; margin-right: 3em;" id="languages">
{% block language %}
{% for lang in LANGUAGES %}
{% if not forloop.first %} |{% endif %}
<a href="/setlang/?language={{ lang.0 }}&next={{ request.path }}">{{ lang.1 }}</a>
{% endfor %}
{% endblock language %}
</div> <!--/languages -->
<div id="header">
<div style="height: 60px; width: 180px; float: left"><b>Logo Goes Here. Logo Goes Here. Logo Goes Here. Logo Goes Here.</b></div>
<H2>Sample Django Questionnaire</H2>
<HR>
</div><!-- /header -->
<div id="content" class="container">
{% block content %}
<p>Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam,
quis nostrud execitation ullamco laboris nisi ut aliquip ex ea commodo
consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
cillum dolore eu fugiat nulla pariatur.</p>
{% endblock %}
</div><!-- /content -->
</div><!-- /wrapper -->
</body>
</html>

View File

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

View File

@ -0,0 +1,123 @@
import copy
from django.db import models
from django.db.models.fields import NOT_PROVIDED
from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.utils.translation import get_language
LANGUAGE_CODE = 0
LANGUAGE_NAME = 1
def get_real_fieldname(field, lang):
return '%s_%s' % (field, lang)
def get_real_fieldname_in_each_language(field):
return [get_real_fieldname(field, lang[LANGUAGE_CODE])
for lang in settings.LANGUAGES]
def canonical_fieldname(db_field):
""" all "description_en", "description_fr", etc. field names will return "description" """
return getattr(db_field, 'original_fieldname', db_field.name) # original_fieldname is set by transmeta
def default_value(field):
'''
When accessing to the name of the field itself, the value
in the current language will be returned. Unless it's set,
the value in the default language will be returned.
'''
def default_value_func(self):
attname = lambda x: get_real_fieldname(field, x)
if getattr(self, attname(get_language()), None):
result = getattr(self, attname(get_language()))
elif getattr(self, attname(get_language()[:2]), None):
result = getattr(self, attname(get_language()[:2]))
elif getattr(self, attname(settings.LANGUAGE_CODE), None):
result = getattr(self, attname(settings.LANGUAGE_CODE))
else:
default_transmeta_attr = attname(
getattr(settings, 'TRANSMETA_DEFAULT_LANGUAGE', 'en')
)
result = getattr(self, default_transmeta_attr, None)
return result
return default_value_func
class TransMeta(models.base.ModelBase):
'''
Metaclass that allow a django field, to store a value for
every language. The syntax to us it is next:
class MyClass(models.Model):
__metaclass__ transmeta.TransMeta
my_field = models.CharField(max_length=20)
my_i18n_field = models.CharField(max_length=30)
class Meta:
translate = ('my_i18n_field',)
Then we'll be able to access a specific language by
<field_name>_<language_code>. If just <field_name> is
accessed, we'll get the value of the current language,
or if null, the value in the default language.
'''
def __new__(cls, name, bases, attrs):
if 'Meta' in attrs and hasattr(attrs['Meta'], 'translate'):
fields = attrs['Meta'].translate
delattr(attrs['Meta'], 'translate')
else:
new_class = super(TransMeta, cls).__new__(cls, name, bases, attrs)
# we inherits possible translatable_fields from superclasses
abstract_model_bases = [base for base in bases if hasattr(base, '_meta') \
and base._meta.abstract]
translatable_fields = []
for base in abstract_model_bases:
if hasattr(base._meta, 'translatable_fields'):
translatable_fields.extend(list(base._meta.translatable_fields))
new_class._meta.translatable_fields = tuple(translatable_fields)
return new_class
if not isinstance(fields, tuple):
raise ImproperlyConfigured("Meta's translate attribute must be a tuple")
default_language = getattr(settings, 'TRANSMETA_DEFAULT_LANGUAGE', \
settings.LANGUAGE_CODE)
for field in fields:
if not field in attrs or \
not isinstance(attrs[field], models.fields.Field):
raise ImproperlyConfigured(
"There is no field %(field)s in model %(name)s, "\
"as specified in Meta's translate attribute" % \
dict(field=field, name=name))
original_attr = attrs[field]
for lang in settings.LANGUAGES:
lang_code = lang[LANGUAGE_CODE]
lang_attr = copy.copy(original_attr)
lang_attr.original_fieldname = field
lang_attr_name = get_real_fieldname(field, lang_code)
if lang_code != default_language:
# only will be required for default language
if not lang_attr.null and lang_attr.default is NOT_PROVIDED:
lang_attr.null = True
if not lang_attr.blank:
lang_attr.blank = True
if lang_attr.verbose_name:
lang_attr.verbose_name = u'%s %s' % (lang_attr.verbose_name, lang_code)
attrs[lang_attr_name] = lang_attr
del attrs[field]
attrs[field] = property(default_value(field))
new_class = super(TransMeta, cls).__new__(cls, name, bases, attrs)
if hasattr(new_class, '_meta'):
new_class._meta.translatable_fields = fields
return new_class

View File

View File

@ -0,0 +1,152 @@
"""
Detect new translatable fields in all models and sync database structure.
You will need to execute this command in two cases:
1. When you add new languages to settings.LANGUAGES.
2. When you new translatable fields to your models.
"""
from django.conf import settings
from django.core.management.base import BaseCommand
from django.core.management.color import no_style
from django.db import connection, transaction
from django.db.models import get_models
from transmeta import get_real_fieldname
def ask_for_default_language():
print 'Available languages:'
for i, lang_tuple in enumerate(settings.LANGUAGES):
print '\t%d. %s' % (i+1, lang_tuple[1])
print 'Choose a language in which to put current untranslated data.'
while True:
prompt = "What's the language of current data? (1-%s) " % len(lang_tuple)
answer = raw_input(prompt).strip()
if answer != '':
try:
index = int(answer) - 1
if index < 0 or index > len(settings.LANGUAGES):
print "That's not a valid number"
else:
return settings.LANGUAGES[index][0]
except ValueError:
print "Please write a number"
def ask_for_confirmation(sql_sentences, model_full_name):
print '\nSQL to synchronize "%s" schema:' % model_full_name
for sentence in sql_sentences:
print ' %s' % sentence
while True:
prompt = '\nAre you sure that you want to execute the previous SQL: (y/n) [n]: '
answer = raw_input(prompt).strip()
if answer == '':
return False
elif answer not in ('y', 'n', 'yes', 'no'):
print 'Please answer yes or no'
elif answer == 'y' or answer == 'yes':
return True
else:
return False
def print_missing_langs(missing_langs, field_name, model_name):
print '\nMissing languages in "%s" field from "%s" model: %s' % \
(field_name, model_name, ", ".join(missing_langs))
class Command(BaseCommand):
help = "Detect new translatable fields or new available languages and sync database structure"
def handle(self, *args, **options):
""" command execution """
# set manual transaction management
transaction.commit_unless_managed()
transaction.enter_transaction_management()
transaction.managed(True)
self.cursor = connection.cursor()
self.introspection = connection.introspection
self.default_lang = ask_for_default_language()
all_models = get_models()
found_missing_fields = False
for model in all_models:
if hasattr(model._meta, 'translatable_fields'):
model_full_name = '%s.%s' % (model._meta.app_label, model._meta.module_name)
translatable_fields = model._meta.translatable_fields
db_table = model._meta.db_table
for field_name in translatable_fields:
missing_langs = list(self.get_missing_languages(field_name, db_table))
if missing_langs:
found_missing_fields = True
print_missing_langs(missing_langs, field_name, model_full_name)
sql_sentences = self.get_sync_sql(field_name, missing_langs, model)
execute_sql = ask_for_confirmation(sql_sentences, model_full_name)
if execute_sql:
print 'Executing SQL...',
for sentence in sql_sentences:
self.cursor.execute(sentence)
# commit
transaction.commit()
print 'Done'
else:
print 'SQL not executed'
transaction.leave_transaction_management()
if not found_missing_fields:
print '\nNo new translatable fields detected'
def get_table_fields(self, db_table):
""" get table fields from schema """
db_table_desc = self.introspection.get_table_description(self.cursor, db_table)
return [t[0] for t in db_table_desc]
def get_missing_languages(self, field_name, db_table):
""" get only missings fields """
db_table_fields = self.get_table_fields(db_table)
for lang_code, lang_name in settings.LANGUAGES:
if get_real_fieldname(field_name, lang_code) not in db_table_fields:
yield lang_code
def was_translatable_before(self, field_name, db_table):
""" check if field_name was translatable before syncing schema """
db_table_fields = self.get_table_fields(db_table)
if field_name in db_table_fields:
# this implies field was never translatable before, data is in this field
return False
else:
return True
def get_sync_sql(self, field_name, missing_langs, model):
""" returns SQL needed for sync schema for a new translatable field """
qn = connection.ops.quote_name
style = no_style()
sql_output = []
db_table = model._meta.db_table
was_translatable_before = self.was_translatable_before(field_name, db_table)
for lang in missing_langs:
new_field = get_real_fieldname(field_name, lang)
f = model._meta.get_field(new_field)
col_type = f.db_type()
field_sql = [style.SQL_FIELD(qn(f.column)), style.SQL_COLTYPE(col_type)]
# column creation
sql_output.append("ALTER TABLE %s ADD COLUMN %s" % (qn(db_table), ' '.join(field_sql)))
if lang == self.default_lang and not was_translatable_before:
# data copy from old field (only for default language)
sql_output.append("UPDATE %s SET %s = %s" % (qn(db_table), \
qn(f.column), qn(field_name)))
if not f.null and lang == self.default_lang:
# changing to NOT NULL after having data copied
sql_output.append("ALTER TABLE %s ALTER COLUMN %s SET %s" % \
(qn(db_table), qn(f.column), \
style.SQL_KEYWORD('NOT NULL')))
if not was_translatable_before:
# we drop field only if field was no translatable before
sql_output.append("ALTER TABLE %s DROP COLUMN %s" % (qn(db_table), qn(field_name)))
return sql_output

18
example/urls.py Normal file
View File

@ -0,0 +1,18 @@
from django.conf.urls.defaults import *
from django.contrib import admin
from django.conf import settings
admin.autodiscover()
urlpatterns = patterns('',
(r'q/', include('questionnaire.urls')),
(r'^$', 'page.views.page', {'page' : 'index'}),
(r'^(?P<page>.*)\.html$', 'page.views.page'),
(r'^(?P<lang>..)/(?P<page>.*)\.html$', 'page.views.langpage'),
(r'^setlang/$', 'page.views.set_language'),
(r'^media/(.*)', 'django.views.static.serve',
{ 'document_root' : settings.MEDIA_ROOT }),
(r'^admin/(.*)', admin.site.root),
)

93
questionnaire/__init__.py Normal file
View File

@ -0,0 +1,93 @@
"""
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 os, os.path
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
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) )
import questionnaire.qprocessors # make sure ours are imported first
add_type('sameas', 'Same as Another Question (put question number (alone) in checks')
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)

59
questionnaire/admin.py Normal file
View File

@ -0,0 +1,59 @@
#!/usr/bin/python
# vim: set fileencoding=utf-8
from django.contrib import admin
from models import *
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', 'number']
inlines = [ChoiceInline]
def queryset(self, request):
# we have a custom template
qs = Questionnaire.objects.all().order_by('name')
return qs
class QuestionnaireAdmin(admin.ModelAdmin):
pass
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', 'runid', 'question', 'answer']
list_display = ['runid', 'subject', 'question']
list_filter = ['subject', 'runid']
ordering = [ 'subject', 'runid', 'question', ]
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)

153
questionnaire/emails.py Normal file
View File

@ -0,0 +1,153 @@
"""
Functions to send email reminders to users.
"""
from django.core.mail import SMTPConnection, 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 models import Subject, QuestionSet, RunInfo
from datetime import datetime
from django.shortcuts import render_to_response, get_object_or_404
import random, time, smtplib, rfc822
try: from hashlib import md5
except: from md5 import md5
class SmarterEmailMessage(EmailMessage):
"""
SmarterEmailMessage allows rfc822 valid To addresses
"""
def recipients(self):
"""
For all recipients, convert to plain email address.
ie. Given "Joe Bloggs "<joe@example.com>, return joe@example.com
"""
return map(lambda x: rfc822.parseaddr(x)[1], self.to + self.bcc)
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)
try:
conn = SMTPConnection()
msg = SmarterEmailMessage(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")
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=questionset)
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), mimetype="text/plain")
return "\n".join(outlog)

View File

@ -0,0 +1,389 @@
- 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: {
checks: '',
heading: 001_TestQS,
questionnaire: 1,
sortid: 1,
text_de: Test Fragebogenseite 1,
text_en: Test QuestionSet 1,
text_fr: null
}
model: questionnaire.questionset
pk: 1
- fields: {
checks: '',
heading: 002_TestQS,
questionnaire: 1,
sortid: 2,
text_de: Test Fragebogenseite 1,
text_en: Test QuestionSet 1,
text_fr: null
}
model: questionnaire.questionset
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: 1,
random: 'test:test',
runid: 'test:test',
state: '',
subject: 1
}
model: questionnaire.runinfo
pk: 1
- fields: {
completed: 2009-05-16,
runid: 'test:test',
subject: 1
}
model: questionnaire.runinfohistory
pk: 1
- fields: {
checks: '',
extra_de: '',
extra_en: '',
extra_fr: '',
number: '1',
questionset: 1,
text_de: '[de] Question 1',
text_en: '[en] Question 1',
text_fr: '[fr] Question 1',
type: open
}
model: questionnaire.question
pk: 1
- fields: {
checks: '',
extra_de: '',
extra_en: '',
extra_fr: '',
number: '2',
questionset: 1,
text_de: '[de] Question 2',
text_en: '[en] Question 2',
text_fr: '[fr] Question 2',
type: open-textfield
}
model: questionnaire.question
pk: 2
- fields: {
checks: '',
extra_de: '',
extra_en: '',
extra_fr: '',
number: '3',
questionset: 1,
text_de: '[de] Question 3',
text_en: '[en] Question 3',
text_fr: '[fr] Question 3',
type: choice-yesno
}
model: questionnaire.question
pk: 3
- fields: {
checks: '',
extra_de: '',
extra_en: '',
extra_fr: '',
number: '4',
questionset: 1,
text_de: '[de] Question 4',
text_en: '[en] Question 4',
text_fr: '[fr] Question 4',
type: choice-yesnodontknow
}
model: questionnaire.question
pk: 4
- fields: {
checks: required-yes,
extra_de: '',
extra_en: '',
extra_fr: '',
number: '5',
questionset: 1,
text_de: '[de] Question 5',
text_en: '[en] Question 5',
text_fr: '[fr] Question 5',
type: choice-yesnocomment
}
model: questionnaire.question
pk: 5
- fields: {
checks: required-no,
extra_de: '',
extra_en: '',
extra_fr: '',
number: '6',
questionset: 1,
text_de: '[de] Question 6',
text_en: '[en] Question 6',
text_fr: '[fr] Question 6',
type: choice-yesnocomment
}
model: questionnaire.question
pk: 6
- fields: {
checks: 'range=5-10 requiredif=3,yes',
extra_de: '',
extra_en: '',
extra_fr: '',
number: '7',
questionset: 1,
text_de: '[de] Question 7, requiredif 3,yes',
text_en: '[en] Question 7, requiredif 3,yes',
text_fr: '[fr] Question 7, requiredif 3,yes',
type: range
}
model: questionnaire.question
pk: 7
- fields: {
checks: 'units=day,week,month,year',
extra_de: '',
extra_en: '',
extra_fr: '',
number: '8',
questionset: 1,
text_de: '[de] Question 8',
text_en: '[en] Question 8',
text_fr: '[fr] Question 8',
type: timeperiod
}
model: questionnaire.question
pk: 8
- fields: {
checks: '',
extra_de: '',
extra_en: '',
extra_fr: '',
number: '9',
questionset: 2,
text_de: '[de] Question 9',
text_en: '[en] Question 9',
text_fr: '[fr] Question 9',
type: choice
}
model: questionnaire.question
pk: 9
- fields: {
checks: '',
extra_de: '',
extra_en: '',
extra_fr: '',
number: '10',
questionset: 2,
text_de: '[de] Question 10',
text_en: '[en] Question 10',
text_fr: '[fr] Question 10',
type: choice-freeform
}
model: questionnaire.question
pk: 10
- fields: {
checks: '',
extra_de: '',
extra_en: '',
extra_fr: '',
number: '11',
questionset: 2,
text_de: '[de] Question 11',
text_en: '[en] Question 11',
text_fr: '[fr] Question 11',
type: choice-multiple
}
model: questionnaire.question
pk: 11
- fields: {
checks: '',
extra_de: '',
extra_en: '',
extra_fr: '',
number: '12',
questionset: 2,
text_de: '[de] Question 12',
text_en: '[en] Question 12',
text_fr: '[fr] Question 12',
type: choice-multiple-freeform
}
model: questionnaire.question
pk: 12
- fields: {
question: 9,
sortid: 1,
text_de: Option 1,
text_en: Choice 1,
text_fr: null,
value: q9_choice1
}
model: questionnaire.choice
pk: 1
- fields: {
question: 9,
sortid: 2,
text_de: Option 2,
text_en: Choice 2,
text_fr: null,
value: q9_choice2
}
model: questionnaire.choice
pk: 2
- fields: {
question: 9,
sortid: 3,
text_de: Option 3,
text_en: Choice 3,
text_fr: null,
value: q9_choice3
}
model: questionnaire.choice
pk: 3
- fields: {
question: 9,
sortid: 4,
text_de: Option 4,
text_en: Choice 4,
text_fr: null,
value: q9_choice4
}
model: questionnaire.choice
pk: 4
- fields: {
question: 10,
sortid: 1,
text_de: Option 1,
text_en: Choice 1,
text_fr: null,
value: q10_choice1
}
model: questionnaire.choice
pk: 5
- fields: {
question: 10,
sortid: 2,
text_de: Option 2,
text_en: Choice 2,
text_fr: null,
value: q10_choice2
}
model: questionnaire.choice
pk: 6
- fields: {
question: 10,
sortid: 3,
text_de: Option 3,
text_en: Choice 3,
text_fr: null,
value: q10_choice3
}
model: questionnaire.choice
pk: 7
- fields: {
question: 10,
sortid: 4,
text_de: Option 4,
text_en: Choice 4,
text_fr: null,
value: q10_choice4
}
model: questionnaire.choice
pk: 8
- fields: {
question: 11,
sortid: 1,
text_de: Option 1,
text_en: Choice 1,
text_fr: null,
value: q11_choice1
}
model: questionnaire.choice
pk: 9
- fields: {
question: 11,
sortid: 2,
text_de: Option 2,
text_en: Choice 2,
text_fr: null,
value: q11_choice2
}
model: questionnaire.choice
pk: 10
- fields: {
question: 11,
sortid: 3,
text_de: Option 3,
text_en: Choice 3,
text_fr: null,
value: q11_choice3
}
model: questionnaire.choice
pk: 11
- fields: {
question: 11,
sortid: 4,
text_de: Option 4,
text_en: Choice 4,
text_fr: null,
value: q11_choice4
}
model: questionnaire.choice
pk: 12
- fields: {
question: 12,
sortid: 1,
text_de: Option 1,
text_en: Choice 1,
text_fr: null,
value: q12_choice1
}
model: questionnaire.choice
pk: 13
- fields: {
question: 12,
sortid: 2,
text_de: Option 2,
text_en: Choice 2,
text_fr: null,
value: q12_choice2
}
model: questionnaire.choice
pk: 14
- fields: {
question: 12,
sortid: 3,
text_de: Option 3,
text_en: Choice 3,
text_fr: null,
value: q12_choice3
}
model: questionnaire.choice
pk: 15
- fields: {
question: 12,
sortid: 4,
text_de: Option 4,
text_en: Choice 4,
text_fr: null,
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,134 @@
# 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-05-15 16:41+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:18
msgid "Disqualified"
msgstr "Disqualifiziert"
#: models.py:19
msgid "Discontinued"
msgstr "Abgebrochen"
#: models.py:25
msgid "Unset"
msgstr "Keine Angabe"
#: models.py:26
msgid "Male"
msgstr "Männlich"
#: models.py:27
msgid "Female"
msgstr "Weiblich"
#: models.py:32
msgid "Subject receives emails"
msgstr "Person bekommt Emails"
#: models.py:33
msgid "Subject is sent paper form"
msgstr "Person bekommt Papierformular"
#: qprocessors/choice.py:32 qprocessors/simple.py:61
msgid "You must select an option"
msgstr "Sie müssen eine Option auswählen"
#: qprocessors/choice.py:36 qprocessors/simple.py:65 qprocessors/simple.py:67
#: qprocessors/simple.py:69 qprocessors/simple.py:72
#: qprocessors/timeperiod.py:47
msgid "Field cannot be blank"
msgstr "Dieses Feld muss ausgefüllt werden"
#: qprocessors/choice.py:40
msgid "Invalid option!"
msgstr "Ungültige Angabe"
#: qprocessors/choice.py:101
#, 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 eine 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:33 qprocessors/timeperiod.py:49
msgid "Invalid time period"
msgstr "Ungültiger Zeitraum"
#: qprocessors/timeperiod.py:39
msgid "Time period must be a whole number"
msgstr "Angabe muss aus einer ganzen Zahl bestehen"
#: templates/questionnaire/choice-yesnocomment.html:20
msgid "Yes"
msgstr "Ja"
#: templates/questionnaire/choice-yesnocomment.html:22
msgid "No"
msgstr "Nein"
#: templates/questionnaire/choice-yesnocomment.html:25
msgid "Don't Know"
msgstr "Weiss nicht"
#: templates/questionnaire/questionset.html:72
msgid "Continue"
msgstr "Weiter"

View File

@ -0,0 +1,136 @@
# 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-05-15 16:41+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:18
msgid "Disqualified"
msgstr ""
#: models.py:19
msgid "Discontinued"
msgstr ""
#: models.py:25
msgid "Unset"
msgstr ""
#: models.py:26
msgid "Male"
msgstr ""
#: models.py:27
msgid "Female"
msgstr ""
#: models.py:32
msgid "Subject receives emails"
msgstr ""
#: models.py:33
msgid "Subject is sent paper form"
msgstr ""
#: qprocessors/choice.py:32 qprocessors/simple.py:61
msgid "You must select an option"
msgstr "Veuillez choisir une option!"
#: qprocessors/choice.py:36 qprocessors/simple.py:65 qprocessors/simple.py:67
#: qprocessors/simple.py:69 qprocessors/simple.py:72
#: qprocessors/timeperiod.py:47
msgid "Field cannot be blank"
msgstr ""
"Merci de donner une réponse à cette question; ce champs ne peut rester vide."
#: qprocessors/choice.py:40
#, fuzzy
msgid "Invalid option!"
msgstr "Option non valable"
#: qprocessors/choice.py:101
#, 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 une 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:33 qprocessors/timeperiod.py:49
msgid "Invalid time period"
msgstr "Période non valable"
#: qprocessors/timeperiod.py:39
msgid "Time period must be a whole number"
msgstr "La période doit être un chiffre entier"
#: templates/questionnaire/choice-yesnocomment.html:20
msgid "Yes"
msgstr "Oui"
#: templates/questionnaire/choice-yesnocomment.html:22
msgid "No"
msgstr "Non"
#: templates/questionnaire/choice-yesnocomment.html:25
msgid "Don't Know"
msgstr "Ne sais pas"
#: templates/questionnaire/questionset.html:72
msgid "Continue"
msgstr "Continuer"

View File

View File

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

View File

@ -0,0 +1,26 @@
/* -----------------------------------------------------------------------
Blueprint CSS Framework 0.8
http://blueprintcss.org
* Copyright (c) 2007-Present. See LICENSE for more info.
* See README for instructions on how to use Blueprint.
* For credits and origins, see AUTHORS.
* This is a compressed file. See the sources in the 'src' directory.
----------------------------------------------------------------------- */
/* ie.css */
body {text-align:center;}
.container {text-align:left;}
* html .column, * html div.span-1, * html div.span-2, * html div.span-3, * html div.span-4, * html div.span-5, * html div.span-6, * html div.span-7, * html div.span-8, * html div.span-9, * html div.span-10, * html div.span-11, * html div.span-12, * html div.span-13, * html div.span-14, * html div.span-15, * html div.span-16, * html div.span-17, * html div.span-18, * html div.span-19, * html div.span-20, * html div.span-21, * html div.span-22, * html div.span-23, * html div.span-24 {overflow-x:hidden;}
* html legend {margin:0px -8px 16px 0;padding:0;}
ol {margin-left:2em;}
sup {vertical-align:text-top;}
sub {vertical-align:text-bottom;}
html>body p code {*white-space:normal;}
hr {margin:-8px auto 11px;}
.clearfix, .container {display:inline-block;}
* html .clearfix, * html .container {height:1%;}
fieldset {padding-top:0;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 655 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 537 B

View File

@ -0,0 +1,32 @@
Buttons
* Gives you great looking CSS buttons, for both <a> and <button>.
* Demo: particletree.com/features/rediscovering-the-button-element
Credits
----------------------------------------------------------------
* Created by Kevin Hale [particletree.com]
* Adapted for Blueprint by Olav Bjorkoy [bjorkoy.com]
Usage
----------------------------------------------------------------
1) Add this plugin to lib/settings.yml.
See compress.rb for instructions.
2) Use the following HTML code to place the buttons on your site:
<button type="submit" class="button positive">
<img src="css/blueprint/plugins/buttons/icons/tick.png" alt=""/> Save
</button>
<a class="button" href="/password/reset/">
<img src="css/blueprint/plugins/buttons/icons/key.png" alt=""/> Change Password
</a>
<a href="#" class="button negative">
<img src="css/blueprint/plugins/buttons/icons/cross.png" alt=""/> Cancel
</a>

View File

@ -0,0 +1,97 @@
/* --------------------------------------------------------------
buttons.css
* Gives you some great CSS-only buttons.
Created by Kevin Hale [particletree.com]
* particletree.com/features/rediscovering-the-button-element
See Readme.txt in this folder for instructions.
-------------------------------------------------------------- */
a.button, button {
display:block;
float:left;
margin: 0.7em 0.5em 0.7em 0;
padding:5px 10px 5px 7px; /* Links */
border:1px solid #dedede;
border-top:1px solid #eee;
border-left:1px solid #eee;
background-color:#f5f5f5;
font-family:"Lucida Grande", Tahoma, Arial, Verdana, sans-serif;
font-size:100%;
line-height:130%;
text-decoration:none;
font-weight:bold;
color:#565656;
cursor:pointer;
}
button {
width:auto;
overflow:visible;
padding:4px 10px 3px 7px; /* IE6 */
}
button[type] {
padding:4px 10px 4px 7px; /* Firefox */
line-height:17px; /* Safari */
}
*:first-child+html button[type] {
padding:4px 10px 3px 7px; /* IE7 */
}
button img, a.button img{
margin:0 3px -3px 0 !important;
padding:0;
border:none;
width:16px;
height:16px;
float:none;
}
/* Button colors
-------------------------------------------------------------- */
/* Standard */
button:hover, a.button:hover{
background-color:#dff4ff;
border:1px solid #c2e1ef;
color:#336699;
}
a.button:active{
background-color:#6299c5;
border:1px solid #6299c5;
color:#fff;
}
/* Positive */
body .positive {
color:#529214;
}
a.positive:hover, button.positive:hover {
background-color:#E6EFC2;
border:1px solid #C6D880;
color:#529214;
}
a.positive:active {
background-color:#529214;
border:1px solid #529214;
color:#fff;
}
/* Negative */
body .negative {
color:#d12f19;
}
a.negative:hover, button.negative:hover {
background-color:#fbe3e4;
border:1px solid #fbc2c4;
color:#d12f19;
}
a.negative:active {
background-color:#d12f19;
border:1px solid #d12f19;
color:#fff;
}

View File

@ -0,0 +1,14 @@
Fancy Type
* Gives you classes to use if you'd like some
extra fancy typography.
Credits and instructions are specified above each class
in the fancy-type.css file in this directory.
Usage
----------------------------------------------------------------
1) Add this plugin to lib/settings.yml.
See compress.rb for instructions.

View File

@ -0,0 +1,71 @@
/* --------------------------------------------------------------
fancy-type.css
* Lots of pretty advanced classes for manipulating text.
See the Readme file in this folder for additional instructions.
-------------------------------------------------------------- */
/* Indentation instead of line shifts for sibling paragraphs. */
p + p { text-indent:2em; margin-top:-1.5em; }
form p + p { text-indent: 0; } /* Don't want this in forms. */
/* For great looking type, use this code instead of asdf:
<span class="alt">asdf</span>
Best used on prepositions and ampersands. */
.alt {
color: #666;
font-family: "Warnock Pro", "Goudy Old Style","Palatino","Book Antiqua", Georgia, serif;
font-style: italic;
font-weight: normal;
}
/* For great looking quote marks in titles, replace "asdf" with:
<span class="dquo">&#8220;</span>asdf&#8221;
(That is, when the title starts with a quote mark).
(You may have to change this value depending on your font size). */
.dquo { margin-left: -.5em; }
/* Reduced size type with incremental leading
(http://www.markboulton.co.uk/journal/comments/incremental_leading/)
This could be used for side notes. For smaller type, you don't necessarily want to
follow the 1.5x vertical rhythm -- the line-height is too much.
Using this class, it reduces your font size and line-height so that for
every four lines of normal sized type, there is five lines of the sidenote. eg:
New type size in em's:
10px (wanted side note size) / 12px (existing base size) = 0.8333 (new type size in ems)
New line-height value:
12px x 1.5 = 18px (old line-height)
18px x 4 = 72px
72px / 5 = 14.4px (new line height)
14.4px / 10px = 1.44 (new line height in em's) */
p.incr, .incr p {
font-size: 10px;
line-height: 1.44em;
margin-bottom: 1.5em;
}
/* Surround uppercase words and abbreviations with this class.
Based on work by Jørgen Arnor Gårdsø Lom [http://twistedintellect.com/] */
.caps {
font-variant: small-caps;
letter-spacing: 1px;
text-transform: lowercase;
font-size:1.2em;
line-height:1%;
font-weight:bold;
padding:0 2px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 777 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 641 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 691 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 741 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 591 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 B

View File

@ -0,0 +1,18 @@
Link Icons
* Icons for links based on protocol or file type.
This is not supported in IE versions < 7.
Credits
----------------------------------------------------------------
* Marc Morgan
* Olav Bjorkoy [bjorkoy.com]
Usage
----------------------------------------------------------------
1) Add this line to your HTML:
<link rel="stylesheet" href="css/blueprint/plugins/link-icons/screen.css" type="text/css" media="screen, projection">

View File

@ -0,0 +1,40 @@
/* --------------------------------------------------------------
link-icons.css
* Icons for links based on protocol or file type.
See the Readme file in this folder for additional instructions.
-------------------------------------------------------------- */
/* Use this class if a link gets an icon when it shouldn't. */
body a.noicon {
background:transparent none !important;
padding:0 !important;
margin:0 !important;
}
/* Make sure the icons are not cut */
a[href^="http:"], a[href^="mailto:"], a[href^="http:"]:visited,
a[href$=".pdf"], a[href$=".doc"], a[href$=".xls"], a[href$=".rss"],
a[href$=".rdf"], a[href^="aim:"] {
padding:2px 22px 2px 0;
margin:-2px 0;
background-repeat: no-repeat;
background-position: right center;
}
/* External links */
a[href^="http:"] { background-image: url(icons/external.png); }
a[href^="mailto:"] { background-image: url(icons/email.png); }
a[href^="http:"]:visited { background-image: url(icons/visited.png); }
/* Files */
a[href$=".pdf"] { background-image: url(icons/pdf.png); }
a[href$=".doc"] { background-image: url(icons/doc.png); }
a[href$=".xls"] { background-image: url(icons/xls.png); }
/* Misc */
a[href$=".rss"],
a[href$=".rdf"] { background-image: url(icons/feed.png); }
a[href^="aim:"] { background-image: url(icons/im.png); }

View File

@ -0,0 +1,10 @@
RTL
* Mirrors Blueprint, so it can be used with Right-to-Left languages.
By Ran Yaniv Hartstein, ranh.co.il
Usage
----------------------------------------------------------------
1) Add this line to your HTML:
<link rel="stylesheet" href="css/blueprint/plugins/rtl/screen.css" type="text/css" media="screen, projection">

View File

@ -0,0 +1,109 @@
/* --------------------------------------------------------------
rtl.css
* Mirrors Blueprint for left-to-right languages
By Ran Yaniv Hartstein [ranh.co.il]
-------------------------------------------------------------- */
body .container { direction: rtl; }
body .column {
float: right;
margin-right: 0;
margin-left: 10px;
}
body div.last { margin-left: 0; }
body table .last { padding-left: 0; }
body .append-1 { padding-right: 0; padding-left: 40px; }
body .append-2 { padding-right: 0; padding-left: 80px; }
body .append-3 { padding-right: 0; padding-left: 120px; }
body .append-4 { padding-right: 0; padding-left: 160px; }
body .append-5 { padding-right: 0; padding-left: 200px; }
body .append-6 { padding-right: 0; padding-left: 240px; }
body .append-7 { padding-right: 0; padding-left: 280px; }
body .append-8 { padding-right: 0; padding-left: 320px; }
body .append-9 { padding-right: 0; padding-left: 360px; }
body .append-10 { padding-right: 0; padding-left: 400px; }
body .append-11 { padding-right: 0; padding-left: 440px; }
body .append-12 { padding-right: 0; padding-left: 480px; }
body .append-13 { padding-right: 0; padding-left: 520px; }
body .append-14 { padding-right: 0; padding-left: 560px; }
body .append-15 { padding-right: 0; padding-left: 600px; }
body .append-16 { padding-right: 0; padding-left: 640px; }
body .append-17 { padding-right: 0; padding-left: 680px; }
body .append-18 { padding-right: 0; padding-left: 720px; }
body .append-19 { padding-right: 0; padding-left: 760px; }
body .append-20 { padding-right: 0; padding-left: 800px; }
body .append-21 { padding-right: 0; padding-left: 840px; }
body .append-22 { padding-right: 0; padding-left: 880px; }
body .append-23 { padding-right: 0; padding-left: 920px; }
body .prepend-1 { padding-left: 0; padding-right: 40px; }
body .prepend-2 { padding-left: 0; padding-right: 80px; }
body .prepend-3 { padding-left: 0; padding-right: 120px; }
body .prepend-4 { padding-left: 0; padding-right: 160px; }
body .prepend-5 { padding-left: 0; padding-right: 200px; }
body .prepend-6 { padding-left: 0; padding-right: 240px; }
body .prepend-7 { padding-left: 0; padding-right: 280px; }
body .prepend-8 { padding-left: 0; padding-right: 320px; }
body .prepend-9 { padding-left: 0; padding-right: 360px; }
body .prepend-10 { padding-left: 0; padding-right: 400px; }
body .prepend-11 { padding-left: 0; padding-right: 440px; }
body .prepend-12 { padding-left: 0; padding-right: 480px; }
body .prepend-13 { padding-left: 0; padding-right: 520px; }
body .prepend-14 { padding-left: 0; padding-right: 560px; }
body .prepend-15 { padding-left: 0; padding-right: 600px; }
body .prepend-16 { padding-left: 0; padding-right: 640px; }
body .prepend-17 { padding-left: 0; padding-right: 680px; }
body .prepend-18 { padding-left: 0; padding-right: 720px; }
body .prepend-19 { padding-left: 0; padding-right: 760px; }
body .prepend-20 { padding-left: 0; padding-right: 800px; }
body .prepend-21 { padding-left: 0; padding-right: 840px; }
body .prepend-22 { padding-left: 0; padding-right: 880px; }
body .prepend-23 { padding-left: 0; padding-right: 920px; }
body .border {
padding-right: 0;
padding-left: 4px;
margin-right: 0;
margin-left: 5px;
border-right: none;
border-left: 1px solid #eee;
}
body .colborder {
padding-right: 0;
padding-left: 24px;
margin-right: 0;
margin-left: 25px;
border-right: none;
border-left: 1px solid #eee;
}
body .pull-1 { margin-left: 0; margin-right: -40px; }
body .pull-2 { margin-left: 0; margin-right: -80px; }
body .pull-3 { margin-left: 0; margin-right: -120px; }
body .pull-4 { margin-left: 0; margin-right: -160px; }
body .push-0 { margin: 0 18px 0 0; }
body .push-1 { margin: 0 18px 0 -40px; }
body .push-2 { margin: 0 18px 0 -80px; }
body .push-3 { margin: 0 18px 0 -120px; }
body .push-4 { margin: 0 18px 0 -160px; }
body .push-0, body .push-1, body .push-2,
body .push-3, body .push-4 { float: left; }
/* Typography with RTL support */
body h1,body h2,body h3,
body h4,body h5,body h6 { font-family: Arial, sans-serif; }
html body { font-family: Arial, sans-serif; }
body pre,body code,body tt { font-family: monospace; }
/* Mirror floats and margins on typographic elements */
body p img { float: right; margin: 1.5em 0 1.5em 1.5em; }
body dd, body ul, body ol { margin-left: 0; margin-right: 1.5em;}
body td, body th { text-align:right; }

View File

@ -0,0 +1,30 @@
/* -----------------------------------------------------------------------
Blueprint CSS Framework 0.8
http://blueprintcss.org
* Copyright (c) 2007-Present. See LICENSE for more info.
* See README for instructions on how to use Blueprint.
* For credits and origins, see AUTHORS.
* This is a compressed file. See the sources in the 'src' directory.
----------------------------------------------------------------------- */
/* print.css */
body {line-height:1.5;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;color:#000;background:none;font-size:10pt;}
.container {background:none;}
hr {background:#ccc;color:#ccc;width:100%;height:2px;margin:2em 0;padding:0;border:none;}
hr.space {background:#fff;color:#fff;}
h1, h2, h3, h4, h5, h6 {font-family:"Helvetica Neue", Arial, "Lucida Grande", sans-serif;}
code {font:.9em "Courier New", Monaco, Courier, monospace;}
img {float:left;margin:1.5em 1.5em 1.5em 0;}
a img {border:none;}
p img.top {margin-top:0;}
blockquote {margin:1.5em;padding:1em;font-style:italic;font-size:.9em;}
.small {font-size:.9em;}
.large {font-size:1.1em;}
.quiet {color:#999;}
.hide {display:none;}
a:link, a:visited {background:transparent;font-weight:700;text-decoration:underline;}
a:link:after, a:visited:after {content:" (" attr(href) ")";font-size:90%;}

View File

@ -0,0 +1,250 @@
/* -----------------------------------------------------------------------
Blueprint CSS Framework 0.8
http://blueprintcss.org
* Copyright (c) 2007-Present. See LICENSE for more info.
* See README for instructions on how to use Blueprint.
* For credits and origins, see AUTHORS.
* This is a compressed file. See the sources in the 'src' directory.
----------------------------------------------------------------------- */
/* reset.css */
html, body, div, span, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, code, del, dfn, em, img, q, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td {margin:0;padding:0;border:0;font-weight:inherit;font-style:inherit;font-size:100%;font-family:inherit;vertical-align:baseline;}
body {line-height:1.5;}
table {border-collapse:separate;border-spacing:0;}
caption, th, td {text-align:left;font-weight:normal;}
table, td, th {vertical-align:middle;}
blockquote:before, blockquote:after, q:before, q:after {content:"";}
blockquote, q {quotes:"" "";}
a img {border:none;}
/* typography.css */
body {font-size:75%;color:#222;background:#fff;font-family:"Helvetica Neue", Arial, Helvetica, sans-serif;}
h1, h2, h3, h4, h5, h6 {font-weight:normal;color:#111;}
h1 {font-size:3em;line-height:1;margin-bottom:0.5em;}
h2 {font-size:2em;margin-bottom:0.75em;}
h3 {font-size:1.5em;line-height:1;margin-bottom:1em;}
h4 {font-size:1.2em;line-height:1.25;margin-bottom:1.25em;}
h5 {font-size:1em;font-weight:bold;margin-bottom:1.5em;}
h6 {font-size:1em;font-weight:bold;}
h1 img, h2 img, h3 img, h4 img, h5 img, h6 img {margin:0;}
p {margin:0 0 1.5em;}
p img.left {float:left;margin:1.5em 1.5em 1.5em 0;padding:0;}
p img.right {float:right;margin:1.5em 0 1.5em 1.5em;}
a:focus, a:hover {color:#000;}
a {color:#009;text-decoration:underline;}
blockquote {margin:1.5em;color:#666;font-style:italic;}
strong {font-weight:bold;}
em, dfn {font-style:italic;}
dfn {font-weight:bold;}
sup, sub {line-height:0;}
abbr, acronym {border-bottom:1px dotted #666;}
address {margin:0 0 1.5em;font-style:italic;}
del {color:#666;}
pre {margin:1.5em 0;white-space:pre;}
pre, code, tt {font:1em 'andale mono', 'lucida console', monospace;line-height:1.5;}
li ul, li ol {margin:0 1.5em;}
ul, ol {margin:0 1.5em 1.5em 1.5em;}
ul {list-style-type:disc;}
ol {list-style-type:decimal;}
dl {margin:0 0 1.5em 0;}
dl dt {font-weight:bold;}
dd {margin-left:1.5em;}
table {margin-bottom:1.4em;width:100%;}
th {font-weight:bold;}
thead th {background:#c3d9ff;}
th, td, caption {padding:4px 10px 4px 5px;}
tr.even td {background:#e5ecf9;}
tfoot {font-style:italic;}
caption {background:#eee;}
.small {font-size:.8em;margin-bottom:1.875em;line-height:1.875em;}
.large {font-size:1.2em;line-height:2.5em;margin-bottom:1.25em;}
.hide {display:none;}
.quiet {color:#666;}
.loud {color:#000;}
.highlight {background:#ff0;}
.added {background:#060;color:#fff;}
.removed {background:#900;color:#fff;}
.first {margin-left:0;padding-left:0;}
.last {margin-right:0;padding-right:0;}
.top {margin-top:0;padding-top:0;}
.bottom {margin-bottom:0;padding-bottom:0;}
/* grid.css */
.showgrid {background:url(src/grid.png);}
.column, div.span-1, div.span-2, div.span-3, div.span-4, div.span-5, div.span-6, div.span-7, div.span-8, div.span-9, div.span-10, div.span-11, div.span-12, div.span-13, div.span-14, div.span-15, div.span-16, div.span-17, div.span-18, div.span-19, div.span-20, div.span-21, div.span-22, div.span-23, div.span-24 {float:left;margin-right:10px;}
.last, div.last {margin-right:0;}
.span-1 {width:30px;}
.span-2 {width:70px;}
.span-3 {width:110px;}
.span-4 {width:150px;}
.span-5 {width:190px;}
.span-6 {width:230px;}
.span-7 {width:270px;}
.span-8 {width:310px;}
.span-9 {width:350px;}
.span-10 {width:390px;}
.span-11 {width:430px;}
.span-12 {width:470px;}
.span-13 {width:510px;}
.span-14 {width:550px;}
.span-15 {width:590px;}
.span-16 {width:630px;}
.span-17 {width:670px;}
.span-18 {width:710px;}
.span-19 {width:750px;}
.span-20 {width:790px;}
.span-21 {width:830px;}
.span-22 {width:870px;}
.span-23 {width:910px;}
.span-24, div.span-24 {width:950px;margin:0;}
input.span-1, textarea.span-1, select.span-1 {width:30px!important;}
input.span-2, textarea.span-2, select.span-2 {width:50px!important;}
input.span-3, textarea.span-3, select.span-3 {width:90px!important;}
input.span-4, textarea.span-4, select.span-4 {width:130px!important;}
input.span-5, textarea.span-5, select.span-5 {width:170px!important;}
input.span-6, textarea.span-6, select.span-6 {width:210px!important;}
input.span-7, textarea.span-7, select.span-7 {width:250px!important;}
input.span-8, textarea.span-8, select.span-8 {width:290px!important;}
input.span-9, textarea.span-9, select.span-9 {width:330px!important;}
input.span-10, textarea.span-10, select.span-10 {width:370px!important;}
input.span-11, textarea.span-11, select.span-11 {width:410px!important;}
input.span-12, textarea.span-12, select.span-12 {width:450px!important;}
input.span-13, textarea.span-13, select.span-13 {width:490px!important;}
input.span-14, textarea.span-14, select.span-14 {width:530px!important;}
input.span-15, textarea.span-15, select.span-15 {width:570px!important;}
input.span-16, textarea.span-16, select.span-16 {width:610px!important;}
input.span-17, textarea.span-17, select.span-17 {width:650px!important;}
input.span-18, textarea.span-18, select.span-18 {width:690px!important;}
input.span-19, textarea.span-19, select.span-19 {width:730px!important;}
input.span-20, textarea.span-20, select.span-20 {width:770px!important;}
input.span-21, textarea.span-21, select.span-21 {width:810px!important;}
input.span-22, textarea.span-22, select.span-22 {width:850px!important;}
input.span-23, textarea.span-23, select.span-23 {width:890px!important;}
input.span-24, textarea.span-24, select.span-24 {width:940px!important;}
.append-1 {padding-right:40px;}
.append-2 {padding-right:80px;}
.append-3 {padding-right:120px;}
.append-4 {padding-right:160px;}
.append-5 {padding-right:200px;}
.append-6 {padding-right:240px;}
.append-7 {padding-right:280px;}
.append-8 {padding-right:320px;}
.append-9 {padding-right:360px;}
.append-10 {padding-right:400px;}
.append-11 {padding-right:440px;}
.append-12 {padding-right:480px;}
.append-13 {padding-right:520px;}
.append-14 {padding-right:560px;}
.append-15 {padding-right:600px;}
.append-16 {padding-right:640px;}
.append-17 {padding-right:680px;}
.append-18 {padding-right:720px;}
.append-19 {padding-right:760px;}
.append-20 {padding-right:800px;}
.append-21 {padding-right:840px;}
.append-22 {padding-right:880px;}
.append-23 {padding-right:920px;}
.prepend-1 {padding-left:40px;}
.prepend-2 {padding-left:80px;}
.prepend-3 {padding-left:120px;}
.prepend-4 {padding-left:160px;}
.prepend-5 {padding-left:200px;}
.prepend-6 {padding-left:240px;}
.prepend-7 {padding-left:280px;}
.prepend-8 {padding-left:320px;}
.prepend-9 {padding-left:360px;}
.prepend-10 {padding-left:400px;}
.prepend-11 {padding-left:440px;}
.prepend-12 {padding-left:480px;}
.prepend-13 {padding-left:520px;}
.prepend-14 {padding-left:560px;}
.prepend-15 {padding-left:600px;}
.prepend-16 {padding-left:640px;}
.prepend-17 {padding-left:680px;}
.prepend-18 {padding-left:720px;}
.prepend-19 {padding-left:760px;}
.prepend-20 {padding-left:800px;}
.prepend-21 {padding-left:840px;}
.prepend-22 {padding-left:880px;}
.prepend-23 {padding-left:920px;}
div.border {padding-right:4px;margin-right:5px;border-right:1px solid #eee;}
div.colborder {padding-right:24px;margin-right:25px;border-right:1px solid #eee;}
.pull-1 {margin-left:-40px;}
.pull-2 {margin-left:-80px;}
.pull-3 {margin-left:-120px;}
.pull-4 {margin-left:-160px;}
.pull-5 {margin-left:-200px;}
.pull-6 {margin-left:-240px;}
.pull-7 {margin-left:-280px;}
.pull-8 {margin-left:-320px;}
.pull-9 {margin-left:-360px;}
.pull-10 {margin-left:-400px;}
.pull-11 {margin-left:-440px;}
.pull-12 {margin-left:-480px;}
.pull-13 {margin-left:-520px;}
.pull-14 {margin-left:-560px;}
.pull-15 {margin-left:-600px;}
.pull-16 {margin-left:-640px;}
.pull-17 {margin-left:-680px;}
.pull-18 {margin-left:-720px;}
.pull-19 {margin-left:-760px;}
.pull-20 {margin-left:-800px;}
.pull-21 {margin-left:-840px;}
.pull-22 {margin-left:-880px;}
.pull-23 {margin-left:-920px;}
.pull-24 {margin-left:-960px;}
.pull-1, .pull-2, .pull-3, .pull-4, .pull-5, .pull-6, .pull-7, .pull-8, .pull-9, .pull-10, .pull-11, .pull-12, .pull-13, .pull-14, .pull-15, .pull-16, .pull-17, .pull-18, .pull-19, .pull-20, .pull-21, .pull-22, .pull-23, .pull-24 {float:left;position:relative;}
.push-1 {margin:0 -40px 1.5em 40px;}
.push-2 {margin:0 -80px 1.5em 80px;}
.push-3 {margin:0 -120px 1.5em 120px;}
.push-4 {margin:0 -160px 1.5em 160px;}
.push-5 {margin:0 -200px 1.5em 200px;}
.push-6 {margin:0 -240px 1.5em 240px;}
.push-7 {margin:0 -280px 1.5em 280px;}
.push-8 {margin:0 -320px 1.5em 320px;}
.push-9 {margin:0 -360px 1.5em 360px;}
.push-10 {margin:0 -400px 1.5em 400px;}
.push-11 {margin:0 -440px 1.5em 440px;}
.push-12 {margin:0 -480px 1.5em 480px;}
.push-13 {margin:0 -520px 1.5em 520px;}
.push-14 {margin:0 -560px 1.5em 560px;}
.push-15 {margin:0 -600px 1.5em 600px;}
.push-16 {margin:0 -640px 1.5em 640px;}
.push-17 {margin:0 -680px 1.5em 680px;}
.push-18 {margin:0 -720px 1.5em 720px;}
.push-19 {margin:0 -760px 1.5em 760px;}
.push-20 {margin:0 -800px 1.5em 800px;}
.push-21 {margin:0 -840px 1.5em 840px;}
.push-22 {margin:0 -880px 1.5em 880px;}
.push-23 {margin:0 -920px 1.5em 920px;}
.push-24 {margin:0 -960px 1.5em 960px;}
.push-1, .push-2, .push-3, .push-4, .push-5, .push-6, .push-7, .push-8, .push-9, .push-10, .push-11, .push-12, .push-13, .push-14, .push-15, .push-16, .push-17, .push-18, .push-19, .push-20, .push-21, .push-22, .push-23, .push-24 {float:right;position:relative;}
.prepend-top {margin-top:1.5em;}
.append-bottom {margin-bottom:1.5em;}
.box {padding:1.5em;margin-bottom:1.5em;background:#E5ECF9;}
hr {background:#ddd;color:#ddd;clear:both;float:none;width:100%;height:.1em;margin:0 0 1.45em;border:none;}
hr.space {background:#fff;color:#fff;}
.clearfix:after, .container:after {content:"\0020";display:block;height:0;clear:both;visibility:hidden;overflow:hidden;}
.clearfix, .container {display:block;}
.clear {clear:both;}
/* forms.css */
label {font-weight:bold;}
fieldset {padding:1.4em;margin:0 0 1.5em 0;border:1px solid #ccc;}
legend {font-weight:bold;font-size:1.2em;}
input.text, input.title, textarea, select {margin:0.5em 0;border:1px solid #bbb;}
input.text:focus, input.title:focus, textarea:focus, select:focus {border:1px solid #666;}
input.text, input.title {width:300px;padding:5px;}
input.title {font-size:1.5em;}
textarea {width:390px;height:250px;padding:5px;}
.error, .notice, .success {padding:.8em;margin-bottom:1em;border:2px solid #ddd !important;}
.error {background:#FBE3E4 !important;color:#8a1f11;border-color:#FBC2C4 !important;}
.notice {background:#FFF6BF;color:#514721;border-color:#FFD324;}
.success {background:#E6EFC2;color:#264409;border-color:#C6D880;}
.error a {color:#8a1f11;}
.notice a {color:#514721;}
.success a {color:#264409;}

View File

@ -0,0 +1,49 @@
/* --------------------------------------------------------------
forms.css
* Sets up some default styling for forms
* Gives you classes to enhance your forms
Usage:
* For text fields, use class .title or .text
-------------------------------------------------------------- */
label { font-weight: bold; }
fieldset { padding:1.4em; margin: 0 0 1.5em 0; border: 1px solid #ccc; }
legend { font-weight: bold; font-size:1.2em; }
/* Form fields
-------------------------------------------------------------- */
input.text, input.title,
textarea, select {
margin:0.5em 0;
border:1px solid #bbb;
}
input.text:focus, input.title:focus,
textarea:focus, select:focus {
border:1px solid #666;
}
input.text,
input.title { width: 300px; padding:5px; }
input.title { font-size:1.5em; }
textarea { width: 390px; height: 250px; padding:5px; }
/* Success, notice and error boxes
-------------------------------------------------------------- */
.error,
.notice,
.success { padding: .8em; margin-bottom: 1em; border: 2px solid #ddd; }
.error { background: #FBE3E4; color: #8a1f11; border-color: #FBC2C4; }
.notice { background: #FFF6BF; color: #514721; border-color: #FFD324; }
.success { background: #E6EFC2; color: #264409; border-color: #C6D880; }
.error a { color: #8a1f11; }
.notice a { color: #514721; }
.success a { color: #264409; }

View File

@ -0,0 +1,213 @@
/* --------------------------------------------------------------
grid.css
* Sets up an easy-to-use grid of 24 columns.
By default, the grid is 950px wide, with 24 columns
spanning 30px, and a 10px margin between columns.
If you need fewer or more columns, namespaces or semantic
element names, use the compressor script (lib/compress.rb)
Note: Changes made in this file will not be applied when
using the compressor: make changes in lib/blueprint/grid.css.rb
-------------------------------------------------------------- */
/* A container should group all your columns. */
.container {
width: 950px;
margin: 0 auto;
}
/* Use this class on any .span / container to see the grid. */
.showgrid { background: url(src/grid.png); }
/* Columns
-------------------------------------------------------------- */
/* Sets up basic grid floating and margin. */
.column, div.span-1, div.span-2, div.span-3, div.span-4, div.span-5,
div.span-6, div.span-7, div.span-8, div.span-9, div.span-10,
div.span-11, div.span-12, div.span-13, div.span-14, div.span-15,
div.span-16, div.span-17, div.span-18, div.span-19, div.span-20,
div.span-21, div.span-22, div.span-23, div.span-24 {
float: left;
margin-right: 10px;
}
/* The last column in a row needs this class. */
.last, div.last { margin-right: 0; }
/* Use these classes to set the width of a column. */
.span-1 { width: 30px; }
.span-2 { width: 70px; }
.span-3 { width: 110px; }
.span-4 { width: 150px; }
.span-5 { width: 190px; }
.span-6 { width: 230px; }
.span-7 { width: 270px; }
.span-8 { width: 310px; }
.span-9 { width: 350px; }
.span-10 { width: 390px; }
.span-11 { width: 430px; }
.span-12 { width: 470px; }
.span-13 { width: 510px; }
.span-14 { width: 550px; }
.span-15 { width: 590px; }
.span-16 { width: 630px; }
.span-17 { width: 670px; }
.span-18 { width: 710px; }
.span-19 { width: 750px; }
.span-20 { width: 790px; }
.span-21 { width: 830px; }
.span-22 { width: 870px; }
.span-23 { width: 910px; }
.span-24 { width: 950px; margin: 0; }
/* Add these to a column to append empty cols. */
.append-1 { padding-right: 40px; }
.append-2 { padding-right: 80px; }
.append-3 { padding-right: 120px; }
.append-4 { padding-right: 160px; }
.append-5 { padding-right: 200px; }
.append-6 { padding-right: 240px; }
.append-7 { padding-right: 280px; }
.append-8 { padding-right: 320px; }
.append-9 { padding-right: 360px; }
.append-10 { padding-right: 400px; }
.append-11 { padding-right: 440px; }
.append-12 { padding-right: 480px; }
.append-13 { padding-right: 520px; }
.append-14 { padding-right: 560px; }
.append-15 { padding-right: 600px; }
.append-16 { padding-right: 640px; }
.append-17 { padding-right: 680px; }
.append-18 { padding-right: 720px; }
.append-19 { padding-right: 760px; }
.append-20 { padding-right: 800px; }
.append-21 { padding-right: 840px; }
.append-22 { padding-right: 880px; }
.append-23 { padding-right: 920px; }
/* Add these to a column to prepend empty cols. */
.prepend-1 { padding-left: 40px; }
.prepend-2 { padding-left: 80px; }
.prepend-3 { padding-left: 120px; }
.prepend-4 { padding-left: 160px; }
.prepend-5 { padding-left: 200px; }
.prepend-6 { padding-left: 240px; }
.prepend-7 { padding-left: 280px; }
.prepend-8 { padding-left: 320px; }
.prepend-9 { padding-left: 360px; }
.prepend-10 { padding-left: 400px; }
.prepend-11 { padding-left: 440px; }
.prepend-12 { padding-left: 480px; }
.prepend-13 { padding-left: 520px; }
.prepend-14 { padding-left: 560px; }
.prepend-15 { padding-left: 600px; }
.prepend-16 { padding-left: 640px; }
.prepend-17 { padding-left: 680px; }
.prepend-18 { padding-left: 720px; }
.prepend-19 { padding-left: 760px; }
.prepend-20 { padding-left: 800px; }
.prepend-21 { padding-left: 840px; }
.prepend-22 { padding-left: 880px; }
.prepend-23 { padding-left: 920px; }
/* Border on right hand side of a column. */
div.border {
padding-right: 4px;
margin-right: 5px;
border-right: 1px solid #eee;
}
/* Border with more whitespace, spans one column. */
div.colborder {
padding-right: 24px;
margin-right: 25px;
border-right: 1px solid #eee;
}
/* Use these classes on an element to push it into the
next column, or to pull it into the previous column. */
.pull-1 { margin-left: -40px; }
.pull-2 { margin-left: -80px; }
.pull-3 { margin-left: -120px; }
.pull-4 { margin-left: -160px; }
.pull-5 { margin-left: -200px; }
.pull-1, .pull-2, .pull-3, .pull-4, .pull-5 {
float:left;
position:relative;
}
.push-1 { margin: 0 -40px 1.5em 40px; }
.push-2 { margin: 0 -80px 1.5em 80px; }
.push-3 { margin: 0 -120px 1.5em 120px; }
.push-4 { margin: 0 -160px 1.5em 160px; }
.push-5 { margin: 0 -200px 1.5em 200px; }
.push-1, .push-2, .push-3, .push-4, .push-5 {
float: right;
position:relative;
}
/* Misc classes and elements
-------------------------------------------------------------- */
/* In case you need to add a gutter above/below an element */
.prepend-top {
margin-top:1.5em;
}
.append-bottom {
margin-bottom:1.5em;
}
/* Use a .box to create a padded box inside a column. */
.box {
padding: 1.5em;
margin-bottom: 1.5em;
background: #E5ECF9;
}
/* Use this to create a horizontal ruler across a column. */
hr {
background: #ddd;
color: #ddd;
clear: both;
float: none;
width: 100%;
height: .1em;
margin: 0 0 1.45em;
border: none;
}
hr.space {
background: #fff;
color: #fff;
}
/* Clearing floats without extra markup
Based on How To Clear Floats Without Structural Markup by PiE
[http://www.positioniseverything.net/easyclearing.html] */
.clearfix:after, .container:after {
content: "\0020";
display: block;
height: 0;
clear: both;
visibility: hidden;
overflow:hidden;
}
.clearfix, .container {display: block;}
/* Regular clearing
apply to column that should drop below previous ones. */
.clear { clear:both; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 B

View File

@ -0,0 +1,59 @@
/* --------------------------------------------------------------
ie.css
Contains every hack for Internet Explorer,
so that our core files stay sweet and nimble.
-------------------------------------------------------------- */
/* Make sure the layout is centered in IE5 */
body { text-align: center; }
.container { text-align: left; }
/* Fixes IE margin bugs */
* html .column, * html div.span-1, * html div.span-2,
* html div.span-3, * html div.span-4, * html div.span-5,
* html div.span-6, * html div.span-7, * html div.span-8,
* html div.span-9, * html div.span-10, * html div.span-11,
* html div.span-12, * html div.span-13, * html div.span-14,
* html div.span-15, * html div.span-16, * html div.span-17,
* html div.span-18, * html div.span-19, * html div.span-20,
* html div.span-21, * html div.span-22, * html div.span-23,
* html div.span-24 { overflow-x: hidden; }
/* Elements
-------------------------------------------------------------- */
/* Fixes incorrect styling of legend in IE6. */
* html legend { margin:0px -8px 16px 0; padding:0; }
/* Fixes incorrect placement of ol numbers in IE6/7. */
ol { margin-left:2em; }
/* Fixes wrong line-height on sup/sub in IE. */
sup { vertical-align: text-top; }
sub { vertical-align: text-bottom; }
/* Fixes IE7 missing wrapping of code elements. */
html>body p code { *white-space: normal; }
/* IE 6&7 has problems with setting proper <hr> margins. */
hr { margin: -8px auto 11px; }
/* Clearing
-------------------------------------------------------------- */
/* Makes clearfix actually work in IE */
.clearfix, .container {display: inline-block;}
* html .clearfix,
* html .container {height: 1%;}
/* Forms
-------------------------------------------------------------- */
/* Fixes padding on fieldset */
fieldset {padding-top: 0;}

View File

@ -0,0 +1,85 @@
/* --------------------------------------------------------------
print.css
* Gives you some sensible styles for printing pages.
* See Readme file in this directory for further instructions.
Some additions you'll want to make, customized to your markup:
#header, #footer, #navigation { display:none; }
-------------------------------------------------------------- */
body {
line-height: 1.5;
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
color:#000;
background: none;
font-size: 10pt;
}
/* Layout
-------------------------------------------------------------- */
.container {
background: none;
}
hr {
background:#ccc;
color:#ccc;
width:100%;
height:2px;
margin:2em 0;
padding:0;
border:none;
}
hr.space {
background: #fff;
color: #fff;
}
/* Text
-------------------------------------------------------------- */
h1,h2,h3,h4,h5,h6 { font-family: "Helvetica Neue", Arial, "Lucida Grande", sans-serif; }
code { font:.9em "Courier New", Monaco, Courier, monospace; }
img { float:left; margin:1.5em 1.5em 1.5em 0; }
a img { border:none; }
p img.top { margin-top: 0; }
blockquote {
margin:1.5em;
padding:1em;
font-style:italic;
font-size:.9em;
}
.small { font-size: .9em; }
.large { font-size: 1.1em; }
.quiet { color: #999; }
.hide { display:none; }
/* Links
-------------------------------------------------------------- */
a:link, a:visited {
background: transparent;
font-weight:700;
text-decoration: underline;
}
a:link:after, a:visited:after {
content: " (" attr(href) ")";
font-size: 90%;
}
/* If you're having trouble printing relative links, uncomment and customize this:
(note: This is valid CSS3, but it still won't go through the W3C CSS Validator) */
/* a[href^="/"]:after {
content: " (http://www.yourdomain.com" attr(href) ") ";
} */

View File

@ -0,0 +1,38 @@
/* --------------------------------------------------------------
reset.css
* Resets default browser CSS.
-------------------------------------------------------------- */
html, body, div, span, object, iframe,
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
a, abbr, acronym, address, code,
del, dfn, em, img, q, dl, dt, dd, ol, ul, li,
fieldset, form, label, legend,
table, caption, tbody, tfoot, thead, tr, th, td {
margin: 0;
padding: 0;
border: 0;
font-weight: inherit;
font-style: inherit;
font-size: 100%;
font-family: inherit;
vertical-align: baseline;
}
body {
line-height: 1.5;
}
/* Tables still need 'cellspacing="0"' in the markup. */
table { border-collapse: separate; border-spacing: 0; }
caption, th, td { text-align: left; font-weight: normal; }
table, td, th { vertical-align: middle; }
/* Remove possible quote marks (") from <q>, <blockquote>. */
blockquote:before, blockquote:after, q:before, q:after { content: ""; }
blockquote, q { quotes: "" ""; }
/* Remove annoying border on linked images. */
a img { border: none; }

View File

@ -0,0 +1,105 @@
/* --------------------------------------------------------------
typography.css
* Sets up some sensible default typography.
-------------------------------------------------------------- */
/* Default font settings.
The font-size percentage is of 16px. (0.75 * 16px = 12px) */
body {
font-size: 75%;
color: #222;
background: #fff;
font-family: "Helvetica Neue", Arial, Helvetica, sans-serif;
}
/* Headings
-------------------------------------------------------------- */
h1,h2,h3,h4,h5,h6 { font-weight: normal; color: #111; }
h1 { font-size: 3em; line-height: 1; margin-bottom: 0.5em; }
h2 { font-size: 2em; margin-bottom: 0.75em; }
h3 { font-size: 1.5em; line-height: 1; margin-bottom: 1em; }
h4 { font-size: 1.2em; line-height: 1.25; margin-bottom: 1.25em; }
h5 { font-size: 1em; font-weight: bold; margin-bottom: 1.5em; }
h6 { font-size: 1em; font-weight: bold; }
h1 img, h2 img, h3 img,
h4 img, h5 img, h6 img {
margin: 0;
}
/* Text elements
-------------------------------------------------------------- */
p { margin: 0 0 1.5em; }
p img.left { float: left; margin: 1.5em 1.5em 1.5em 0; padding: 0; }
p img.right { float: right; margin: 1.5em 0 1.5em 1.5em; }
a:focus,
a:hover { color: #000; }
a { color: #009; text-decoration: underline; }
blockquote { margin: 1.5em; color: #666; font-style: italic; }
strong { font-weight: bold; }
em,dfn { font-style: italic; }
dfn { font-weight: bold; }
sup, sub { line-height: 0; }
abbr,
acronym { border-bottom: 1px dotted #666; }
address { margin: 0 0 1.5em; font-style: italic; }
del { color:#666; }
pre { margin: 1.5em 0; white-space: pre; }
pre,code,tt { font: 1em 'andale mono', 'lucida console', monospace; line-height: 1.5; }
/* Lists
-------------------------------------------------------------- */
li ul,
li ol { margin:0 1.5em; }
ul, ol { margin: 0 1.5em 1.5em 1.5em; }
ul { list-style-type: disc; }
ol { list-style-type: decimal; }
dl { margin: 0 0 1.5em 0; }
dl dt { font-weight: bold; }
dd { margin-left: 1.5em;}
/* Tables
-------------------------------------------------------------- */
table { margin-bottom: 1.4em; width:100%; }
th { font-weight: bold; }
thead th { background: #c3d9ff; }
th,td,caption { padding: 4px 10px 4px 5px; }
tr.even td { background: #e5ecf9; }
tfoot { font-style: italic; }
caption { background: #eee; }
/* Misc classes
-------------------------------------------------------------- */
.small { font-size: .8em; margin-bottom: 1.875em; line-height: 1.875em; }
.large { font-size: 1.2em; line-height: 2.5em; margin-bottom: 1.25em; }
.hide { display: none; }
.quiet { color: #666; }
.loud { color: #000; }
.highlight { background:#ff0; }
.added { background:#060; color: #fff; }
.removed { background:#900; color: #fff; }
.first { margin-left:0; padding-left:0; }
.last { margin-right:0; padding-right:0; }
.top { margin-top:0; padding-top:0; }
.bottom { margin-bottom:0; padding-bottom:0; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1023 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 540 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 530 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 543 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

View File

@ -0,0 +1,67 @@
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) == "!") {
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;
}
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;
}
//obj.style.backgroundColor = !res ? "#eee" : "#fff";
obj.disabled = !res;
}
function valchanged(qnum, value) {
qvalues[qnum] = value;
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;
}

319
questionnaire/models.py Normal file
View File

@ -0,0 +1,319 @@
from django.db import models
from transmeta import TransMeta
from django.utils.translation import ugettext as _
from questionnaire import QuestionChoices
import re
from utils import split_numal
from django.utils import simplejson as json
from parsers import parse_checks, ParseException
from django.conf import settings
_numre = re.compile("(\d+)([a-z]+)", re.I)
class Subject(models.Model):
state = models.CharField(max_length=16, default="inactive", choices = (
("active", _("Active")),
("inactive", _("Inactive")),
("disqualified", _("Disqualified")),
("discontinued", _("Discontinued")),
))
surname = models.CharField(max_length=64, blank=True, null=True)
givenname = models.CharField(max_length=64, blank=True, null=True)
email = models.EmailField(null=True, blank=True)
gender = models.CharField(max_length=8, default="unset", blank=True,
choices = ( ("unset", _("Unset")),
("male", _("Male")),
("female", _("Female")),
)
)
nextrun = models.DateField()
formtype = models.CharField(max_length=16, default='email', choices = (
("email", _("Subject receives emails")),
("paperform", _("Subject is sent paper form"),
)))
language = models.CharField(max_length=2, default=settings.LANGUAGE_CODE,
choices = settings.LANGUAGES
)
def __unicode__(self):
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 history(self):
return RunInfoHistory.objects.filter(subject=self).order_by('runid')
def pending(self):
return RunInfo.objects.filter(subject=self).order_by('runid')
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", default="/media/complete.html")
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
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=64, 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(help_text="This is interpreted as Textile: <a href='http://hobix.com/textile/quick.html'>http://hobix.com/textile/quick.html</a>")
def questions(self):
if not hasattr(self, "__qcache"):
self.__qcache = list(Question.objects.filter(questionset=self).order_by('number'))
self.__qcache.sort()
return self.__qcache
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',)
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)
# 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(auto_now=True)
lastemailerror = models.CharField(max_length=64, null=True, blank=True)
state = models.CharField(max_length=16, null=True, blank=True)
cookies = models.CharField(max_length=512, null=True, blank=True)
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, 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:
cookies[key] = str(value)
cstr = json.dumps(cookies)
if len(cstr) > 512: # XXX - hard coded to match cookie length above
raise Exception("Cannot set cookie. No more space in cookie jar!")
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, 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'
class RunInfoHistory(models.Model):
subject = models.ForeignKey(Subject)
runid = models.CharField(max_length=32)
completed = models.DateField()
def __unicode__(self):
return "%s: %s on %s" % (self.runid, self.subject, self.completed)
class Meta:
verbose_name_plural = 'Run Info History'
class Question(models.Model):
__metaclass__ = TransMeta
questionset = models.ForeignKey(QuestionSet)
number = models.CharField(max_length=8) # 1, 2a, 2b, 3c - also used for sorting
text = models.TextField(blank=True)
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=128, blank=True, null=True, help_text=u"Extra information (use on question type)")
checks = models.CharField(u"Additional checks", max_length=64, blank=True,
null=True, help_text=u"""Additional checks to be performed for this value (space separated). You may also specify an entry as key=value or key="value with spaces".<br /><br />For text fields, <tt>required</tt> is a valid check.<br />For yes/no comment, "required", <tt>required-yes</tt>, and <tt>required-no</tt> are valid.<br />For Time period, you may supply <tt>units=hour,day,month,year</tt>.<br /><br />If this question is only valid if another question's answer is something specific, use <tt>requiredif="QuestionNumber,Value"</tt>. Requiredif also takes boolean expressions using <tt>and</tt>, <tt>or</tt>, and <tt>not</tt>, as well as grouping with parenthesis. eg. <tt>requiredif="5,yes or (6,no and 1,yes)"</tt>, where the values in parenthesis are evaluated first.""")
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, e:
logging.exception("Error Parsing Checks for Question %s: %s" % (
self.number, self.sameas().checks))
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 newline(self):
# if user wants multiple breaks, they can use nobreaks, or just one newline
# after the question text, just nobreak .. a little bit confusing, maybe
checks = self.sameas().checks or ''
if "nobreak" in checks:
return False
return True
def sameas(self):
if self.type == 'sameas':
self.__sameas = res = getattr(self, "__sameas", Question.objects.filter(number=self.checks)[0])
return res
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 "<br />" + self.number
def choices(self):
if self.type == 'sameas':
return self.sameas().choices()
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"
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)
class Meta:
translate = ('text', 'extra')
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)
def __unicode__(self):
return u'(%s) %d. %s' % (self.question.number, self.sortid, self.text)
class Meta:
translate = ('text',)
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 choice_str(self, secondary = False):
choice_string = ""
choices = self.question.get_choices()
split_answers = self.answer.split()
for choice in choices:
for split_answer in split_answers:
if str(split_answer) == choice.value:
choice_string += str(choice.text) + " "
def check_answer(self):
"Confirm that the supplied answer matches what we expect"
return True

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

View File

@ -0,0 +1,9 @@
from django.conf import settings
from questionnaire import *
from django.utils.translation import ugettext as _
import simple # store value as returned
import choice # multiple choice, do checks
import range # range of numbers
import timeperiod # time periods
import custom # backwards compatibility support

View File

@ -0,0 +1,108 @@
from questionnaire import *
from django.utils.translation import ugettext as _, ungettext
@question_proc('choice', 'choice-freeform')
def question_choice(request, question):
choices = []
cd = question.getcheckdict()
key = "question_%s" % question.number
key2 = "question_%s_comment" % question.number
val = None
if key in request.POST:
val = request.POST[key]
else:
if 'default' in cd:
val = cd['default']
for choice in question.choices():
choices.append( ( choice.value == val, choice, ) )
return {
'choices' : choices,
'sel_entry' : val == '_entry_',
'required' : True,
"nobreaks" : cd.get("nobreaks", False),
"comment" : request.POST.get(key2, ""),
}
@answer_proc('choice', 'choice-freeform')
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['comment']
if not opt:
raise AnswerException(_(u'Field cannot be blank'))
else:
valid = [c.value for c in question.choices()]
if opt not in valid:
raise AnswerException(_(u'Invalid option!'))
return opt
add_type('choice', 'Choice [radio]')
add_type('choice-freeform', 'Choice with a freeform option [radio]')
@question_proc('choice-multiple', 'choice-multiple-freeform')
def template_multiple(request, question):
key = "question_%s" % question.number
choices = []
counter = 0
cd = question.getcheckdict()
defaults = cd.get('default','').split(',')
for choice in question.choices():
counter += 1
key = "question_%s_multiple_%d" % (question.number, choice.sortid)
if key in request.POST or \
(request.method == 'GET' and choice.value in defaults):
choices.append( (choice, key, ' checked',) )
else:
choices.append( (choice, key, '',) )
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, '',) )
return {
"choices": choices,
"extras": extras,
"nobreaks" : cd.get("nobreaks", False),
"template" : "questionnaire/choice-multiple-freeform.html",
"required" : cd.get("required", False) and cd.get("required") != "0",
}
@answer_proc('choice-multiple', 'choice-multiple-freeform')
def process_multiple(question, answer):
multiple = []
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()
for k, v in answer.items():
if k.startswith('multiple'):
multiple.append(v)
if k.startswith('more') and len(v.strip()) > 0:
multiple.append(v)
if len(multiple) < requiredcount:
raise AnswerException(ungettext(u"You must select at least %d option",
u"You must select at least %d options",
requiredcount) % requiredcount)
return "; ".join(multiple)
add_type('choice-multiple', 'Multiple-Choice, Multiple-Answers [checkbox]')
add_type('choice-multiple-freeform', 'Multiple-Choice, Multiple-Answers, plus freeform [checkbox, input]')

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 questionnaire import *
from questionnaire import Processors, QuestionProcessors
from django.utils.translation import ugettext as _
@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,40 @@
from questionnaire import *
from django.utils.translation import ugettext as _
@question_proc('range')
def question_range(request, question):
cd = question.getcheckdict()
Range = cd.get('range', '1-5')
try:
rmin, rmax = Range.split('-', 1)
rmin, rmax = int(rmin), int(rmax)
except ValueError:
rmin = 0
rmax = int(range)
selected = int(request.POST.get('question_%s' % question.number, rmin))
Range = range(rmin, rmax+1)
return {
'required' : True,
'range' : Range,
'selected' : selected,
}
@answer_proc('range')
def process_range(question, answer):
checkdict = question.getcheckdict()
try:
rmin,rmax = checkdict.get('range','1-10').split('-',1)
rmin, rmax = int(rmin), int(rmax)
except:
raise AnswerException("Error in question. Additional checks field should contain range='min-max'")
try:
ans = int(answer['ANSWER'])
except:
raise AnswerException("Could not convert `%r` to integer.")
if ans > rmax or ans < rmin:
raise AnswerException(_(u"Out of range"))
return ans
add_type('range', 'Range of numbers [select]')

View File

@ -0,0 +1,95 @@
from questionnaire import *
from django.utils.translation import ugettext as _
@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
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,
'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]
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):
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:
if not ans.strip() and checkdict.get('required', False):
raise AnswerException(_(u'Field cannot be blank'))
if ansdict.has_key('comment') and len(ansdict['comment']) > 0:
return "%s; %s" % (ans, ansdict['comment'])
return ans
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 questionnaire import *
from django.utils.translation import ugettext as _, ugettext_lazy
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,7 @@
{% extends "admin/change_form.html" %}
{% load questionnaire %}
{% block object-tools %}
{% if original %}
<a href="{{ original|qtesturl }}">Show on Site</a>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,36 @@
{% 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 cl.get_query_set %}
<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.questions %}
<a href="{{ q.id }}/">{{ q.number }}. {{ q.text }}</a> [{{ q.type }}]<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,9 @@
{% load i18n %}
{% for sel, choice in qdict.choices %}
<input onClick="document.getElementById('{{ question.number }}_comment').disabled = true; valchanged('{{ question.number }}', this.value)" type="radio" id="{{ question.number }}_{{ forloop.counter }}" name="question_{{ question.number }}" value="{{ choice.value }}" {% if sel %} checked{% endif %}>
<label for="{{ question.number }}_{{ forloop.counter }}">{{ choice.text }}</label>
{% if not question.getcheckdict.nobreaks %}<br />{% endif %}
{% endfor %}
<input onClick="document.getElementById('{{ question.number }}_comment').disabled = false; valchanged('{{ question.number }}', '_entry_');" type="radio" id="{{ question.number }}_entry" name="question_{{ question.number }}" value="_entry_"{% if qdict.sel_entry %}checked{% endif %}>
<input id="{{ question.number }}_comment" {% if not qdict.sel_entry %}disabled="disabled"{% endif %} type="input" name="question_{{ question.number }}_comment" value="{{ qdict.comment }}">{% if question.extra %}{{ question.extra }}{% endif %}

View File

@ -0,0 +1,11 @@
{% load i18n %}
{% for choice, key, checked in qdict.choices %}
<input type="checkbox" id="{{ key }}" name="{{ key }}" value="{{ choice.value }}" {{ checked }}>
<label for="{{ key }}">{{ choice.text }}</label>{% if not qdict.nobreaks %}<br />{% endif %}
{% endfor %}
{% if qdict.extras %}
{% if question.extra %}<label for="question_{{ question.number }}_more1">{{ question.extra }}</label><br />{% endif %}
{% for key, value in qdict.extras %}
<b>{{ forloop.counter }}.</b> <input type="input" name="{{ key }}" size="50" value="{{ value }}"><br />
{% endfor %}
{% endif %}

View File

@ -0,0 +1,14 @@
{% load i18n %}
<input onClick="valchanged('{{ question.number }}', this.value);" type="radio" id="{{ question.number }}_yes" name="question_{{ question.number }}" value="yes"{% ifequal qdict.value "yes" %} checked{% endifequal %}>
<label for="{{ question.number }}_yes">{% trans "Yes" %}</label>
<input onClick="valchanged('{{ question.number }}', this.value);" type="radio" id="{{ question.number }}_no" name="question_{{ question.number }}" value="no"{% ifequal qdict.value "no" %} checked{% endifequal %}>
<label for="{{ question.number }}_no">{% trans "No" %}</label>
{% if qdict.hasdontknow %}
<input onClick="valchanged('{{ question.number }}', this.value);" type="radio" id="{{ question.number }}_dontknow" name="question_{{ question.number }}" value="dontknow"{% ifequal qdict.value "dontknow" %} checked{% endifequal %}>
<label for="{{ question.number }}_dontknow">{% trans "Don't Know" %}</label>
{% endif %}
{% if qdict.hascomment %}
{% if question.extra %}<br />{{ question.extra }}{% else %}: {% endif %}
<input type="input" {{ qdict.checks|safe }} id="{{ question.number }}_comment" name="question_{{ question.number }}_comment" size="50" value="{{ qdict.comment }}">
{% endif %}
<br />

View File

@ -0,0 +1,6 @@
{% load i18n %}
{% for sel, choice in qdict.choices %}
<input type="radio" onClick="valchanged('{{ question.number }}', this.value)" id="{{ question.number }}_{{ forloop.counter }}" name="question_{{ question.number }}" value="{{ choice.value }}"{% if sel %} checked{% endif %}>
<label for="{{ question.number }}_{{ forloop.counter }}">{{ choice.text }}</label>
{% if not qdict.nobreaks %}<br />{% endif %}
{% endfor %}

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block content %}
<h2>Merci vielmals! Die Umfragung ist fertig! Sie bekommen eine neue einladung nächstes Jahr.</h2>
<h4>If you change your email address in the meantime, please contact the study nurse.</h4>
{% endblock %}

View File

@ -0,0 +1,6 @@
{% extends "base.html" %}
{% block content %}
<h2>Thanks for completing the survey! You will be contacted again next year.</h2>
<h4>If you change your email address in the meantime, please contact the study nurse.</h4>
{% endblock %}

View File

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

View File

@ -0,0 +1,2 @@
{% load i18n %}
<textarea name="question_{{ question.number }}" cols="60" rows="10"></textarea>

View File

@ -0,0 +1,2 @@
{% load i18n %}
<input type="text" size="60" id="{{ question.number }}" name="question_{{ question.number }}" value="{{ qdict.value }}">

View File

@ -0,0 +1,87 @@
{% extends "base.html" %}
{% load markup questionnaire i18n %}
{% block headextra %}
<script type="text/javascript" src="/media/questionset.js"></script>
{% 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 %}
{% endblock %}
{% block styleextra %}
.questionset_text {
font-size: large;
}
.qnumber {
color: #000;
}
.required {
font-style: italic;
color: #600;
}
img.percentImage {
background: white url(/media/progress/percentImage_back4.png) top left no-repeat;
padding: 0;
margin: 5px 0 0 0;
background-position: 1px 0;
}
{% endblock %}
{% block content %}
{% if progress %}
<p style="text-align: center;">
<img alt="{{ progress.0 }}" src="/media/progress/percentImage.png" class="percentImage" style="background-position: {{ progress.1 }}px 0pt;" /> {{ progress.0 }}% complete.</p>
{% endif %}
<div class="questionset_text">{{ questionset.text|textile }}</div>
<form name="qform" id="qform" action="{{ request.path }}" method="POST">
<input type="hidden" name="questionset_id" value="{{ questionset.id }}">
{% for question, qdict in qlist %}
{% with errors|dictget:question.number as error %}
<div class="question type_{{ qdict.qtype }} container{% if error %} error prepend-top{% endif %}{{ qdict.qnum_class }}{{ qdict.qalpha_class }}" id="qc_{{ question.number }}" {{qdict.checkstring|safe}}>
{% if error %}<b>* {{ error }}</b><br />{% endif %}
{% if request.user.is_authenticated %}
<span style="float: right"><a href="/admin/questionnaire/question/{{ question.id }}/"><font size="xx-small">(edit {{ question.number }})</font></a></span>
{% endif %}
{% if qdict.custom %}
{% include qdict.template %}
{% else %}
{% if question.newline %}
<b class="qnumber{% if qdict.required %} required{% endif %}">{{ question.display_number|safe }}.</b> {{ question.text }}
<br /><div class="prepend-1">
{% else %}
{% if question.text.strip %}
<div class="span-{{ alignment }}"><b class="qnumber{% if qdict.required %} required{% endif %}">{{ question.display_number|safe }}.</b> {{ question.text }}
</div>
{% else %}
<b class="qnumber{% if qdict.required %} required{% endif %}">{{ question.display_number|safe }}.</b> {{ question.text }}
{% endif %}
{% endif %}
{% include qdict.template %}
{% if question.newline %}</div> <!-- /prepend-1 -->{% endif %}
{% endif %}
</div> <!-- /question container -->
{% endwith %}
{% endfor %}
<br />
</span>
<div style="text-align:center; margin-bottom: 0.5em;"><input name="submit" type="submit" value="{% trans "Continue" %}"></div>
{% with questionset.prev as prev %}
{% if prev %}<a href="javascript:history.back();" title="">Return to previous page</a>{% endif %}
{% endwith %}
</form>
<script type="text/javascript">
{% for trigger in triggers %}addtrigger("{{trigger}}");
{% endfor %}
{% for k,v in qvalues.items %}qvalues['{{ k }}'] = '{{ v|escapejs }}';
{% endfor %}
for(key in qvalues) {
valchanged(key, qvalues[key]);
}
</script>
{% endblock %}

View File

@ -0,0 +1,5 @@
<select onchange="valchanged('{{ question.number }}', this.options[this.selectedIndex].value);" name="question_{{ question.number }}" style="margin-top: 1em">
{% for x in qdict.range %}
<option value="{{ x }}" {% ifequal qdict.selected x %}selected{% endifequal %}>&nbsp;&nbsp;{{ x }}&nbsp;&nbsp;</option>
{% endfor %}
</select>

View File

@ -0,0 +1,6 @@
<input type="text" name="question_{{ question.number }}" size="10" value="{{ qdict.value }}">
<select style="margin: 0" name="question_{{ question.number }}_unit">
{% for val, desc, sel in qdict.timeperiods %}
<option value="{{ val }}"{% if sel %} selected="selected"{% endif %}>{{ desc }}</option>
{% endfor %}
</select>

View File

View File

@ -0,0 +1,34 @@
#!/usr/bin/python
from django import template
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext as _
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))

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

143
questionnaire/tests.py Normal file
View File

@ -0,0 +1,143 @@
"""
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 questionnaire.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/')
assert 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/')
assert response.status_code == 200
assert 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"})
assert "Don't Know" in response.content
assert response.status_code == 200
runinfo = RunInfo.objects.get(runid='test:test')
assert runinfo.subject.language == 'en'
response = self.client.get('/q/test:test/1/', {"lang" : "de"})
assert "Weiss nicht" in response.content
assert response.status_code == 200
runinfo = RunInfo.objects.get(runid='test:test')
assert 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)
assert response.status_code == 200
errors = response.context[-1]['errors']
assert 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)
assert response.status_code == 200
assert 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/')
assert response.status_code == 200
assert response.template[0].name == 'questionnaire/questionset.html'
response = c.post('/q/1real/', ansdict1)
assert response.status_code == 302
assert 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/')
assert response.status_code == 200
assert response.template[0].name == 'questionnaire/questionset.html'
response = c.post('/q/1real/', ansdict2)
assert response.status_code == 302
assert response['Location'] == 'http://testserver/'
assert RunInfo.objects.filter(runid='1real').count() == 0
dbvalues = {
'1' : ansdict1['question_1'],
'2' : ansdict1['question_2'],
'3' : ansdict1['question_3'],
'4' : ansdict1['question_4'],
'5' : '%s; %s' % (ansdict1['question_5'], ansdict1['question_5_comment']),
'6' : '%s; %s' % (ansdict1['question_6'], ansdict1['question_6_comment']),
'7' : ansdict1['question_7'],
'8' : '%s; %s' % (ansdict1['question_8'], ansdict1['question_8_unit']),
'9' : 'q9_choice1',
'10' : 'my freeform',
'11' : 'q11_choice2; q11_choice4',
'12' : 'q12_choice1; blah',
}
for k, v in dbvalues.items():
ans = Answer.objects.get(runid=runid, subject__id=self.subject_id,
question__number=k)
assert ans.answer == v

15
questionnaire/urls.py Normal file
View File

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

49
questionnaire/utils.py Normal file
View File

@ -0,0 +1,49 @@
#!/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 calc_alignment(string):
l = 2 + len(string.strip()) // 6
if l <= 4:
return 4
if l <= 7:
return 7
if l < 10:
return 10
return l
if __name__ == "__main__":
import doctest
doctest.testmod()

499
questionnaire/views.py Normal file
View File

@ -0,0 +1,499 @@
#!/usr/bin/python
# vim: set fileencoding=utf-8
from django.http import HttpResponse, Http404, \
HttpResponsePermanentRedirect, HttpResponseRedirect
from django.template import RequestContext, Context, Template, loader
from django.views.decorators.cache import cache_control
from django.core.urlresolvers import reverse
from django.contrib.auth.decorators import login_required
from django.shortcuts import render_to_response, get_object_or_404
from django.contrib.sites.models import Site
from django.db import transaction
from django.conf import settings
from models import *
from questionnaire import QuestionProcessors, Processors, AnswerException, \
questionset_done, questionnaire_done
from datetime import datetime
from django.utils import translation
import time, os, smtplib, rfc822
from parsers import *
from utils import numal_sort, split_numal, calc_alignment
from emails import send_emails, _send_email
import logging
def r2r(tpl, request, **contextdict):
"Shortcut to use RequestContext instead of Context in templates"
contextdict['request'] = request
return render_to_response(tpl, contextdict, context_instance = RequestContext(request))
def get_runinfo(random):
"Return the RunInfo entry with the provided random key"
res = RunInfo.objects.filter(random=random)
if res:
return res[0]
return None
def get_question(number, questionnaire):
"Return the specified Question (by number) from the specified Questionnaire"
res = Question.objects.filter(number=number, questionset__questionnaire=questionnaire)
if res:
return res[0]
return None
def delete_answer(question, subject, runid):
"Delete the specified question/subject/runid combination from the Answer table"
Answer.objects.filter(subject=subject, runid=runid, question=question).delete()
def add_answer(runinfo, question, answer_dict):
"""
Add an Answer to a Question for RunInfo, given the relevant form input
answer_dict contains the POST'd elements for this question, minus the
question_{number} prefix. The question_{number} form value is accessible
with the ANSWER key.
"""
answer = Answer()
answer.question = question
answer.subject = runinfo.subject
answer.runid = runinfo.runid
type = question.get_type()
if "ANSWER" not in answer_dict:
answer_dict['ANSWER'] = None
if type in Processors:
answer.answer = Processors[type](question, answer_dict) or ''
else:
raise AnswerException("No Processor defined for question type %s" % type)
# first, delete all existing answers to this question for this particular user+run
delete_answer(question, runinfo.subject, runinfo.runid)
# then save the new answer to the database
answer.save()
return True
def questionset_satisfies_checks(questionset, runinfo):
"Return True is RunInfo satisfied checks for the specified QuestionSet"
checks = parse_checks(questionset.checks)
for check, value in checks.items():
if check == 'maleonly' and runinfo.subject.gender != 'male':
return False
if check == 'femaleonly' and runinfo.subject.gender != 'female':
return False
if check == 'shownif' and value and value.strip():
depparser = BooleanParser(dep_check, runinfo, {})
res = depparser.parse(value)
if not res:
return False
return True
def redirect_to_qs(runinfo):
"Redirect to the correct and current questionset URL for this RunInfo"
url = reverse("questionset",
args=[ runinfo.random, runinfo.questionset.sortid ])
return HttpResponseRedirect(url)
@transaction.commit_on_success
def questionnaire(request, runcode=None, qs=None):
"""
Process submitted answers (if present) and redirect to next page
If this is a POST request, parse the submitted data in order to store
all the submitted answers. Then return to the next questionset or
return a completed response.
If this isn't a POST request, redirect to the main page.
We only commit on success, to maintain consistency. We also specifically
rollback if there were errors processing the answers for this questionset.
"""
# if runcode provided as query string, redirect to the proper page
if not runcode and request.GET.has_key("runcode"):
transaction.commit()
return HttpResponseRedirect(
reverse("questionnaire",
args=[request.GET['runcode']]))
runinfo = get_runinfo(runcode)
if not runinfo:
transaction.commit()
return HttpResponseRedirect('/')
if runinfo and not qs:
# Only change the language to the subjects choice for the initial
# questionnaire page (may be a direct link from an email)
if hasattr(request, 'session'):
request.session['django_language'] = runinfo.subject.language
translation.activate(runinfo.subject.language)
# --------------------------------
# --- Handle non-POST requests ---
# --------------------------------
if request.method != "POST":
if qs is not None:
qs = get_object_or_404(QuestionSet, sortid=qs, questionnaire=runinfo.questionset.questionnaire)
if runinfo.random.startswith('test:'):
pass # ok for testing
elif qs.sortid > runinfo.questionset.sortid:
# you may jump back, but not forwards
return redirect_to_qs(runinfo)
runinfo.questionset = qs
runinfo.save()
transaction.commit()
# no questionset id in URL, so redirect to the correct URL
if qs is None:
return redirect_to_qs(runinfo)
return show_questionnaire(request, runinfo)
# -------------------------------------
# --- Process POST with QuestionSet ---
# -------------------------------------
# if the submitted page is different to what runinfo says, update runinfo
# XXX - do we really want this?
qs = request.POST.get('questionset_id', None)
try:
qsobj = QuestionSet.objects.filter(pk=qs)[0]
if qsobj.questionnaire == runinfo.questionset.questionnaire:
if runinfo.questionset != qsobj:
runinfo.questionset = qsobj
runinfo.save()
except:
pass
questionnaire = runinfo.questionset.questionnaire
questionset = runinfo.questionset
# to confirm that we have the correct answers
expected = questionset.questions()
items = request.POST.items()
extra = {} # question_object => { "ANSWER" : "123", ... }
# this will ensure that each question will be processed, even if we did not receive
# any fields for it. Also works to ensure the user doesn't add extra fields in
for x in expected:
items.append( (u'question_%s_Trigger953' % x.number, None) )
# generate the answer_dict for each question, and place in extra
for item in items:
key, value = item[0], item[1]
if key.startswith('question_'):
answer = key.split("_", 2)
question = get_question(answer[1], questionnaire)
if not question:
logging.warn("Unknown question when processing: %s" % answer[1])
continue
extra[question] = ans = extra.get(question, {})
if(len(answer) == 2):
ans['ANSWER'] = value
elif(len(answer) == 3):
ans[answer[2]] = value
else:
logging.warn("Poorly formed form element name: %r" % answer)
continue
extra[question] = ans
errors = {}
for question, ans in extra.items():
if u"Trigger953" not in ans:
logging.warn("User attempted to insert extra question (or it's a bug)")
continue
try:
cd = question.getcheckdict()
# requiredif is the new way
depon = cd.get('requiredif',None) or cd.get('dependent',None)
if depon:
depparser = BooleanParser(dep_check, runinfo, extra)
if not depparser.parse(depon):
# if check is not the same as answer, then we don't care
# about this question plus we should delete it from the DB
delete_answer(question, runinfo.subject, runinfo.runid)
if cd.get('store', False):
runinfo.set_cookie(question.number, None)
continue
add_answer(runinfo, question, ans)
if cd.get('store', False):
runinfo.set_cookie(question.number, ans['ANSWER'])
except AnswerException, e:
errors[question.number] = e
except Exception:
logging.exception("Unexpected Exception")
transaction.rollback()
raise
if len(errors) > 0:
res = show_questionnaire(request, runinfo, errors=errors)
transaction.rollback()
return res
questionset_done.send(sender=None,runinfo=runinfo,questionset=questionset)
next = questionset.next()
while next and not questionset_satisfies_checks(next, runinfo):
next = next.next()
runinfo.questionset = next
runinfo.save()
if next is None: # we are finished
hist = RunInfoHistory()
hist.subject = runinfo.subject
hist.runid = runinfo.runid
hist.completed = datetime.now()
hist.save()
questionnaire_done.send(sender=None, runinfo=runinfo,
questionnaire=questionnaire)
redirect_url = questionnaire.redirect_url
for x,y in (('$LANG', translation.get_language()),
('$SUBJECTID', runinfo.subject.id),
('$RUNID', runinfo.runid),):
redirect_url = redirect_url.replace(x, str(y))
if runinfo.runid in ('12345', '54321') \
or runinfo.runid.startswith('test:'):
runinfo.questionset = QuestionSet.objects.filter(questionnaire=questionnaire).order_by('sortid')[0]
runinfo.save()
else:
runinfo.delete()
transaction.commit()
if redirect_url:
return HttpResponseRedirect(redirect_url)
return r2r("questionnaire/complete.$LANG.html", request)
transaction.commit()
return redirect_to_qs(runinfo)
def get_progress(percent):
"Based on a percentage as a float, calculate percentage needed for CSS progress bar"
if int(percent) >= 1:
return 100, "1"
pc = "-%s" % (120 - int(percent * 120) + 1)
return (int(percent * 100), pc)
def show_questionnaire(request, runinfo, errors={}):
"""
Return the QuestionSet template
Also add the javascript dependency code.
"""
questions = runinfo.questionset.questions()
total = len(runinfo.questionset.questionnaire.questionsets())
qlist = []
jsinclude = [] # js files to include
cssinclude = [] # css files to include
jstriggers = []
qvalues = {}
alignment=4
for question in questions:
Type = question.get_type()
_qnum, _qalpha = split_numal(question.number)
if _qalpha:
_qalpha_class = ord(_qalpha[-1]) % 2 and 'odd' or 'even'
qdict = {
'template' : 'questionnaire/%s.html' % (Type),
'qnum' : _qnum,
'qalpha' : _qalpha,
'qtype' : Type,
'qnum_class' : (_qnum % 2 == 0) and " qeven" or " qodd",
'qalpha_class' : _qalpha and (ord(_qalpha[-1]) % 2 \
and ' alodd' or ' aleven') or '',
}
if not question.newline():
alignment = max(alignment, calc_alignment(question.text))
# add javascript dependency checks
cd = question.getcheckdict()
depon = cd.get('requiredif',None) or cd.get('dependent',None)
if depon:
# extra args to BooleanParser are not required for toString
parser = BooleanParser(dep_check)
qdict['checkstring'] = ' checks="%s"' % parser.toString(depon)
jstriggers.append('qc_%s' % question.number)
if 'default' in cd:
qvalues[question.number] = cd['default']
if Type in QuestionProcessors:
qdict.update(QuestionProcessors[Type](request, question))
if 'alignment' in qdict:
alignment = max(alignment, qdict['alignment'])
if 'jsinclude' in qdict:
if qdict['jsinclude'] not in jsinclude:
jsinclude.extend(qdict['jsinclude'])
if 'cssinclude' in qdict:
if qdict['cssinclude'] not in cssinclude:
cssinclude.extend(qdict['jsinclude'])
if 'jstriggers' in qdict:
jstriggers.extend(qdict['jstriggers'])
if 'qvalue' in qdict:
qvalues[question.number] = qdict['qvalue']
qlist.append( (question, qdict) )
progress = None
if runinfo.questionset.sortid != 0:
progress = get_progress(runinfo.questionset.sortid / float(total))
# initialize qvalues
for k,v in runinfo.get_cookiedict().items():
qvalues[k] = v
if request.POST:
for k,v in request.POST.items():
if k.startswith("question_"):
s = k.split("_")
if len(s) == 2:
qvalues[s[1]] = v
r = r2r("questionnaire/questionset.html", request,
questionset=runinfo.questionset,
runinfo=runinfo,
errors=errors,
qlist=qlist,
progress=progress,
triggers=jstriggers,
qvalues=qvalues,
alignment=alignment,
jsinclude=jsinclude,
cssinclude=cssinclude)
r['Cache-Control'] = 'no-cache'
r['Expires'] = 'Mon, 01 Jan 2001 01:01:01 GMT'
return r
@login_required
def export_csv(request, qid): # questionnaire_id
"""
For a given questionnaire id, generaete a CSV containing all the
answers for all subjects.
"""
import tempfile, csv
from django.core.servers.basehttp import FileWrapper
fd = tempfile.TemporaryFile()
qid = int(qid)
columns = [x[0] for x in Question.objects.filter(questionset__questionnaire__id = qid).distinct('number').values_list('number')]
columns.sort(numal_sort)
columns.insert(0,u'subject')
columns.insert(1,u'runid')
writer = csv.DictWriter(fd, columns, restval='--')
coldict = {}
for col in columns:
coldict[col] = col
writer.writerows([coldict,])
answers = Answer.objects.filter(question__questionset__questionnaire__id = qid).order_by('subject', 'runid', 'question__number',)
if not answers:
raise Exception, "EMPTY!" # FIXME
runid = answers[0].runid
subject = answers[0].subject
d = { u'subject' : "%s/%s" % (subject.id, subject.state), u'runid' : runid }
for answer in answers:
if answer.runid != runid or answer.subject != subject:
writer.writerows([d,])
runid = answer.runid
subject = answer.subject
d = { u'subject' : "%s/%s" % (subject.id, subject.state), u'runid' : runid }
d[answer.question.number] = answer.answer.encode('utf-8')
# and don't forget about the last one
if d:
writer.writerows([d,])
response = HttpResponse(FileWrapper(fd), mimetype="text/csv")
response['Content-Length'] = fd.tell()
response['Content-Disposition'] = 'attachment; filename="export-%s.csv"' % qid
fd.seek(0)
return response
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.
"""
questionnaire = runinfo.questionset.questionnaire
if "," not in expr:
expr = expr + ",yes"
check_questionnum, check_answer = expr.split(",",1)
try:
check_question = Question.objects.get(number=check_questionnum,
questionset__questionnaire = questionnaire)
except Question.DoesNotExist:
return False
if check_question in answerdict:
actual_answer = answerdict[check_question].get('ANSWER', '')
elif runinfo.get_cookie(check_questionnum, False):
actual_answer = runinfo.get_cookie(check_questionnum)
else:
# retrieve from database
logging.warn("Put `store` in checks field for question %s" \
% check_questionnum)
ansobj = Answer.objects.filter(question=check_question,
runid=runinfo.runid, subject=runinfo.subject)
if ansobj:
actual_answer = ansobj[0].answer.split(";")[0]
else:
actual_answer = None
if actual_answer is None:
actual_answer = u''
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
if check_answer.startswith("!"):
return check_answer[1:].strip() != actual_answer.strip()
return check_answer.strip() == actual_answer.strip()
@login_required
def send_email(request, runinfo_id):
if request.method != "POST":
return HttpResponse("This page MUST be called as a POST request.")
runinfo = get_object_or_404(RunInfo, pk=int(runinfo_id))
successful = _send_email(runinfo)
return r2r("emailsent.html", request, runinfo=runinfo, successful=successful)