weitere Verbesserungen bis zur finalen Einsatzreife

This commit is contained in:
lars 2012-08-16 16:24:04 +00:00
parent 59ccb5a87c
commit 68a8aca17b
12 changed files with 1210 additions and 109 deletions

View file

@ -6,25 +6,25 @@ DEFINITION_OPTIONS = (
(u"Karo", "http://stadtgestalten.org/umfrage/kirchenplatz2012/media/option2.png",
(u"verbreiterte Gehwege", u"Wochenmarkt auf Ostseite", u"gepflasterte Platzfläche im Westen (Nutzung der Freiflächen für Stände, Außengastronomie, etc. möglich)", u"Kirchenumfeld als befestigte Fläche und Grün", u"30 PKW-Stellplätze auf dem Platz (Ostseite)", u"Bushaltestelle auf der Westseite (gegenüber Mühlenstraße)", u"Taxistand vor Kirchenplatz Nr. 1-2", u"Baumpflanzungen auf dem Platz und straßenbegleitend an den Gehwegen")),
(u"Marktplatz mit Baumhain", "http://stadtgestalten.org/umfrage/kirchenplatz2012/media/option3.png",
(u"verbreiterte Gehwege", u"Wochenmarkt auf Ostseite (ingeschränkte Fläche wg. Baumpflanzungen)", u"teilweise gepflasterte Platzfläche im Westen (Nutzung der Freiflächen (nicht unter den Bäumen) für Stände, Außengastronomie, etc. möglich) sowie starke Baumpflanzungen", u"Kirchenumfeld als befestigte Fläche", u"44 PKW-Stellplätze straßenbegleitend auf der Nord- und Südseite", u"Bushaltestelle auf der Westseite (gegenüber Mühlenstraße)", u"Taxistand vor Kirchenplatz Nr. 13", u"Baumpflanzungen auf dem Platz und straßenbegleitend an den Gehwegen")),
(u"verbreiterte Gehwege", u"Wochenmarkt auf Ostseite (eingeschränkte Fläche wg. Baumpflanzungen)", u"teilweise gepflasterte Platzfläche im Westen (Nutzung der Freiflächen (nicht unter den Bäumen) für Stände, Außengastronomie, etc. möglich) sowie starke Baumpflanzungen", u"Kirchenumfeld als befestigte Fläche", u"44 PKW-Stellplätze straßenbegleitend auf der Nord- und Südseite", u"Bushaltestelle auf der Westseite (gegenüber Mühlenstraße)", u"Taxistand vor Kirchenplatz Nr. 13", u"Baumpflanzungen auf dem Platz und straßenbegleitend an den Gehwegen")),
(u"Befestigter Platz", "http://stadtgestalten.org/umfrage/kirchenplatz2012/media/option4.png",
(u"verbreiterte Gehwege", u"Wochenmarkt auf Ostseite (ingeschränkte Fläche wg. Baumpflanzungen)", u"teilweise gepflasterte Platzfläche im Westen (Nutzung der Freiflächen für Stände, Außengastronomie, etc. möglich) sowie starke Baumpflanzungen", u"Kirchenumfeld als befestigte Fläche", u"39 PKW-Stellplätze straßenbegleitend auf der Nord- und Südseite", u"Bushaltestelle im Bestand", u"Taxistand vor Kirchenplatz Nr. 13", u"Baumpflanzungen auf dem Platz und auf der West- und Ostseite straßenbegleitend an den Gehwegen")))
(u"verbreiterte Gehwege", u"Wochenmarkt auf Ostseite (eingeschränkte Fläche wg. Baumpflanzungen)", u"teilweise gepflasterte Platzfläche im Westen (Nutzung der Freiflächen für Stände, Außengastronomie, etc. möglich) sowie starke Baumpflanzungen", u"Kirchenumfeld als befestigte Fläche", u"39 PKW-Stellplätze straßenbegleitend auf der Nord- und Südseite", u"Bushaltestelle im Bestand", u"Taxistand vor Kirchenplatz Nr. 13", u"Baumpflanzungen auf dem Platz und auf der West- und Ostseite straßenbegleitend an den Gehwegen")))
DEFINITION_QUALITY_RANGES = {
"mehr_weniger": (
(u"++", u"++ Ja, trifft voll zu"),
(u"+", u"+ Ja, mit Abstrichen"),
(u"-", u"- Kaum bis wenig"),
(u"--", u"-- Überhaupt nicht")),
(u"++", u"Ja, trifft voll zu"),
(u"+", u"Ja, mit Abstrichen"),
(u"-", u"Kaum bis wenig"),
(u"--", u"Überhaupt nicht")),
"baeume": (
(u"++", u"++ Lineare Anordnung (Baumreihen)"),
(u"--", u"-- Beliebige Anordnung in Einzelbäume, Baumgruppen, Baumhainen")),
(u"++", u"Lineare Anordnung (Baumreihen)"),
(u"--", u"Beliebige Anordnung in Einzelbäume, Baumgruppen, Baumhainen")),
"gesamt": (
(u"++", u"++ Sehr gute Variante, berücksichtigt so gut wie alle Anforderungen"),
(u"+", u"+ In Teilen gut gelungene Variante"),
(u"-", u"- Wenig attraktiv, berücksichtigt zu wenige Anforderungen"),
(u"--", u"-- Unattraktiv, unpassende Variante")),
(u"++", u"Sehr gute Variante, berücksichtigt so gut wie alle Anforderungen"),
(u"+", u"In Teilen gut gelungene Variante"),
(u"-", u"Wenig attraktiv, berücksichtigt zu wenige Anforderungen"),
(u"--", u"Unattraktiv, unpassende Variante")),
}

View file

@ -1,5 +1,24 @@
#!/usr/bin/env python2.6
# -*- coding: utf-8 -*-
#
#
# umfrage - einfache web-basierte Meinungsabfragen
# Copyright (C) 2012 - Lars Kruse <devel@sumpfralle.de>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
import os
# the basedir is the parent dir of the location of this script
@ -13,8 +32,11 @@ from dataset import DEFINITION_OPTIONS, DEFINITION_QUALITY_RANGES, \
DEFINITION_QUESTIONS
import ConfigParser
import datetime
import uuid
import re
import smtplib
import email.mime.text
import email.utils
import sqlobject
import bobo
import genshi
@ -23,6 +45,13 @@ import genshi.filters
CONFIG_FILE = os.path.join(BASE_DIR, "umfrage.conf")
MAIL_ADDRESS_REGEX = r"^([\w\-\.]+@(\w[\w\-]+\.)+[\w\-]+)$"
# Set the INIT_DB_UMFRAGE environment setting in order to clean up the
# database during the first initial run. All tables are removed and
# initialized again based on the content of src/dataset.py
# e.g. run the following:
# INIT_DB_UMFRAGE=1 python src/umfrage.py
RUN_FIRST_DB_INIT = "INIT_DB_UMFRAGE" in os.environ
config = ConfigParser.SafeConfigParser()
config.read(CONFIG_FILE)
@ -31,11 +60,6 @@ sqlobject.sqlhub.processConnection = sqlobject.connectionForURI(db_uri)
loader = genshi.template.TemplateLoader(os.path.join(BASE_DIR, 'templates'), auto_reload=False)
BASE_DICT = {
"base_url": "/umfrage/kirchenplatz2012/", # the trailing slash is necessary
"errors": {},
}
class Session(sqlobject.SQLObject):
created = sqlobject.DateTimeCol(default=sqlobject.DateTimeCol.now)
name = sqlobject.UnicodeCol(default=lambda: uuid.uuid4().hex)
@ -62,15 +86,31 @@ class Answer(sqlobject.SQLObject):
session = sqlobject.ForeignKey("Session")
class Submission(sqlobject.SQLObject):
session = sqlobject.ForeignKey("Session")
timestamp = sqlobject.DateTimeCol(default=sqlobject.DateTimeCol.now)
to_default_destination = sqlobject.BoolCol()
title = sqlobject.UnicodeCol()
text = sqlobject.UnicodeCol()
def get_default_values(**kwargs):
value_dict = dict(BASE_DICT)
value_dict = {}
# we always need "options" (e.g. on frontpage)
value_dict["options"] = list(Option.select())
value_dict["options"].sort(key=lambda item: item.weight)
# useful values for different pages
value_dict["base_url"] = config.get("hosting", "full_url")
value_dict["to_address"] = config.get("content", "to_address")
value_dict["errors"] = []
# the base_url is expected to end with a slash
if not value_dict["base_url"].endswith("/"):
value_dict["base_url"] += "/"
for key, value in kwargs.items():
value_dict[key] = value
return value_dict
def render(filename, input_data=None, **values):
stream = loader.load(filename).generate(**values)
if not input_data is None:
@ -85,6 +125,7 @@ def get_previous_question(question):
return one_q
return None
def get_next_question(question):
questions = Question.select().orderBy("weight")
for one_q in questions:
@ -92,17 +133,175 @@ def get_next_question(question):
return one_q
return None
@bobo.query('/')
def show_question(session_id=None, question_id=None, go_backward=False,
**kwargs):
params = get_default_values()
def get_last_question():
questions = list(Question.select().orderBy("weight"))
if questions:
return questions[-1]
else:
return None
def get_session(session_id):
session = None
if session_id:
session = list(Session.selectBy(name=session_id))
if session:
session = session[0]
sessions = Session.selectBy(name=session_id)
if sessions.count() > 0:
session = sessions[0]
return session
def send_mail(to_address, from_address, subject, text):
msg = email.mime.text.MIMEText(unicode(text), _charset="utf-8")
msg["Subject"] = unicode(subject)
msg["From"] = from_address
msg["To"] = to_address
msg["Date"] = email.utils.formatdate()
use_ssl = config.get("mail", "use_ssl", "no")
use_ssl = use_ssl.lower() in ("1", "true", "yes", "on", "enabled")
host = config.get("mail", "host", "localhost")
if use_ssl:
s = smtplib.SMTP_SSL(host)
else:
s = smtplib.SMTP(host)
s.sendmail(from_address, [to_address, from_address], msg.as_string())
s.quit()
@bobo.query('/submit')
def do_submit(session_id=None, subject=None, from_address=None,
to_address=None, summary_text=None, go_backward=None):
session = get_session(session_id)
if go_backward:
return render_question(session, get_last_question())
input_data = {}
params = get_default_values()
if not session:
return bobo.redirect(params["base_url"])
params["session"] = session
if not subject:
params["errors"].append(u"Bitte tragen Sie einen Email-Titel (Betreff) ein!")
else:
input_data["subject"] = subject
if from_address:
# even with invalid input: store it for another attempt
input_data["from_address"] = from_address
if from_address and re.match(MAIL_ADDRESS_REGEX, from_address):
pass
elif from_address:
params["errors"].append(u"Die Absende-Emailadresse scheint ungültig zu sein. Bitte korrigieren Sie dies!")
else:
params["errors"].append(u"Bitte tragen Sie eine Email-Adresse als Absender ein!")
if to_address:
# even with invalid input: store it for another attempt
input_data["to_address"] = to_address
if to_address and re.match(MAIL_ADDRESS_REGEX, to_address):
pass
elif to_address:
params["errors"].append(u"Die Ziel-Emailadresse scheint ungültig zu sein. Bitte korrigieren Sie dies!")
else:
params["errors"].append(u"Bitte tragen Sie eine Ziel-Adresse für die Email " + \
"ein (Vorgabe: %s)!" % params["to_address"])
if not summary_text:
params["errors"].append(u"Die Email darf nicht leer sein. Kehren Sie " + \
"zurück zum Fragebogen und klicken Sie erneut auf " + \
"'Abschließen' um den Vorgabetext wiederherzustellen!")
else:
input_data["summary_text"] = summary_text
if params["errors"]:
return render("summary.html", input_data=input_data, **params)
else:
try:
send_mail(to_address, from_address, subject, summary_text)
except smtplib.SMTPException, err_msg:
params["errors"] = "Der Versand der Mail schlug fehl: %s" % err_msg
return render("summary.html", input_data=input_data, **params)
submission = Submission(session=session,
to_default_destination=(to_address == params["to_address"]),
title=subject, text=summary_text)
return render("submitted.html", **params)
def get_quality_text(question, quality):
for key, text in question.quality_levels:
if key == quality:
return text
return None
def get_answer_lines(answer):
is_empty = True
lines = []
lines.append("")
lines.append("== %s ==" % answer.option.title)
if answer.quality:
is_empty = False
lines.append("Bewertung: %s (%s)" % (answer.quality,
get_quality_text(answer.question, answer.quality)))
if answer.text.strip():
is_empty = False
lines.append("Kommentar:")
for line in answer.text.splitlines():
if line.strip():
lines.append("* %s" % line.strip())
if is_empty:
return []
else:
return lines
def get_summary_text(session):
lines = []
prefix = config.get("content", "text_prefix", "")
if prefix:
lines.append(prefix)
lines.append("")
questions = Question.select().orderBy("weight")
for question in questions:
answers = list(Answer.selectBy(session=session, question=question))
if not answers:
# no answers: skip this question
continue
answers.sort(key=lambda item: item.option.weight)
lines.append("")
lines.append("= %s =" % question.text)
for answer in answers:
lines.extend(get_answer_lines(answer))
lines.append("")
lines.append("")
return os.linesep.join(lines)
def show_summary(session):
params = get_default_values()
params["session"] = session
input_data = {}
input_data["subject"] = config.get("content", "subject")
input_data["to_address"] = params["to_address"]
input_data["summary_text"] = get_summary_text(session)
return render("summary.html", input_data=input_data, **params)
def render_question(session, question):
input_data = {}
for answer in Answer.selectBy(session=session, question=question):
for key in ("text", "quality"):
input_data["option_%s_%s" % (answer.option.id, key)] = \
getattr(answer, key)
params = get_default_values()
params.update({"session": session,
"question": question,
"next_question": get_next_question(question),
"previous_question": get_previous_question(question),
})
return render("question.html", input_data=input_data, **params)
@bobo.query('/')
def update_question(bobo_request, session_id=None, question_id=None,
go_backward=False):
kwargs = bobo_request.params
session = get_session(session_id)
if not session:
session = Session()
params = get_default_values()
params["session"] = session
return render("start.html", **params)
question = None
@ -122,14 +321,14 @@ def show_question(session_id=None, question_id=None, go_backward=False,
opt_params[key] = kwargs[dict_key]
# at least one item was found
if opt_params:
answer = Answer.selectBy(session=session, question=question,
answers = Answer.selectBy(session=session, question=question,
option=option)
if not answer:
if answers.count() > 0:
answer = answers[0]
else:
# create new one
answer = Answer(session=session, question=question,
option=option)
else:
answer = answer[0]
# update one value
for key in opt_params:
setattr(answer, key, opt_params[key])
@ -139,31 +338,21 @@ def show_question(session_id=None, question_id=None, go_backward=False,
target_question = get_next_question(question)
if not target_question:
# special case: we are finished
return render("summary.html", session=session)
return show_summary(session)
if not target_question:
# start with the first question
target_question = Question.select().orderBy("weight")[0]
# populate the next question
input_data = {}
for answer in Answer.selectBy(session=session, question=target_question):
for key in ("text", "quality"):
input_data["option_%s_%s" % (answer.option.id, key)] = \
getattr(answer, key)
params.update({"session": session,
"question": target_question,
"next_question": get_next_question(target_question),
"previous_question": get_previous_question(target_question),
})
return render("question.html", input_data=input_data, **params)
return render_question(session, target_question)
@bobo.query('')
def redirect_startpage():
return bobo.redirect(BASE_DICT["base_url"])
return bobo.redirect(get_default_values()["base_url"])
# initialize the tables (do it only once!)
if True:
tables = (Session, Question, Option, Answer)
if RUN_FIRST_DB_INIT:
tables = (Session, Question, Option, Answer, Submission)
# drop all
drop_tables = list(tables)
drop_tables.reverse()