diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000000000000000000000000000000000000..331cb8dc1d129081d470d64a128e02a880548a05 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length=160 diff --git a/config.example.py b/config.example.py index aee4db25e7218591c68903e753ade802f3ad5488..6015434aacea46ad4b27ad5e853856fae2b1e365 100644 --- a/config.example.py +++ b/config.example.py @@ -1,10 +1,11 @@ +import datetime + SECRET_KEY = b'' SQLALCHEMY_DATABASE_URI = '' DEBUG = True PORT = 5001 SESSION_COOKIE_SECURE = True -import datetime REMEMBER_COOKIE_NAME = 'remember_token' REMEMBER_COOKIE_DURATION = datetime.timedelta(30) REMEMBER_COOKIE_DOMAIN = None @@ -28,11 +29,8 @@ BRANDING_APP_URL = 'https://lehrpreis.example.com' BRANDING_ORG_NAME = 'Example Oragnization' BRANDING_CONTACT = 'it@example-organisation.com' BRANDING_LOGO = 'logo.png' -BRANDING_INFORMATION = {'de': '''<h3>Die Regeln</h3> -<p>Du kannst jeden Dozenten dessen Lehre dir besonders gefallen hat nominieren. Vorraussetzung ist, dass dieser aus der Informatik stammt. Also Mathe-Service-Dozenten sind nicht zugelassen. Auch Dozenten aus Anwendungsfächern werden nicht geehrt.</p> -<p>Die Kategorie sollte auch passen. Professoren und Post-Docs, die eigenständig eine Veranstaltung betreuen, gehören in die Kategorie "Prof". Mitarbeiter, die Übungen betreuen, Vorlesungen nur vertretungsweise halten oder sonstwie unterstützend tätig sind in die Kategorie "WM".</p> -<p>Danke für deine Aufmerksamkeit!</p>''', - 'en': '<p>This text was not yet translated.</p>'} +BRANDING_INFORMATION = {'de': '<p>Dieser Text wurde bisher nicht übersetzt.</p>', + 'en': '<p>This text was not yet translated.</p>'} MAIL_ENABLED = True MAIL_ADDRESS = 'committee@example.com' diff --git a/lehrpreis.py b/lehrpreis.py index ae02d895241144691483278b06a28e63e84297c1..d83d67a5c3725acf08ab0d48799b3f47c7576a18 100755 --- a/lehrpreis.py +++ b/lehrpreis.py @@ -1,23 +1,20 @@ #!/usr/bin/env python3 -from flask import Flask, Response, redirect, render_template, flash, url_for, session, request +from flask import Flask, redirect, render_template, flash, url_for, session, request from flask_sqlalchemy import SQLAlchemy from flask_login import login_required, current_user, LoginManager, login_user, logout_user, UserMixin from flask_wtf import FlaskForm as Form -from flask_babel import Babel, gettext, ngettext, format_date, lazy_gettext, force_locale +from flask_babel import Babel, gettext, lazy_gettext, force_locale from flask_babel import refresh as babel_refresh from wtforms import StringField, BooleanField, PasswordField, DateField, SelectField from wtforms.validators import Optional, InputRequired import smtplib -import email from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart import ssl import ldap3 -from ldap3.utils.dn import parse_dn from sqlalchemy import String, Integer, Boolean, Date, ForeignKey -from sqlalchemy.orm import relationship, backref -import re +from sqlalchemy.orm import relationship import functools import datetime import config @@ -30,15 +27,16 @@ login_manager.init_app(app) login_manager.login_view = '.login' login_manager.login_message = gettext('Please log in to access this page.') login_manager.login_message_category = 'alert-info' -#login_manager.session_protection = 'strong' # basic +# login_manager.session_protection = 'strong' # basic babel = Babel(app) + @babel.localeselector def get_locale(): if not ('lang' in session): session['lang'] = request.accept_languages.best_match(['de', 'en']) return session['lang'] -app.jinja_env.globals['get_locale'] = get_locale + # http://flask.pocoo.org/snippets/120/ class back(): @@ -60,8 +58,12 @@ class back(): @staticmethod def redirect(default=default_view, cookie=cookie): return redirect(back.url(default, cookie)) + + +app.jinja_env.globals['get_locale'] = get_locale back = back() + @app.route('/switch-language') def switch_lang(): lang = get_locale() @@ -70,6 +72,7 @@ def switch_lang(): flash(gettext('The language was changed to English.'), 'alert-success') return back.redirect() + class User(UserMixin): def __init__(self, name, domain): self.user = name @@ -77,22 +80,23 @@ class User(UserMixin): def get_id(self): return self.user - + @staticmethod def get(user_id): return User(user_id, '') + @login_manager.user_loader def load_user(user_id): return User.get(user_id) + @app.route('/login', methods=['GET', 'POST']) def login(): form = LoginForm() if form.validate_on_submit(): - auth_manager = ADManager(host=config.AD_HOST, domain=config.AD_DOMAIN, - user_dn=config.AD_USER_DN, group_dn=config.AD_GROUP_DN, - ca_cert=config.AD_CA_CERT) + auth_manager = ADManager(host=config.AD_HOST, domain=config.AD_DOMAIN, user_dn=config.AD_USER_DN, + group_dn=config.AD_GROUP_DN, ca_cert=config.AD_CA_CERT) if auth_manager.authenticate(form.username.data, form.password.data): user = User(form.username.data, auth_manager.domain) login_user(user, remember=form.remember_me.data) @@ -103,6 +107,7 @@ def login(): return back.redirect() return render_template('login.html', form=form) + @app.route("/logout") @login_required def logout(): @@ -110,15 +115,13 @@ def logout(): session.pop('groups') return back.redirect() + class ADManager: - def __init__(self, host, domain, user_dn, group_dn, - port=636, use_ssl=True, ca_cert=None): + def __init__(self, host, domain, user_dn, group_dn, port=636, use_ssl=True, ca_cert=None): tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED) if ca_cert is not None: - tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED, - ca_certs_file=ca_cert) - self.server = ldap3.Server(host, port=port, use_ssl=use_ssl, - tls=tls_config) + tls_config = ldap3.Tls(validate=ssl.CERT_REQUIRED, ca_certs_file=ca_cert) + self.server = ldap3.Server(host, port=port, use_ssl=use_ssl, tls=tls_config) self.domain = domain self.user_dn = user_dn self.group_dn = group_dn @@ -140,7 +143,7 @@ class ADManager: for result in user_reader.search(): if result.userAccountControl in [546, 514, 66050, 66082]: return False - if not config.USER_GROUP in self.groups(username, password): + if config.USER_GROUP not in self.groups(username, password): return False return True except ldap3.core.exceptions.LDAPSocketOpenError: @@ -153,6 +156,7 @@ class ADManager: name_filter = "cn:={}".format(username) user_reader = ldap3.Reader(connection, obj_def, self.user_dn, name_filter) group_def = ldap3.ObjectDef("group", connection) + def _yield_recursive_groups(group_dn): group_reader = ldap3.Reader(connection, group_def, group_dn, None) for entry in group_reader.search(): @@ -167,10 +171,11 @@ class ADManager: connection = self.prepare_connection() connection.bind() obj_def = ldap3.ObjectDef("group", connection) - group_reader = ldap3.Reader(connection, obj_def, self.group_dn) + reader = ldap3.Reader(connection, obj_def, self.group_dn) for result in reader.search(): yield result.name.value + class Instance(db.Model): __tablename__ = 'instances' id = db.Column(Integer, nullable=False, unique=True, index=True, primary_key=True) @@ -180,6 +185,7 @@ class Instance(db.Model): categories = db.Column(String(255), nullable=False) nominees = relationship('Nomination', foreign_keys='Nomination.instance_id', backref='instance', cascade="all, delete-orphan") + class Nomination(db.Model): __tablename__ = 'nominations' id = db.Column(Integer, nullable=False, unique=True, index=True, primary_key=True) @@ -189,22 +195,25 @@ class Nomination(db.Model): category = db.Column(String(255), nullable=False) instance_id = db.Column(Integer, ForeignKey('instances.id'), nullable=False) + @app.route('/', methods=['GET', 'POST']) @back.anchor def main(): today = datetime.date.today() - inst = Instance.query.filter_by(enabled=True).filter(Instance.date > today).order_by(Instance.date.asc()).first() or Instance.query.filter(Instance.date > today).order_by(Instance.date.asc()).first() + inst = Instance.query.filter_by(enabled=True).filter(Instance.date > today).order_by( + Instance.date.asc()).first() or Instance.query.filter(Instance.date > today).order_by( + Instance.date.asc()).first() form = NominateForm() - form.category.choices = [ (category.strip(), category.strip()) for category in inst.categories.split(';') ] if inst else [] + form.category.choices = [(category.strip(), category.strip()) for category in inst.categories.split(';')] if inst else [] if form.validate_on_submit(): if inst is None or not (inst.enabled or current_user.is_authenticated()): flash(gettext('You cannot nominate right now. Sorry.'), 'alert-danger') return redirect(url_for('.main')) - nom = Nomination(person = form.person.data, - module = form.module.data, - reason = form.reason.data, - category = form.category.data, - instance_id = inst.id) + nom = Nomination(person=form.person.data, + module=form.module.data, + reason=form.reason.data, + category=form.category.data, + instance_id=inst.id) db.session.add(nom) db.session.commit() flash(gettext('We received and saved your nomination. Thank you.'), 'alert-success') @@ -220,6 +229,7 @@ def main(): s.send_message(mail) return render_template('main.html', inst=inst, form=form) + @app.route('/manage', methods=['GET', 'POST']) @back.anchor @login_required @@ -227,16 +237,17 @@ def manage(): insts = Instance.query.all() form = InstanceForm() if form.validate_on_submit(): - inst = Instance(name = form.name.data, - enabled = bool(form.enabled.data), - date = form.date.data, - categories = form.categories.data) + inst = Instance(name=form.name.data, + enabled=bool(form.enabled.data), + date=form.date.data, + categories=form.categories.data) db.session.add(inst) db.session.commit() flash(gettext('The instance was created.'), 'alert-success') return redirect(url_for('.manage')) return render_template('manage.html', insts=insts, form=form) + @app.route('/manage/nominees/<int:instid>') @back.anchor @login_required @@ -245,6 +256,7 @@ def nominees(instid): nominees = Nomination.query.filter_by(instance_id=instid).all() return render_template('nominees.html', noms=nominees, inst=inst) + @app.route('/manage/modify/<int:instid>', methods=['GET', 'POST']) @back.anchor @login_required @@ -261,6 +273,7 @@ def modify(instid): return redirect(url_for('.manage')) return render_template('modify.html', inst=inst, form=form) + @app.route('/manage/enable/<int:instid>', methods=['GET', 'POST']) @back.anchor @login_required @@ -274,6 +287,7 @@ def enable(instid): return redirect(url_for('.manage')) return render_template('enable.html', inst=inst, form=form) + @app.route('/manage/disable/<int:instid>', methods=['GET', 'POST']) @back.anchor @login_required @@ -287,6 +301,7 @@ def disable(instid): return redirect(url_for('.manage')) return render_template('disable.html', inst=inst, form=form) + @app.route('/manage/delete/<int:instid>', methods=['GET', 'POST']) @back.anchor @login_required @@ -306,27 +321,38 @@ class LoginForm(Form): password = PasswordField(lazy_gettext('Password'), validators=[InputRequired(lazy_gettext('Please enter your password here.'))]) remember_me = BooleanField(lazy_gettext('Keep me logged in.'), validators=[Optional()]) + class NominateForm(Form): person = StringField(lazy_gettext('Nominee'), validators=[InputRequired(lazy_gettext('Please enter the name of the person you want to nominate.'))]) category = SelectField(lazy_gettext('Category'), validators=[InputRequired(lazy_gettext('Please select the category you want to nominate the person in.'))]) module = StringField(lazy_gettext('Module'), validators=[InputRequired(lazy_gettext('Please enter the module/course/lecture that was outstanding.'))]) reason = StringField(lazy_gettext('Reason'), validators=[InputRequired(lazy_gettext('Please enter a reason why the nominee shall be awarded.'))]) - tos = BooleanField(lazy_gettext('The nominee is eligible for the award.'), validators=[InputRequired(lazy_gettext('Please verify the eligibility beforehand.'))]) + tos = BooleanField(lazy_gettext('The nominee is eligible for the award.'), validators=[InputRequired( + lazy_gettext('Please verify the eligibility beforehand.'))]) + class InstanceForm(Form): name = StringField(lazy_gettext('Name'), validators=[InputRequired(lazy_gettext('Please enter the instances name, e.g. a semester identifier.'))]) enabled = BooleanField(lazy_gettext('Enable'), validators=[Optional()]) - date = DateField(lazy_gettext('Award Date'), description=lazy_gettext('Please use the format YYYY-MM-DD.'), validators=[InputRequired(lazy_gettext('Please enter the date this is instance is awarded.'))]) - categories = StringField(lazy_gettext('Categories'), description=lazy_gettext('Please enter one or more nomination categories and seperate them using a semicolon.'), validators=[InputRequired(lazy_gettext('Please enter some categories.'))]) + date = DateField(lazy_gettext('Award Date'), + description=lazy_gettext('Please use the format YYYY-MM-DD.'), + validators=[InputRequired(lazy_gettext('Please enter the date this is instance is awarded.'))]) + categories = StringField(lazy_gettext('Categories'), + description=lazy_gettext('Please enter one or more nomination categories and seperate them using a semicolon.'), + validators=[InputRequired(lazy_gettext('Please enter some categories.'))]) + class EnableForm(Form): pass + class DisableForm(Form): pass + class DeleteForm(Form): pass + if __name__ == '__main__': app.run(port=config.PORT)