374 lines
14 KiB
Python
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)
|