kasownik/web/webapp/views.py

374 lines
14 KiB
Python

# - * - coding=utf-8 - * -
# Copyright (c) 2015, Sergiusz Bazanski <q3k@q3k.org>
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are met:
#
# 1. Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import sys
import datetime
import json
import requests
import re
from email.mime.text import MIMEText
from subprocess import Popen, PIPE
from webapp import app, forms, User, db, models, mc, cache_enabled, admin_required
from flask.ext.login import login_user, login_required, logout_user, current_user
from flask import Response, request, redirect, flash, render_template, url_for, abort, g
import logic
import directory
import traceback
@app.route('/')
def stats():
return render_template('stats.html')
@app.route('/memberlist')
@login_required
def memberlist():
cache_key = 'kasownik-view-memberlist'
cache_data = mc.get(cache_key)
if not cache_data or not cache_enabled:
members = models.Member.get_members(True)
cache_data = []
for member in members:
element = member.get_status()
if not element['judgement']:
continue
cache_data.append(element)
mc.set(cache_key, cache_data)
return render_template('memberlist.html',
active_members=cache_data)
@app.route('/profile', methods=['POST', 'GET'])
@login_required
def self_profile():
member = models.Member.get_members(True).filter_by(username=current_user.username).first()
if not member:
abort(404)
status = member.get_status()
cn = directory.get_member_fields(g.ldap, member.username, 'cn')['cn']
#cesform = forms.ContactEmailSettingsForm(request.form)
#if request.method == "POST" and cesform.validate():
# pe = request.form['preferred_email']
# member.preferred_email = request.form['preferred_email']
#db.session.add(member)
#db.session.commit()
return render_template("admin_member.html", member=member, status=status,
cn=cn, admin=False)
@app.route("/admin")
@admin_required
@login_required
def admin_index():
members = [m.get_status() for m in models.Member.get_members(True)]
for member in members:
due = member['months_due']
if due < 1:
member['color'] = "00FF00"
elif due < 3:
member['color'] = "E0941B"
else:
member['color'] = "FF0000"
active_members = filter(lambda m: m['judgement'], members)
inactive_members = filter(lambda m: not m['judgement'], members)
diff = directory.get_ldap_group_diff(members)
if diff is not None:
flash("LDAP sync required")
return render_template("admin_index.html",
active_members=active_members,
inactive_members=inactive_members)
@app.route("/admin/ldapsync", methods=["POST", "GET"])
@admin_required
@login_required
def admin_ldap_sync():
members = [m.get_status() for m in models.Member.get_members(True)]
diff = directory.get_ldap_group_diff(members)
if diff is None:
return render_template("admin_ldap_sync.html", form=False)
form = forms.LDAPSyncForm(request.form)
form.fatty_to_add.choices = zip(diff['fatty_to_add'],diff['fatty_to_add'])
form.fatty_to_add.default = diff['fatty_to_add']
form.fatty_to_remove.choices = zip(diff['fatty_to_remove'],diff['fatty_to_remove'])
form.fatty_to_remove.default = diff['fatty_to_remove']
form.starving_to_add.choices = zip(diff['starving_to_add'],diff['starving_to_add'])
form.starving_to_add.default = diff['starving_to_add']
form.starving_to_remove.choices = zip(diff['starving_to_remove'],diff['starving_to_remove'])
form.starving_to_remove.default = diff['starving_to_remove']
form.process(request.form)
if request.method == "POST" and form.validate():
changes = {'fatty': {}, 'starving': {}}
changes['fatty']['add'] = form.fatty_to_add.data
changes['fatty']['remove'] = form.fatty_to_remove.data
changes['starving']['add'] = form.starving_to_add.data
changes['starving']['remove'] = form.starving_to_remove.data
directory.update_member_groups(g.ldap, changes)
return render_template("admin_ldap_sync.html", form=form)
@app.route("/admin/csv")
@admin_required
@login_required
def admin_csv():
members = []
for m in models.Member.get_members(True):
member = m.get_status()
if member['type'] == 'supporting':
continue
member['contact_email'] = m.get_contact_email()
member['cn'] = directory.get_member_fields(g.ldap, member['username'], 'cn')['cn']
members.append(member)
active_members = filter(lambda m: m['judgement'], members)
output = render_template("admin_csv.html", active_members=active_members)
return Response(output)
@app.route('/admin/member/<membername>')
@login_required
@admin_required
def admin_member(membername):
member = models.Member.get_members(True).filter_by(username=membername).first()
if not member:
abort(404)
status = member.get_status()
cn = directory.get_member_fields(g.ldap, member.username, 'cn')['cn']
return render_template("admin_member.html", member=member, status=status,
cn=cn, admin=True)
@app.route("/admin/member/<membername>/policy:<policy>")
@login_required
@admin_required
def admin_member_set_policy(membername,policy):
member = models.Member.query.filter_by(username=membername).first()
member.payment_policy = models.PaymentPolicy[policy].value
db.session.add(member)
db.session.commit()
return redirect(request.referrer)
@app.route("/admin/member/<membername>/membership:<membershiptype>")
@login_required
@admin_required
def admin_member_set_membership(membername,membershiptype):
member = models.Member.query.filter_by(username=membername).first()
member.type = models.MembershipType[membershiptype].name
db.session.add(member)
db.session.commit()
return redirect(request.referrer)
@app.route("/admin/member/add/<membershiptype>/<username>")
@login_required
@admin_required
def add_member(membershiptype, username):
member = models.Member(None, username, models.MembershipType[membershiptype].name, True)
db.session.add(member)
db.session.commit()
return "ok"
@app.route("/admin/match")
@login_required
@admin_required
def admin_match():
transfers_unmatched = logic.get_unmatched_transfers()
return render_template("match.html", transfers_unmatched=transfers_unmatched)
@app.route("/admin/match/auto", methods=["GET"])
@login_required
@admin_required
def admin_match_auto():
matched = 0
left = 0
transfers_unmatched = logic.get_unmatched_transfers()
affected_members = []
for transfer in transfers_unmatched:
matchability, member, months = transfer.get_matchability()
try:
print "[i] Matching transfer {} for {:.2f}PLN by member {}, {} months".format(transfer.id, transfer.amount/100, member.username, months)
except AttributeError:
print "[e] Member data invalid, WTF - {}".format(repr(member))
continue
if matchability == models.Transfer.MATCH_OK:
if len(member.transfers) > 0:
year, month = member.get_next_unpaid()
if None in (year, month):
print "[w] next_unpaid borked, skipping"
continue
else:
year, month = transfer.date.year, transfer.date.month
for m in range(months):
mt = models.MemberTransfer(None, year, month, transfer)
member.transfers.append(mt)
db.session.add(mt)
flash("Matched transfer {} for {:.2f}PLN to member {} for month {}-{}".format(transfer.id, transfer.amount/100, member.username, year, month))
year, month = member._yearmonth_increment((year,month))
matched += 1
affected_members.append(member)
else:
left += 1
db.session.commit()
for member in affected_members:
member.get_status(force_refresh=True)
flash("Matched %i, %i left" % (matched, left))
return redirect(url_for("admin_match"))
@app.route("/admin/match/manual", methods=["GET"])
@login_required
@admin_required
def match_manual():
transfers_unmatched = logic.get_unmatched_transfers()
return render_template("match_manual.html", transfers_unmatched=transfers_unmatched)
@app.route("/admin/match/<username>/<int:months>/<path:uid>")
@login_required
@admin_required
def match(username, uid, months):
member = models.Member.query.filter_by(username=username).first()
if not member:
return "no member"
transfer = models.Transfer.query.filter_by(uid=uid).first()
if not transfer:
return "no transfer"
for _ in range(months):
year, month = member.get_next_unpaid()
mt = models.MemberTransfer(None, year, month, transfer)
member.transfers.append(mt)
db.session.add(mt)
db.session.commit()
member.get_status(force_refresh=True)
return "ok, %i PLN get!" % transfer.amount
@app.route("/admin/match/", methods=["POST"])
@login_required
@admin_required
def match_user_transfer():
username = request.form["username"]
uid = request.form["uid"]
member = models.Member.query.filter_by(username=username).first()
if not member:
return "no such member! :("
transfer = models.Transfer.query.filter_by(uid=uid).first()
if not transfer:
return "no transfer"
return render_template("match_user_transfer.html", member=member, transfer=transfer)
@app.route("/admin/spam/", methods=["GET", "POST"])
@login_required
@admin_required
def sendspam():
now = datetime.datetime.now()
members = models.Member.query.filter_by(
active=True, payment_policy=models.PaymentPolicy.normal.value).all()
form = forms.SpamForm()
form.members.choices = [(member.id, member) for member in members]
form.members.default = [member.id for member in members]
form.process(request.form)
if request.method == 'POST' and form.validate():
spam = []
for member in members:
if member.id not in form.members.data:
continue
content = render_template(
'mailing/due.txt',
member=member,
status=member.get_status(),
transfers=member.transfers[:5],
now=now)
# Just ignore empty messages
if not content.strip():
continue
msg = MIMEText(content, "plain", "utf-8")
msg["From"] = "Faszysta Hackerspace'owy <fascist@hackerspace.pl>"
msg["Subject"] = "Stan składek na dzień %s" % now.strftime("%d/%m/%Y")
msg["To"] = member.get_contact_email()
spam.append(msg)
if form.dry_run.data:
readable = [
msg.as_string().split('\n\n')[0] + '\n\n' + msg.get_payload(decode=True) for msg in spam]
return Response('\n====\n'.join(readable), mimetype='text/text')
for msg in spam:
p = Popen(["/usr/sbin/sendmail", "-t"], stdin=PIPE)
p.communicate(msg.as_string())
flash('%d messages sent!' % len(spam))
return redirect(url_for('admin_index'))
return render_template('admin_spam.html', form=form)
@app.route("/login", methods=["POST", "GET"])
def login():
form = forms.LoginForm(request.form)
if request.method == "POST" and form.validate():
if requests.post("https://auth.hackerspace.pl/",
dict(login=form.username.data, password=form.password.data)).status_code == 200:
user = User(form.username.data)
login_user(user)
flash('Logged in succesfully')
if user.is_admin():
return redirect(request.args.get("next") or url_for("admin_index"))
else:
return redirect(request.args.get("next") or url_for("self_profile"))
return render_template("login.html", form=form)
@app.route("/logout")
@login_required
def logout():
logout_user()
return redirect(url_for("stats"))
from prometheus_client import multiprocess
from prometheus_client import generate_latest, CollectorRegistry, CONTENT_TYPE_LATEST
@app.route("/varz")
def metrics():
registry = CollectorRegistry()
multiprocess.MultiProcessCollector(registry)
data = generate_latest(registry)
return Response(data, mimetype=CONTENT_TYPE_LATEST)