weitere Verbesserungen bis zur finalen Einsatzreife
This commit is contained in:
parent
59ccb5a87c
commit
68a8aca17b
12 changed files with 1210 additions and 109 deletions
|
@ -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")),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue