From 274d1790239f52440137cc922018a87af32a4ab7 Mon Sep 17 00:00:00 2001 From: Nico Geyso Date: Tue, 9 Dec 2014 23:59:54 +0100 Subject: Refactoring --- app/__init__.py | 48 ++--------- app/backend.py | 56 +++++++------ app/exts.py | 4 - app/forms.py | 30 +++---- app/main.py | 114 +++++++++++++++----------- app/static/style.css | 179 +++++++++++++++++++++++++++++++++++++++++ app/static/style_v3.css | 174 --------------------------------------- app/templates/403.html | 2 +- app/templates/404.html | 16 ++++ app/templates/_layout.html | 47 +++++++++++ app/templates/courses.html | 15 ++++ app/templates/exams.html | 29 +++++++ app/templates/index.html | 5 +- app/templates/layout.html | 48 ----------- app/templates/module_list.html | 15 ---- app/templates/module_show.html | 30 ------- app/templates/upload.html | 32 ++++---- 17 files changed, 426 insertions(+), 418 deletions(-) delete mode 100644 app/exts.py create mode 100644 app/static/style.css delete mode 100644 app/static/style_v3.css create mode 100644 app/templates/404.html create mode 100644 app/templates/_layout.html create mode 100644 app/templates/courses.html create mode 100644 app/templates/exams.html delete mode 100644 app/templates/layout.html delete mode 100644 app/templates/module_list.html delete mode 100644 app/templates/module_show.html diff --git a/app/__init__.py b/app/__init__.py index 6c6b08c..7022d2b 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -2,10 +2,6 @@ from flask import Flask, render_template, g from .main import main -from .backend import Storage -import sys -reload(sys) -sys.setdefaultencoding('utf-8') def create_app(config=None): """Creates the Flask app.""" @@ -13,10 +9,8 @@ def create_app(config=None): configure_app(app) configure_error_handlers(app) - init_app(app) - for blueprint in [main]: - app.register_blueprint(blueprint) + app.register_blueprint(main) return app @@ -36,43 +30,13 @@ def configure_app(app): app.logger.addHandler(file_handler) -def init_app(app): - import os - @app.before_request - def init(): - g.studies = {} - for i, study in enumerate(app.config['STUDIES'].items()): - abbr = study[0] - g.studies[abbr] = Storage(os.path.join('app', 'static','studies', abbr)) - - modules = app.config['STUDIES'][study[0]] - # extend module list with git values - for module in g.studies[abbr].get_modules(): - # check if module is already listed - if all(map(lambda (k,v): v != module, modules)): - slug = module.decode('ascii', errors='ignore') - app.config['STUDIES'][study[0]].append((slug, module)) - - -## populate Module-List -#fit = {} -#for i, study in enumerate(app.config['STUDIES'].items()): -# abbr = study[0] -# fit[abbr] = Fit(os.path.join('static','studies',abbr + '.git')) -# -# modules = app.config['STUDIES'][study[0]] -# # extend module list with git values -# for module in fit[abbr].get_modules(): -# # check if module is already listed -# if all(map(lambda (k,v): v != module, modules)): -# slug = module.decode('ascii', errors='ignore') -# app.config['STUDIES'][study[0]].append((slug, module)) - def configure_error_handlers(app): @app.route('/forbidden') - @app.errorhandler(400) @app.errorhandler(403) + def forbidden(e): + return render_template('403.html', error=e), e.code + + @app.errorhandler(400) @app.errorhandler(404) def errorhandler(e): - return render_template('error.html', error=e), e.code - + return render_template('404.html', error=e), e.code diff --git a/app/backend.py b/app/backend.py index 6c59edd..0dbebb8 100644 --- a/app/backend.py +++ b/app/backend.py @@ -2,38 +2,48 @@ import os import magic +from functools import partial class Storage: - def __init__(self, root_path): - self.root = root_path + def __init__(self, path_rel, path_app): + self.path = path_app + self.root = path_rel def _join(self, *arg): - return os.path.join(self.root, *arg) + return os.path.join(self.path, *arg) - def get_file(self, module, year, name): - with open(self._join(module, year, name), 'r') as f: - data = f.read() - mime = magic.Magic(mime=True) - mime_type = mime.from_buffer(data[:1024]) - return mime_type, data + def get_courses(self): + """ Lists all courses of a study """ + return [o.decode('utf-8') for o in os.listdir(self.path) + if os.path.isdir(self._join(o))] - def get_modules(self): - return [o for o in os.listdir(self.root) if os.path.isdir(self._join(o))] + def exam_exists(self, course, year, name): + """ Exists if an exam (file) exists """ + return os.path.exists(self._join(course, year, name)) - def get_module(self, module): - for root, dirs, files in os.walk(self._join(module)): + def add_exam(self, course, year, filename, data): + # create course dir with year if it does not exist already + dir_name = self._join(course, year) + if not os.path.exists(dir_name): + os.makedirs(dir_name) + + # save exam + path = self._join(course, year, filename) + with open(path, 'wb') as f: + f.write(data) + + def get_exams(self, name): + """ Lists all exams of a given course """ + # loop over all directories which do not contain any subdirs + for root, dirs, files in os.walk(self._join(name)): if len(dirs) == 0: + # metainformation is encoded in path: course/year/exam.pdf splitted = root.split(os.path.sep) if len(splitted) > 1: year = splitted[-1] - module = splitted[-2] + course = splitted[-2] if year.isdigit(): - yield (year, files) - - def add_file(self, module, year, filename, data): - dir_name = self._join(module, year) - if not os.path.exists(dir_name): - os.makedirs(dir_name) - path = self._join(module, year, filename) - with open(path, 'wb') as f: - f.write(data) + # yield entries as tuples (name, path) grouped by years + func = partial(os.path.join, self.root, course, year) + entries = [(f, func(f)) for f in files] + yield (year, entries) diff --git a/app/exts.py b/app/exts.py deleted file mode 100644 index 7d42edc..0000000 --- a/app/exts.py +++ /dev/null @@ -1,4 +0,0 @@ -# -*- coding: utf-8 -*- - -from flask.ext.sqlalchemy import SQLAlchemy -db = SQLAlchemy() diff --git a/app/forms.py b/app/forms.py index e562df2..2332179 100644 --- a/app/forms.py +++ b/app/forms.py @@ -6,36 +6,32 @@ from flask.ext.wtf import Form from wtforms import TextField, FileField, SelectField, validators from wtforms.validators import ValidationError +year_start = date.today().year +year_end = current_app.config['FORM_START_YEAR']-1 +choices = [(str(x),x) for x in xrange(year_start, year_end, -1)] class UploadForm(Form): """ Upload Form class for validation """ study = TextField('Studiengang') exam = FileField('Klausur') - module = SelectField('Kurs') - module_new = TextField('Modulname', validators=[validators.Optional(), - validators.Length(min=5)]) - year = SelectField( - 'Jahr', - validators=[validators.Required()], - choices = [ (str(x),x) for x in - #xrange(date.today().year, current_app.config['FORM_START_YEAR']-1, -1) - xrange(date.today().year, 2000, -1) - ] - ) + course = SelectField('Kurs') + course_new = TextField('Modulname', validators=[validators.Optional(), + validators.Length(min=5)]) + year = SelectField('Jahr', validators=[validators.Required()], + choices = choices) def validate_exam(form, field): exts = current_app.config['ALLOWED_EXTENSIONS'] ext = map(field.data.filename.endswith, exts) - if not any(ext): raise ValidationError(u'Ungültiger Dateityp') if field.data.content_length > current_app.config['MAX_CONTENT_LENGTH']: raise ValidationError(u'Zu große Datei') - def validate_module(form, field): - modules = dict(current_app.config['STUDIES'][form.study.data]) - data = form.module.data - if data not in modules or data == '': - raise ValidationError(u'Bitte wähle ein Modul!') + def validate_course(form, field): + courses = dict(current_app.config['STUDIES'][form.study.data]) + data = form.course.data + if data not in courses or data == '': + raise ValidationError(u'Bitte wähle einen Kurs!') diff --git a/app/main.py b/app/main.py index cf6a99e..ef4e549 100644 --- a/app/main.py +++ b/app/main.py @@ -4,32 +4,59 @@ import os, sys from flask import Blueprint, render_template, request, flash, redirect,\ url_for, current_app, g from werkzeug import secure_filename -from .forms import UploadForm +from wtforms.validators import ValidationError +from .backend import Storage main = Blueprint('main', __name__) +def get_studies(): + """ + Add all existing courses of backend to study list. + This list is used to fill form values with courses. + """ + studies = getattr(g, '_studies', None) + if studies is None: + studies = g._studies = {} + it = current_app.config['STUDIES'].items() + for i, (abbr, courses) in enumerate(it): + path_rel = os.path.join('studies', abbr) + path_app = os.path.join('app', 'static', path_rel) + studies[abbr] = Storage(path_rel, path_app) + + # iterate over all courses in our backend + for course in g._studies[abbr].get_courses(): + # check if course is already listed + entry = (course.encode('ascii', errors='ignore'), course) + if entry not in courses: + current_app.config['STUDIES'][abbr].append(entry) + + current_app.config['STUDIES'][abbr].sort() + return studies + @main.route('//upload/', methods=['GET', 'POST']) -@main.route('//upload/', methods=['GET', 'POST']) -def upload(study, module = None): +@main.route('//upload/', methods=['GET', 'POST']) +def upload(study, course = None): + from .forms import UploadForm form = UploadForm() form.study.data = study - form.module.choices = current_app.config['STUDIES'][study] - if 'new' not in dict(form.module.choices): - form.module.choices.append(('', u'---')) - form.module.choices.append(('new', u'neues Modul hinzufügen')) - + # dynamically fill form values + form.course.choices = current_app.config['STUDIES'][study] + if 'new' not in dict(form.course.choices): + form.course.choices.append(('', u'---')) + form.course.choices.append(('new', u'neuen Kurs hinzufügen')) if form.validate_on_submit(): - if form.module.data == 'new': - module = form.module_new.data - slug = module.encode('ascii', errors='ignore') - i = len(current_app.config['STUDIES'][study]) - 2 - current_app.config['STUDIES'][study].insert(i, (slug,module)) + if form.course.data == 'new': + course = form.course_new.data + slug = course.encode('ascii', errors='ignore') + current_app.config['STUDIES'][study].append((slug,course)) + current_app.config['STUDIES'][study].sort() + else: - module = dict(current_app.config['STUDIES'][study])[form.module.data] + course = dict(current_app.config['STUDIES'][study])[form.course.data] year = form.year.data filename = secure_filename(form.exam.data.filename) @@ -38,45 +65,42 @@ def upload(study, module = None): except: data = form.exam.data.stream.read() - g.studies[study].add_file(module, year, filename, data) - flash("Datei %s gespeichert." % filename) - - return redirect(url_for('.study_index', study = study, module = module)) - - try: form.module.data = [k for (k,v) in form.module.choices if v == module][0] - except: pass - - return render_template('upload.html', - study = study, form = form, module=module) - + backend = get_studies()[study] + if not backend.exam_exists(course, year, filename): + backend.add_exam(course, year, filename, data) + flash(u'Datei %s gespeichert.' % filename) + return redirect(url_for('.courses_show', study = study, course = course)) + else: + flash(u'Datei mit gleichem Namen existiert schon!', 'error') + try: + form.course.data = [k for (k,v) in form.course.choices if v == course][0] + except: + pass -@main.route('//files///') -def study_show(study, module, year, filename): - mime_type, data = g.studies[study].get_file(module,year,filename) - header = { 'Content-Type' : mime_type } - return data, 200, header + return render_template('upload.html', study = study, form = form, + course = course) -@main.route('//modules/') -@main.route('//modules/') -def study_index(study, module=None): - if module: - entries = sorted(g.studies[study].get_module(module), reverse=True) - return render_template('module_show.html', - study = study, module=module, entries=entries - ) +@main.route('//courses/') +@main.route('//courses/') +def courses_show(study, course = None): + """ Lists all courses or exams for a course """ + backend = get_studies()[study] + if course: + entries = sorted(backend.get_exams(course), reverse = True) + return render_template('exams.html', study = study, course = course, + entries=entries) - modules = g.studies[study].get_modules() - return render_template('module_list.html', study = study, modules=modules) + courses = sorted(backend.get_courses()) + return render_template('courses.html', study = study, courses = courses) @main.route('/') def index(): + """ Lists all course of studies """ get_img_path = lambda x: os.path.join('studies', x, 'logo.png') - studies = [(name,get_img_path(name)) for name,m in current_app.config['STUDIES'].items()] + it = current_app.config['STUDIES'].items() + studies = [(name, get_img_path(name)) for name,m in it] - return render_template( - 'index.html', - studies = studies - ) + return render_template('index.html', studies = studies) diff --git a/app/static/style.css b/app/static/style.css new file mode 100644 index 0000000..7075a7a --- /dev/null +++ b/app/static/style.css @@ -0,0 +1,179 @@ +html, body { + padding: 0; + margin: 0; + height: 100%; + font: 13px/1.5 'Helvetica Neue', Arial, 'Liberation Sans', FreeSans, sans-serif; +} + +body { + background-color: #333; +} + +#content { + background-color: #fff; + clear: both; + margin: 0px auto; + width: 1000px; + padding: 20px; +} + + +#header { + background-color: #fff; + margin: 0px auto 10px auto; + float: left; + width: 100%; +} + + + +footer { + clear: both; + margin: 0px auto; + width: 920px; + padding: 20px; + text-align: center; + color: #fff; +} + +footer a, footer a:visited { + color: #fff; +} + +#sub-header { + clear: both; + width:100%; +} + +#container { + float: left; +} + +#header h1, #header p, #header form { + padding: 20px 0px 20px 20px; +} + +#header h1 { + display: inline-block; + width: 100%; + text-align: center; + margin: 0px; +} + +#header h1 small { + color: #777; + font-size: 50%; +} + + +#filelist { + list-style: none; +} + +#container p { + margin-bottom: 0px; +} + +label { + float: left; + width: 200px; +} + +ul.flashes { + margin: 0px auto; + width: 40%; +} + +ul.flashes li { + list-style-type: none; + padding: 5px; + padding: 0px; + text-align: center; + border: 2px solid green; + background-color: #0D5; +} + +ul.flashes li.error { + border: 2px solid red; + background-color: rgb(255, 116, 116); +} + +a, a:visited { + font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; + color: #0069D6; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +#sub-header p { + float: right; + text-align: right; +} + +span.error { + font-weight: bold; + color: #F00; + display: block; + margin-left: 200px; +} + +select { + font-family: monospace; + width: 350px; + border: 1px solid #ccc; + padding: 5px 5px; + background-color: #fff; +} + +#course_new { + width: 200px; + margin-left: 10px; + padding: 5px 5px; + border: 1px solid #ccc; +} + +#studies { + margin: auto; + text-align: center; + padding: 40px 0px 60px 0px; +} + +#studies ul { + display: inline-block; +} + +#studies li { + float: left; + list-style-type: none; + text-align: center; + width: 200px; +} + +#studies li a { + margin: 0px 20px; + padding: 10px; + border: 1px solid #ccc; + display: block; +} + +#studies li a:hover { + background-color: #ccc; + border: 1px solid #000; +} + +#studies li img{ + display: block; + margin: 0px auto; +} + +#upload form, #upload div { + display: inline-block; +} + +#upload div { + vertical-align: top; + padding-left: 30px; +} diff --git a/app/static/style_v3.css b/app/static/style_v3.css deleted file mode 100644 index 6907658..0000000 --- a/app/static/style_v3.css +++ /dev/null @@ -1,174 +0,0 @@ -html, body { - padding: 0; - margin: 0; - height: 100%; - font: 13px/1.5 'Helvetica Neue', Arial, 'Liberation Sans', FreeSans, sans-serif; -} - -body { - background-color: #333; -} - -#content { - background-color: #fff; - clear: both; - margin: 0px auto; - width: 1000px; - padding: 20px; -} - - -#header { - background-color: #fff; - margin: 0px auto 10px auto; - float: left; - width: 100%; -} - - - -footer { - clear: both; - margin: 0px auto; - width: 920px; - padding: 20px; - text-align: center; - color: #fff; -} - -footer a, footer a:visited { - color: #fff; -} - -#sub-header { - clear: both; - width:100%; -} - -#container { - float: left; -} - -#header h1, #header p, #header form { - padding: 20px 0px 20px 20px; -} - -#header h1 { - display: inline-block; - width: 100%; - text-align: center; - margin: 0px; -} - -#header h1 small { - color: #777; - font-size: 50%; -} - - -#filelist { - list-style: none; -} - -#container p { - margin-bottom: 0px; -} - -label { - float: left; - width: 200px; -} - -ul.flashes { - margin: 0px auto; - width: 40%; -} - -ul.flashes li { - list-style-type: none; - border: 2px solid green; - background-color: #0D5; - padding: 5px; - padding: 0px; - text-align: center; -} - -a, a:visited { - font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; - color: #0069D6; - text-decoration: none; -} - -a:hover { - text-decoration: underline; -} - -#sub-header p { - float: right; - text-align: right; -} - -span.error { - font-weight: bold; - color: #F00; - display: block; - margin-left: 200px; -} - -select { - font-family: monospace; - width: 350px; - border: 1px solid #ccc; - padding: 5px 5px; - background-color: #fff; -} - -#module_new { - width: 200px; - margin-left: 10px; - padding: 5px 5px; - border: 1px solid #ccc; -} - -#studies { - margin: auto; - text-align: center; - padding: 40px 0px 60px 0px; -} - -#studies ul { - display: inline-block; -} - -#studies li { - float: left; - list-style-type: none; - text-align: center; - width: 200px; -} - -#studies li a { - margin: 0px 20px; - padding: 10px; - border: 1px solid #ccc; - display: block; -} - -#studies li a:hover { - background-color: #ccc; - border: 1px solid #000; -} - -#studies li img{ - display: block; - margin: 0px auto; -} - -#upload form, #upload div { - display: inline-block; -} - -#upload div { - vertical-align: top; - padding-left: 30px; -} diff --git a/app/templates/403.html b/app/templates/403.html index 5f2317f..1569421 100644 --- a/app/templates/403.html +++ b/app/templates/403.html @@ -1,4 +1,4 @@ -{% extends "layout.html" %} +{% extends "_layout.html" %} {% block body %}

Fehler 403 - Zugang verboten

diff --git a/app/templates/404.html b/app/templates/404.html new file mode 100644 index 0000000..a1526ff --- /dev/null +++ b/app/templates/404.html @@ -0,0 +1,16 @@ +{% extends "_layout.html" %} +{% block body %} +

Fehler 403 - Zugang verboten

+ +
+
Code
+
{{error.code}}
+ +
Beschreibung
+
{{error.description}}
+
+ +

+ zur Startseite +

+{% endblock %} diff --git a/app/templates/_layout.html b/app/templates/_layout.html new file mode 100644 index 0000000..5eaef3f --- /dev/null +++ b/app/templates/_layout.html @@ -0,0 +1,47 @@ + + + + + + + Fit + + + + +
+ +
+ {% if study %} +

+ {% if not request.base_url.endswith(url_for('.upload', study=study, course= course))%} + {% if not request.base_url.endswith(url_for('forbidden')) %} + neue Klausur hochladen + {% endif %} + {% else %} + zurück + {% endif %} +

+

{{study.capitalize()}}

+ {% endif %} + +
    + {% with messages = get_flashed_messages(with_categories=true) %} + {% for category, message in messages %} +
  • {{ message }}
  • + {% endfor %} + {% endwith %} +
+
+ + {% block body %}{% endblock %} + +
+ + + + diff --git a/app/templates/courses.html b/app/templates/courses.html new file mode 100644 index 0000000..f315e11 --- /dev/null +++ b/app/templates/courses.html @@ -0,0 +1,15 @@ +{% extends "_layout.html" %} +{% block body %} +

Klausuren

+ +
    + {% for course in courses %} +
  • + {{course}} +
  • + {% else %} +
  • Keine Klausuren bisher hochgeladen!
  • + {% endfor %} +
+ +{% endblock %} diff --git a/app/templates/exams.html b/app/templates/exams.html new file mode 100644 index 0000000..7e4af54 --- /dev/null +++ b/app/templates/exams.html @@ -0,0 +1,29 @@ +{% macro render_courses_list(courses, entries) %} +
    +{% for year,files in entries %} +
  • + {{year}} + +
  • +{% else %} +
  • Keine Einträge bisher
  • +{% endfor %} +
+{% endmacro %} + +{% extends "_layout.html" %} +{% block body %} +
+

{{courses}}

+ + {{ render_courses_list(courses, entries)}} +
+{% endblock %} diff --git a/app/templates/index.html b/app/templates/index.html index 9d090c3..8ab2c06 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -1,4 +1,4 @@ -{% extends "layout.html" %} +{% extends "_layout.html" %} {% block body %}

Studiengänge

@@ -6,7 +6,7 @@ - {% endblock %} diff --git a/app/templates/layout.html b/app/templates/layout.html deleted file mode 100644 index 14ec7c5..0000000 --- a/app/templates/layout.html +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - Fit - - - - -
- -
- {% if study %} -

- {% if not request.base_url.endswith(url_for('.upload', study=study, module = module))%} - {% if not request.base_url.endswith(url_for('errorhandler')) %} - neue Klausur hochladen - {% endif %} - {% else %} - zurück - {% endif %} -

-

{{study.capitalize()}}

- {% endif %} - -
    - {% with messages = get_flashed_messages() %} - {% for message in messages %} -
  • {{ message }}
  • - {% endfor %} - {% endwith %} -
-
- - {% block body %}{% endblock %} - -
- - - - diff --git a/app/templates/module_list.html b/app/templates/module_list.html deleted file mode 100644 index 9469020..0000000 --- a/app/templates/module_list.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "layout.html" %} -{% block body %} -

Klausuren

- -
    - {% for module in modules %} -
  • - {{module}} -
  • - {% else %} -
  • Keine Klausuren bisher hochgeladen!
  • - {% endfor %} -
- -{% endblock %} diff --git a/app/templates/module_show.html b/app/templates/module_show.html deleted file mode 100644 index e31da24..0000000 --- a/app/templates/module_show.html +++ /dev/null @@ -1,30 +0,0 @@ -{% macro render_module_list(module, entries) %} -
    -{% for year,files in entries %} -
  • - {{year}} - -
  • -{% else %} -
  • Keine Einträge bisher
  • -{% endfor %} -
-{% endmacro %} - -{% extends "layout.html" %} -{% block body %} -
-

{{module}}

- - {{ render_module_list(module, entries)}} -
- -{% endblock %} diff --git a/app/templates/upload.html b/app/templates/upload.html index 0e00354..e19a113 100644 --- a/app/templates/upload.html +++ b/app/templates/upload.html @@ -20,17 +20,17 @@ {{ render_fields(field, None) }} {% endmacro %} -{% extends "layout.html" %} +{% extends "_layout.html" %} {% block body %}

neue Klausur hochladen

+ action="{{url_for('.upload', study=study, course=course)}}"> {{ form.csrf_token }} {{ render_field(form.exam) }} - {{ render_fields(form.module, form.module_new, placeholder='Modulname') }} + {{ render_fields(form.course, form.course_new, placeholder='Modulname') }} {{ render_field(form.year) }}

@@ -38,39 +38,39 @@

-
+