Initial Checkin of Seantis Questionnaire
|
@ -0,0 +1,6 @@
|
|||
*.pyc
|
||||
*.mo
|
||||
*~
|
||||
*.sqlite
|
||||
*.swp
|
||||
local_settings.py
|
|
@ -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,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)
|
|
@ -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',)
|
|
@ -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
|
||||
|
|
@ -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
|
|
@ -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>
|
|
@ -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 %}
|
|
@ -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
|
|
@ -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
|
|
@ -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),
|
||||
)
|
|
@ -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)
|
||||
|
|
@ -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)
|
|
@ -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)
|
|
@ -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
|
|
@ -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
|
|
@ -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"
|
|
@ -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"
|
|
@ -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
|
|
@ -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;}
|
After Width: | Height: | Size: 655 B |
After Width: | Height: | Size: 455 B |
After Width: | Height: | Size: 537 B |
|
@ -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>
|
|
@ -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;
|
||||
}
|
|
@ -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.
|
|
@ -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">“</span>asdf”
|
||||
(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;
|
||||
}
|
After Width: | Height: | Size: 777 B |
After Width: | Height: | Size: 641 B |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 691 B |
After Width: | Height: | Size: 741 B |
After Width: | Height: | Size: 591 B |
After Width: | Height: | Size: 46 KiB |
After Width: | Height: | Size: 663 B |
|
@ -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">
|
|
@ -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); }
|
|
@ -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">
|
|
@ -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; }
|
|
@ -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%;}
|
|
@ -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;}
|
|
@ -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; }
|
|
@ -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; }
|
After Width: | Height: | Size: 161 B |
|
@ -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;}
|
|
@ -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) ") ";
|
||||
} */
|
|
@ -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; }
|
|
@ -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; }
|
After Width: | Height: | Size: 1023 B |
After Width: | Height: | Size: 241 B |
After Width: | Height: | Size: 540 B |
After Width: | Height: | Size: 530 B |
After Width: | Height: | Size: 543 B |
After Width: | Height: | Size: 547 B |
|
@ -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;
|
||||
}
|
|
@ -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 " " + 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
|
||||
|
|
@ -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()
|
|
@ -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
|
|
@ -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]')
|
||||
|
||||
|
|
@ -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"))
|
||||
|
|
@ -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]')
|
||||
|
||||
|
||||
|
|
@ -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')
|
||||
|
||||
|
||||
|
|
@ -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]')
|
||||
|
|
@ -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 %}
|
|
@ -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 = '↑'
|
||||
} else {
|
||||
obj.style.display = 'none';
|
||||
head.innerHTML = '↓'
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
{% for questionnaire in cl.get_query_set %}
|
||||
|
||||
<H2 onClick="togglehide('{{ questionnaire.id }}');">
|
||||
<span id="qhead-{{ questionnaire.id }}">↑</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 %}
|
||||
→ <a href="add/?questionset={{ questionset.id }}">Add Question to <tt>{{ questionset.heading }}</tt></a>
|
||||
{% endfor %}
|
||||
<br /><br />→ <a href="/admin/questionnaire/questionset/add/?questionnaire={{ questionnaire.id }}">Add QuestionSet to <tt>{{ questionnaire.name }}</tt></a>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endblock %}
|
|
@ -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 %}
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
|
@ -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 />
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -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 %}
|
|
@ -0,0 +1 @@
|
|||
{% include question.checks %}
|
|
@ -0,0 +1,2 @@
|
|||
{% load i18n %}
|
||||
<textarea name="question_{{ question.number }}" cols="60" rows="10"></textarea>
|
|
@ -0,0 +1,2 @@
|
|||
{% load i18n %}
|
||||
<input type="text" size="60" id="{{ question.number }}" name="question_{{ question.number }}" value="{{ qdict.value }}">
|
|
@ -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 %}
|
|
@ -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 %}> {{ x }} </option>
|
||||
{% endfor %}
|
||||
</select>
|
|
@ -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>
|
|
@ -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))
|
||||
|
|
@ -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'),
|
||||
)
|
|
@ -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
|
||||
|
|
@ -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')
|
||||
)
|
|
@ -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()
|
|
@ -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)
|