From f5d4c0c6e1c259ce8ce737f2d644120ce0078bd3 Mon Sep 17 00:00:00 2001 From: radex Date: Mon, 8 Jul 2024 22:01:00 +0200 Subject: [PATCH] *: run black --- fetch/banking-pekaobiznes.py | 146 ++++++++++++++--------- fetch/models.py | 2 +- web/api-client.py | 4 +- web/manage.py | 4 +- web/webapp/admin.py | 182 ++++++++++++++++++---------- web/webapp/api.py | 37 +++--- web/webapp/authutils.py | 7 +- web/webapp/commands.py | 38 +++--- web/webapp/directory.py | 117 +++++++++++------- web/webapp/forms.py | 23 +++- web/webapp/logic.py | 13 +- web/webapp/models.py | 225 +++++++++++++++++++++-------------- web/webapp/views.py | 48 +++++--- web/webapp/wsgi.py | 8 +- 14 files changed, 541 insertions(+), 313 deletions(-) diff --git a/fetch/banking-pekaobiznes.py b/fetch/banking-pekaobiznes.py index 7c32df5..9865e82 100755 --- a/fetch/banking-pekaobiznes.py +++ b/fetch/banking-pekaobiznes.py @@ -153,7 +153,11 @@ class CAMT052Parser: transfer.type = "IN" transfer.index = 1 - transfer.uid = txdtls.find("ns:Refs", ns).find("ns:InstrId", ns).text + '.' + transfer.type + transfer.uid = ( + txdtls.find("ns:Refs", ns).find("ns:InstrId", ns).text + + "." + + transfer.type + ) transfer.on_account = on_account transfer.raw = ET.tostring(entry).decode() transfer.amount = int(Decimal(amt.text) * 100) @@ -169,6 +173,7 @@ class CAMT052Parser: class PekaoClient: resp = None + def __init__(self, config): self.config = config self.logger = logging.getLogger(self.__class__.__name__) @@ -219,7 +224,9 @@ class PekaoClient: "MaskLoginForm", { "p_passmasked_bis": mask_password( - password, login_mask, alias, + password, + login_mask, + alias, ) }, ) @@ -278,7 +285,9 @@ class PekaoClient: date_from = datetime.datetime.now() - datetime.timedelta(days=60) if date_from < pekao_epoch: - self.logger.warning("Rolling back from %r to %r (pekao epoch", date_from, pekao_epoch) + self.logger.warning( + "Rolling back from %r to %r (pekao epoch", date_from, pekao_epoch + ) date_from = pekao_epoch if date_to is None: @@ -331,13 +340,13 @@ class PekaoClient: def _go(self, url, method="GET", **args): self.logger.debug("=> %s %s", method, url) if self.resp and self.resp.url: - self.session.headers['Referer'] = self.resp.url + self.session.headers["Referer"] = self.resp.url self.resp = self.session.request(method, url, timeout=15, **args) self.logger.debug(" -> [%d] %s", self.resp.status_code, self.resp.url) self.resp.raise_for_status() - self.bs = BeautifulSoup(self.resp.text, features='html.parser') + self.bs = BeautifulSoup(self.resp.text, features="html.parser") def _submit_form(self, name, values): form = self.bs.find("form", {"name": name}) @@ -355,11 +364,12 @@ def lock(fn): logging.error("Lock file %s exists, aborting", fn) sys.exit(3) logging.debug("Setting up lock file %s", fn) - open(fn,'w').close() + open(fn, "w").close() if not os.path.isfile(fn): logging.error("Lock file %s somehow does not exist, aborting", fn) sys.exit(3) + def release(fn): logging.debug("Removing lock file %s", fn) if not os.path.isfile(fn): @@ -370,48 +380,67 @@ def release(fn): logging.error("Lock file %s somehow still exists, WTF?", fn) sys.exit(3) + parser = argparse.ArgumentParser() -parser.add_argument('--config', help="Load configuration file") -parser.add_argument('-n', '--no-action', action="store_true", help='do not commit any database changes') -parser.add_argument('-c', '--cached', action="store_true", help='use cached data (test)') -parser.add_argument('-l', '--load', action='append', help='process specified files (test)') -parser.add_argument('-t', '--token', help='use authentication token') -parser.add_argument('--no-lock', action='store_true', help='don\'t use lockfile (test)') -parser.add_argument('--print-schema', action="store_true", help='print table schema and quit') +parser.add_argument("--config", help="Load configuration file") +parser.add_argument( + "-n", "--no-action", action="store_true", help="do not commit any database changes" +) +parser.add_argument( + "-c", "--cached", action="store_true", help="use cached data (test)" +) +parser.add_argument( + "-l", "--load", action="append", help="process specified files (test)" +) +parser.add_argument("-t", "--token", help="use authentication token") +parser.add_argument("--no-lock", action="store_true", help="don't use lockfile (test)") +parser.add_argument( + "--print-schema", action="store_true", help="print table schema and quit" +) if __name__ == "__main__": args = parser.parse_args() - config = configparser.ConfigParser(defaults=os.environ, interpolation=configparser.ExtendedInterpolation()) - config.read_dict({ - 'logging': { - 'level': 'INFO', - }, - 'general': { - 'cache_dir': 'cache', - 'lockfile': 'lockfile', - }, - }) + config = configparser.ConfigParser( + defaults=os.environ, interpolation=configparser.ExtendedInterpolation() + ) + config.read_dict( + { + "logging": { + "level": "INFO", + }, + "general": { + "cache_dir": "cache", + "lockfile": "lockfile", + }, + } + ) if args.config: config.read(args.config) - logging.basicConfig(level=config['logging']['level'], format=config['logging'].get('format', '%(asctime)s [%(levelname)s] %(name)s: %(message)s')) - logging.getLogger('chardet').setLevel(logging.WARN) - logging.getLogger('charset_normalizer').setLevel(logging.WARN) + logging.basicConfig( + level=config["logging"]["level"], + format=config["logging"].get( + "format", "%(asctime)s [%(levelname)s] %(name)s: %(message)s" + ), + ) + logging.getLogger("chardet").setLevel(logging.WARN) + logging.getLogger("charset_normalizer").setLevel(logging.WARN) - CACHE_DIR = config['general']['cache_dir'] - engine = create_engine(config['database']['uri']) + CACHE_DIR = config["general"]["cache_dir"] + engine = create_engine(config["database"]["uri"]) session = sessionmaker(bind=engine)() if args.print_schema: - logging.debug("Called with --print-schema, will print the create " + - "statement and quit.") + logging.debug( + "Called with --print-schema, will print the create " + "statement and quit." + ) print(get_schema(engine)) sys.exit() if not args.no_lock: - lock(config['general']['lockfile']) + lock(config["general"]["lockfile"]) balances = {} history_logs = {} @@ -419,62 +448,68 @@ if __name__ == "__main__": if args.load: logging.debug("Using manually supplied files") for fn in args.load: - an, f = fn.split(':') + an, f = fn.split(":") account_number = IBParser.parse_account_number(an) if account_number is None: - logging.error("File name number \"{}\" unparseable".format(f)) + logging.error('File name number "{}" unparseable'.format(f)) continue logging.debug('Loading "%s" as "%s"', f, account_number) - with open(f, 'r') as fd: + with open(f, "r") as fd: history_logs[account_number] = json.loads(fd.read()) elif args.cached: logging.debug("Loading cached files from {}".format(CACHE_DIR)) for f in os.listdir(CACHE_DIR): - if f.startswith('balance-'): + if f.startswith("balance-"): continue account_number = CAMT052Parser.parse_account_number(f) if account_number is None: - logging.error("File name number \"{}\" unparseable".format(f)) + logging.error('File name number "{}" unparseable'.format(f)) continue - with open(CACHE_DIR + "/" + f, 'r') as fd: + with open(CACHE_DIR + "/" + f, "r") as fd: history_logs[account_number] = fd.read() - logging.debug("Loading \"{}\" as \"{}\"".format(f, account_number)) + logging.debug('Loading "{}" as "{}"'.format(f, account_number)) else: logging.debug("Normal run - will connect to the bank") - fetcher = PekaoClient(config['scraper']) + fetcher = PekaoClient(config["scraper"]) if args.token: fetcher.token = args.token logging.debug("Using provided token") - elif "alias" not in config['scraper'] or "password" not in config['scraper']: + elif "alias" not in config["scraper"] or "password" not in config["scraper"]: fetcher.login(input("[?] ID: "), input("[?] Password: ")) else: logging.debug("Using saved credentials") - fetcher.login(config["scraper"]['alias'], config["scraper"]['password']) + fetcher.login(config["scraper"]["alias"], config["scraper"]["password"]) accounts = fetcher.list_accounts() for account_id, account in accounts.items(): - account_number = CAMT052Parser.parse_account_number(account['p_acc_no']) - logging.debug("Fetching history for account {} ({}) {}".format( - account_number, account_id, account["p_acc_alias"], - )) + account_number = CAMT052Parser.parse_account_number(account["p_acc_no"]) + logging.debug( + "Fetching history for account {} ({}) {}".format( + account_number, + account_id, + account["p_acc_alias"], + ) + ) history = fetcher.fetch_transfers_camt052(account_id) history_logs[account_number] = history - with open(CACHE_DIR + "/" + account_number, 'w') as fd: - fd.write('' if history is None else history) + with open(CACHE_DIR + "/" + account_number, "w") as fd: + fd.write("" if history is None else history) balances[account_number] = ( - account["p_acc_avail_balance"], account["p_acc_currency"]) - with open(CACHE_DIR + "/balance-"+account_number, 'w') as fd: + account["p_acc_avail_balance"], + account["p_acc_currency"], + ) + with open(CACHE_DIR + "/balance-" + account_number, "w") as fd: fd.write("{} {}\n".format(*balances[account_number])) if not history_logs: - logging.error('Nothing to process') + logging.error("Nothing to process") sys.exit() parsed = {} @@ -482,7 +517,7 @@ if __name__ == "__main__": for account_number, history in history_logs.items(): logging.debug("Parsing history for account {}".format(account_number)) if not history: - logging.debug('No transfers for that account, continuing...') + logging.debug("No transfers for that account, continuing...") continue parser = CAMT052Parser(history, own_accounts=list(history_logs.keys())) @@ -499,13 +534,13 @@ if __name__ == "__main__": stats[account_number]["skipped"] += 1 if args.no_action: - logging.info('Running with --no-action, not commiting.') + logging.info("Running with --no-action, not commiting.") else: session.commit() # That is pretty ugly, but the only alternative would be to change handler # level in runtime, and that'd still need some rollback anyway. - if any(v['added'] for v in stats.values()): + if any(v["added"] for v in stats.values()): log_summary = logging.info else: log_summary = logging.debug @@ -514,10 +549,9 @@ if __name__ == "__main__": log_summary("Account balances:") for account_number, v in balances.items(): balance, currency = v - log_summary("\t{}: {} {}".format( - account_number, balance, currency)) + log_summary("\t{}: {} {}".format(account_number, balance, currency)) log_summary("Done: %r", stats) if not args.no_lock: - release(config['general']['lockfile']) + release(config["general"]["lockfile"]) diff --git a/fetch/models.py b/fetch/models.py index 968c4f2..e7f7773 100644 --- a/fetch/models.py +++ b/fetch/models.py @@ -41,7 +41,7 @@ class RawTransfer(Base): scrape_timestamp = Column(BigInteger, default=lambda: round(time.time() * 1000000)) def __str__(self): - return u'{} *{} #{} @{} -"{}" -#{} => +"{}" +#{} [{}.{:02d} {}] ~"{}"'.format( + return '{} *{} #{} @{} -"{}" -#{} => +"{}" +#{} [{}.{:02d} {}] ~"{}"'.format( self.type, self.uid, self.on_account, diff --git a/web/api-client.py b/web/api-client.py index 403c48c..b547abd 100644 --- a/web/api-client.py +++ b/web/api-client.py @@ -11,7 +11,7 @@ # 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 +# 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 @@ -43,8 +43,10 @@ class APIClient(object): data = serialized.encode("base64") + "," + mac64 r = requests.post("%s/api/%s" % (self.address, name), data) return json.loads(r.text) + return f + if __name__ == "__main__": # invoke an interactive version client = APIClient("testkey", "http://127.0.0.1:5000") diff --git a/web/manage.py b/web/manage.py index 23ef271..41272fa 100755 --- a/web/manage.py +++ b/web/manage.py @@ -5,9 +5,11 @@ from flask.cli import FlaskGroup from webapp.wsgi import app + @click.group(cls=FlaskGroup, create_app=lambda i: app) def cli(): """This is a management script for Kasownik.""" -if __name__ == '__main__': + +if __name__ == "__main__": cli() diff --git a/web/webapp/admin.py b/web/webapp/admin.py index b94bdbc..ee330bd 100644 --- a/web/webapp/admin.py +++ b/web/webapp/admin.py @@ -2,8 +2,18 @@ import datetime from email.mime.text import MIMEText from subprocess import Popen, PIPE -from flask import render_template, request, flash, g, Response, \ - redirect, url_for, abort, Blueprint, current_app +from flask import ( + render_template, + request, + flash, + g, + Response, + redirect, + url_for, + abort, + Blueprint, + current_app, +) from flask_login import login_required from webapp import forms, db, models, admin_required @@ -11,7 +21,8 @@ from . import directory from . import logic -bp = Blueprint('admin', __name__) +bp = Blueprint("admin", __name__) + @bp.route("/admin") @admin_required @@ -19,20 +30,21 @@ bp = Blueprint('admin', __name__) def index(): members = [m.get_status() for m in models.Member.get_members(True)] for member in members: - due = member['months_due'] + due = member["months_due"] if due is not None and due < 1: - member['color'] = "00FF00" + member["color"] = "00FF00" elif due is not None and due < 3: - member['color'] = "E0941B" + member["color"] = "E0941B" else: - member['color'] = "FF0000" - active_members = list(filter(lambda m: m['judgement'], members)) - inactive_members = list(filter(lambda m: not m['judgement'], members)) - return render_template("admin_index.html", - active_members=active_members, - inactive_members=inactive_members, - transfers_unmatched=logic.get_unmatched_transfers() - ) + member["color"] = "FF0000" + active_members = list(filter(lambda m: m["judgement"], members)) + inactive_members = list(filter(lambda m: not m["judgement"], members)) + return render_template( + "admin_index.html", + active_members=active_members, + inactive_members=inactive_members, + transfers_unmatched=logic.get_unmatched_transfers(), + ) @bp.route("/admin/ldapsync", methods=["POST", "GET"]) @@ -46,25 +58,27 @@ def admin_ldap_sync(): 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_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.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_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.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 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 + 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) @@ -78,17 +92,20 @@ def admin_csv(): members = [] for m in models.Member.get_members(True): member = m.get_status() - if member['type'] == 'supporting': + if member["type"] == "supporting": continue - member['contact_email'] = m.get_contact_email() - member['cn'] = directory.get_member_fields(g.ldap, member['username'], 'cn')['cn'] + 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) + active_members = filter(lambda m: m["judgement"], members) output = render_template("admin_csv.html", active_members=active_members) return Response(output) -@bp.route('/admin/member/', methods=['GET', 'POST']) + +@bp.route("/admin/member/", methods=["GET", "POST"]) @login_required @admin_required def admin_member(membername): @@ -96,16 +113,23 @@ def admin_member(membername): if not member: abort(404) status = member.get_status() - cn = directory.get_member_fields(g.ldap, member.username, 'cn')['cn'] + cn = directory.get_member_fields(g.ldap, member.username, "cn")["cn"] admin_form = forms.AdminProfileEdit(obj=member) if admin_form.validate(): admin_form.populate_obj(member) db.session.commit() - flash('Member info changed') + flash("Member info changed") + + return render_template( + "admin_member.html", + member=member, + status=status, + cn=cn, + admin=True, + admin_form=admin_form, + ) - return render_template("admin_member.html", member=member, status=status, - cn=cn, admin=True, admin_form=admin_form) @bp.route("/admin/member//policy:") @login_required @@ -115,7 +139,8 @@ def admin_member_set_policy(membername, policy): member.payment_policy = models.PaymentPolicy[policy].value db.session.add(member) db.session.commit() - return redirect(request.referrer or url_for('.admin_member', membername=membername)) + return redirect(request.referrer or url_for(".admin_member", membername=membername)) + @bp.route("/admin/member//membership:") @login_required @@ -125,18 +150,21 @@ def admin_member_set_membership(membername, membershiptype): member.type = models.MembershipType[membershiptype].name db.session.add(member) db.session.commit() - return redirect(request.referrer or url_for('.admin_member', membername=membername)) + return redirect(request.referrer or url_for(".admin_member", membername=membername)) @bp.route("/admin/member/add//") @login_required @admin_required def add_member(membershiptype, username): - member = models.Member(None, username, models.MembershipType[membershiptype].name, True) + member = models.Member( + None, username, models.MembershipType[membershiptype].name, True + ) db.session.add(member) db.session.commit() - flash('Member created') - return redirect(request.referrer or url_for('.match_manual')) + flash("Member created") + return redirect(request.referrer or url_for(".match_manual")) + @bp.route("/admin/match") @login_required @@ -157,14 +185,18 @@ def match_auto(): mts = transfer.member_transfers member = mts[0].member member.get_status(force_refresh=True) - months = ', '.join('%d-%d' % (mt.year, mt.month) for mt in mts) - flash("Matched transfer {} for {:.2f}PLN to member {} for month {}".format( - transfer.id, transfer.amount/100, member.username, months)) + months = ", ".join("%d-%d" % (mt.year, mt.month) for mt in mts) + flash( + "Matched transfer {} for {:.2f}PLN to member {} for month {}".format( + transfer.id, transfer.amount / 100, member.username, months + ) + ) db.session.commit() flash("Matched %i, %i left" % (len(matched), len(unmatched))) return redirect(url_for(".match_index")) + @bp.route("/admin/match/manual", methods=["GET"]) @login_required @admin_required @@ -172,33 +204,35 @@ def match_manual(): transfers_unmatched = logic.get_unmatched_transfers() return render_template("match_manual.html", transfers_unmatched=transfers_unmatched) + @bp.route("/admin/match/ignore/") @login_required @admin_required def ignore(uid): transfer = models.Transfer.query.filter_by(uid=uid).first() if not transfer: - flash('No transfer found', 'danger') + flash("No transfer found", "danger") return redirect(url_for(".match_manual")) transfer.ignore = True db.session.commit() - flash('Transfer %s ignored' % (transfer,)) + flash("Transfer %s ignored" % (transfer,)) return redirect(request.referrer) + @bp.route("/admin/match///") @login_required @admin_required def match(username, uid, months): member = models.Member.query.filter_by(username=username).first() if not member: - flash('No member found', 'danger') + flash("No member found", "danger") return redirect(url_for(".match_manual")) transfer = models.Transfer.query.filter_by(uid=uid).first() if not transfer: - flash('No transfer found', 'danger') + flash("No transfer found", "danger") return redirect(url_for(".match_manual")) for _ in range(months): @@ -210,7 +244,7 @@ def match(username, uid, months): db.session.commit() member.get_status(force_refresh=True) - flash('OK, %d get' % transfer.amount) + flash("OK, %d get" % transfer.amount) return redirect(url_for(".match_manual")) @@ -222,30 +256,40 @@ def match_user_transfer(): uid = request.form["uid"] member = models.Member.query.filter_by(username=username).first() if not member: - flash('No member found', 'danger') + flash("No member found", "danger") return redirect(url_for(".match_manual")) transfer = models.Transfer.query.filter_by(uid=uid).first() if not transfer: - flash('No transfer found', 'danger') + flash("No transfer found", "danger") return redirect(url_for(".match_manual")) return render_template("match_user_transfer.html", member=member, transfer=transfer) + @bp.route("/admin/spam/", methods=["GET", "POST"]) @login_required @admin_required def sendspam(): now = datetime.datetime.now() - members = models.Member.get_members(True).filter_by( - payment_policy=models.PaymentPolicy.normal.value).all() + members = ( + models.Member.get_members(True) + .filter_by(payment_policy=models.PaymentPolicy.normal.value) + .all() + ) members = [(m, m.get_status()) for m in members] - members.sort(key=lambda m: (-m[1]['months_due'] or 0)) + members.sort(key=lambda m: (-m[1]["months_due"] or 0)) form = forms.SpamForm() - form.members.choices = [(member.id, str(member)) for member, status in members if status['months_due'] or status['judgement']] - form.members.default = [member.id for member, status in members if status['months_due'] > 1] + form.members.choices = [ + (member.id, str(member)) + for member, status in members + if status["months_due"] or status["judgement"] + ] + form.members.default = [ + member.id for member, status in members if status["months_due"] > 1 + ] form.process(request.form) @@ -256,11 +300,16 @@ def sendspam(): continue content = render_template( - 'mailing/due.txt', + "mailing/due.txt", member=member, status=status, - transfers=[t for t in member.transfers if t.transfer.uid != current_app.config['DUMMY_TRANSFER_UID']][-5:], - now=now) + transfers=[ + t + for t in member.transfers + if t.transfer.uid != current_app.config["DUMMY_TRANSFER_UID"] + ][-5:], + now=now, + ) # Just ignore empty messages if not content.strip(): @@ -274,14 +323,17 @@ def sendspam(): if form.dry_run.data: readable = [ - msg.as_string().split('\n\n')[0] + '\n\n' - + msg.get_payload(decode=True).decode('utf-8') for msg in spam] - return Response('\n====\n'.join(readable), mimetype='text/plain') + msg.as_string().split("\n\n")[0] + + "\n\n" + + msg.get_payload(decode=True).decode("utf-8") + for msg in spam + ] + return Response("\n====\n".join(readable), mimetype="text/plain") for msg in spam: p = Popen(["/usr/sbin/sendmail", "-t"], stdin=PIPE) p.communicate(msg.as_bytes()) - flash('%d messages sent!' % len(spam)) - return redirect(url_for('.index')) - return render_template('admin_spam.html', form=form) + flash("%d messages sent!" % len(spam)) + return redirect(url_for(".index")) + return render_template("admin_spam.html", form=form) diff --git a/web/webapp/api.py b/web/webapp/api.py index 4032e93..1754691 100644 --- a/web/webapp/api.py +++ b/web/webapp/api.py @@ -35,7 +35,8 @@ from flask import request, abort, Response, Blueprint from webapp import models, app, cache logger = logging.getLogger(__name__) -bp = Blueprint('api', __name__) +bp = Blueprint("api", __name__) + class APIError(Exception): def __init__(self, message, code=500): @@ -47,6 +48,7 @@ def _public_api_method(path): """A decorator that adds a public, GET based method at /api/.json. The resulting data is JSON-serialized.""" + def decorator2(original): @wraps(original) def wrapper_json(*args, **kwargs): @@ -59,44 +61,51 @@ def _public_api_method(path): code = exc.code status = "error" - last_transfer = models.Transfer.query.order_by(models.Transfer.date.desc()).first() + last_transfer = models.Transfer.query.order_by( + models.Transfer.date.desc() + ).first() modified = str(last_transfer.date) if last_transfer else None - resp = { - "status": status, - "content": content, - "modified": modified - } + resp = {"status": status, "content": content, "modified": modified} return Response(json.dumps(resp), mimetype="application/json"), code + return bp.route("/api/" + path + ".json", methods=["GET"])(wrapper_json) + return decorator2 + @cache.memoize() def _stats_for_month(year, month): # TODO: export this to the config - money_required = 4217+615+615 + money_required = 4217 + 615 + 615 money_paid = 0 - mts = models.MemberTransfer.query.filter_by(year=year, month=month).\ - join(models.MemberTransfer.transfer).all() + mts = ( + models.MemberTransfer.query.filter_by(year=year, month=month) + .join(models.MemberTransfer.transfer) + .all() + ) for mt in mts: amount_all = mt.transfer.amount amount = amount_all / len(mt.transfer.member_transfers) money_paid += amount - return money_required, money_paid/100 + return money_required, money_paid / 100 + @_public_api_method("month//") def api_month(year=None, month=None): money_required, money_paid = _stats_for_month(year, month) return dict(required=money_required, paid=money_paid) + @_public_api_method("judgement/") def api_judgement(membername): member = models.Member.query.filter_by(username=membername).first() if not member: raise APIError("No such member.", 404) - judgement = member.get_status()['judgement'] + judgement = member.get_status()["judgement"] return judgement + @_public_api_method("months_due/") @cache.memoize() def api_months_due(membername): @@ -106,8 +115,8 @@ def api_months_due(membername): year, month = member.get_last_paid() if not year: raise APIError("Member never paid.", 402) - if year and member.active == False and member.username == 'b_rt': - raise APIError("Stoned.",420) + if year and member.active == False and member.username == "b_rt": + raise APIError("Stoned.", 420) if year and member.active == False: raise APIError("No longer a member.", 410) due = member.get_months_due() diff --git a/web/webapp/authutils.py b/web/webapp/authutils.py index ceffe20..363749b 100644 --- a/web/webapp/authutils.py +++ b/web/webapp/authutils.py @@ -3,9 +3,11 @@ from flask_login import AnonymousUserMixin from spaceauth.caps import cap_check from webapp import models + class AnonymousUser(AnonymousUserMixin): is_admin = False + class User(object): def __init__(self, username): self.username = username.lower().strip() @@ -27,8 +29,7 @@ class User(object): @property def is_admin(self): - return cap_check('kasownik_access', self.username) + return cap_check("kasownik_access", self.username) def get_model(self, deep=True): - return models.Member.get_members(deep) \ - .filter_by(username=self.username).first() + return models.Member.get_members(deep).filter_by(username=self.username).first() diff --git a/web/webapp/commands.py b/web/webapp/commands.py index 04c43b1..7e9e122 100644 --- a/web/webapp/commands.py +++ b/web/webapp/commands.py @@ -9,8 +9,9 @@ from . import logic group = AppGroup(__name__) + @group.command() -@click.option('-n', '--dry-run', is_flag=True, help='Don\'t apply any changes.') +@click.option("-n", "--dry-run", is_flag=True, help="Don't apply any changes.") def ldapsync(dry_run): """Synchronizes LDAP groups state.""" @@ -23,29 +24,30 @@ def ldapsync(dry_run): if diff is None: return - changes = {'fatty': {}, 'starving': {}} - changes['fatty']['add'] = diff['fatty_to_add'] - changes['fatty']['remove'] = diff['fatty_to_remove'] - changes['starving']['add'] = diff['starving_to_add'] - changes['starving']['remove'] = diff['starving_to_remove'] + changes = {"fatty": {}, "starving": {}} + changes["fatty"]["add"] = diff["fatty_to_add"] + changes["fatty"]["remove"] = diff["fatty_to_remove"] + changes["starving"]["add"] = diff["starving_to_add"] + changes["starving"]["remove"] = diff["starving_to_remove"] - click.echo('Applying %d changes:' % sum([len(n) for n in diff.values()])) + click.echo("Applying %d changes:" % sum([len(n) for n in diff.values()])) for k, v in changes.items(): - changelist = ['+%s' % n for n in v['add']] + ['-%s' % n for n in v['remove']] + changelist = ["+%s" % n for n in v["add"]] + ["-%s" % n for n in v["remove"]] if changelist: - click.echo('\t%s: %s' % (k, ', '.join(changelist))) + click.echo("\t%s: %s" % (k, ", ".join(changelist))) click.echo() if dry_run: - click.echo('Exiting, just a dry run.') + click.echo("Exiting, just a dry run.") return directory.update_member_groups(g.ldap, changes) - click.echo('Done.') + click.echo("Done.") + @group.command() -@click.option('-n', '--dry-run', is_flag=True, help='Don\'t commit changes.') +@click.option("-n", "--dry-run", is_flag=True, help="Don't commit changes.") def automatch(dry_run): """Matches transfers to membership months.""" transfers_unmatched = logic.get_unmatched_transfers() @@ -58,9 +60,12 @@ def automatch(dry_run): if not dry_run: member.get_status(force_refresh=True) - months = ', '.join('%d-%d' % (mt.year, mt.month) for mt in mts) - click.echo("Matched transfer {} for {:.2f}PLN to member {} for month {}".format( - transfer.id, transfer.amount/100, member.username, months)) + months = ", ".join("%d-%d" % (mt.year, mt.month) for mt in mts) + click.echo( + "Matched transfer {} for {:.2f}PLN to member {} for month {}".format( + transfer.id, transfer.amount / 100, member.username, months + ) + ) if dry_run: click.echo("Dry run, not commiting.") @@ -71,8 +76,9 @@ def automatch(dry_run): if matched: click.echo("Done, %d matched, %d left" % (len(matched), len(unmatched))) + @group.command() def syncdb(): """Initializes database.""" db.create_all() - click.echo('Done.') + click.echo("Done.") diff --git a/web/webapp/directory.py b/web/webapp/directory.py index 7239f26..665bf53 100644 --- a/web/webapp/directory.py +++ b/web/webapp/directory.py @@ -33,45 +33,60 @@ from webapp import mc, cache_enabled, app def connect(): - c = ldap.initialize(app.config['LDAP_URI']) - c.set_option(ldap.OPT_X_TLS_CACERTFILE, app.config['LDAP_CA_PATH']) + c = ldap.initialize(app.config["LDAP_URI"]) + c.set_option(ldap.OPT_X_TLS_CACERTFILE, app.config["LDAP_CA_PATH"]) c.set_option(ldap.OPT_X_TLS_NEWCTX, 0) c.start_tls_s() - c.simple_bind_s(app.config['LDAP_BIND_DN'], - app.config['LDAP_BIND_PASSWORD']) + c.simple_bind_s(app.config["LDAP_BIND_DN"], app.config["LDAP_BIND_PASSWORD"]) return c + @app.before_request def _setup_ldap(): - if not app.config.get('DISABLE_LDAP'): + if not app.config.get("DISABLE_LDAP"): g.ldap = connect() else: g.ldap = None + @app.teardown_request def _destroy_ldap(exception=None): if g.ldap: g.ldap.unbind_s() + def get_ldap_group_diff(members): - active_members = list(filter(lambda m: m['judgement'], members)) - fatty = set([member['username'] for member in active_members if member['type'] in ['fatty', 'supporting']]) - starving = set([member['username'] for member in active_members if member['type'] in ['starving']]) + active_members = list(filter(lambda m: m["judgement"], members)) + fatty = set( + [ + member["username"] + for member in active_members + if member["type"] in ["fatty", "supporting"] + ] + ) + starving = set( + [ + member["username"] + for member in active_members + if member["type"] in ["starving"] + ] + ) - ldap_fatty = set(get_group_members(g.ldap, 'fatty')) - ldap_starving = set(get_group_members(g.ldap, 'starving')) - ldap_potato = set(get_group_members(g.ldap, 'potato')) + ldap_fatty = set(get_group_members(g.ldap, "fatty")) + ldap_starving = set(get_group_members(g.ldap, "starving")) + ldap_potato = set(get_group_members(g.ldap, "potato")) result = {} - result['fatty_to_remove'] = list(ldap_fatty - fatty) - result['fatty_to_add'] = list(fatty - ldap_fatty) - result['starving_to_remove'] = list(ldap_starving - starving) - result['starving_to_add'] = list(starving - ldap_starving) + result["fatty_to_remove"] = list(ldap_fatty - fatty) + result["fatty_to_add"] = list(fatty - ldap_fatty) + result["starving_to_remove"] = list(ldap_starving - starving) + result["starving_to_add"] = list(starving - ldap_starving) if sum([len(result[k]) for k in result]) == 0: return None return result + # kinda clunky with all the member fetching, transforming the list in various ways and updating it again here, but it's a workaround for LDAP crashing on modify_s, no fucks given def update_member_groups(c, changes): for group in changes: @@ -79,72 +94,94 @@ def update_member_groups(c, changes): changed = False for op in changes[group]: for username in changes[group][op]: - if op == 'add': - if get_member_fields(c, username, ['uid'])['uid'] is None: - logging.warning('User %r missing from LDAP, ignoring...', username) + if op == "add": + if get_member_fields(c, username, ["uid"])["uid"] is None: + logging.warning( + "User %r missing from LDAP, ignoring...", username + ) continue changed = True target_members.add(username) - elif op == 'remove': + elif op == "remove": changed = True target_members.remove(username) if not changed: continue values = [] for username in target_members: - values.append('uid={},{}'.format(username,app.config['LDAP_USER_BASE']).encode('utf-8')) - modlist = [(ldap.MOD_REPLACE,'uniqueMember',values)] + values.append( + "uid={},{}".format(username, app.config["LDAP_USER_BASE"]).encode( + "utf-8" + ) + ) + modlist = [(ldap.MOD_REPLACE, "uniqueMember", values)] + + c.modify_s("cn={},{}".format(group, app.config["LDAP_GROUP_BASE"]), modlist) - c.modify_s('cn={},{}'.format(group,app.config['LDAP_GROUP_BASE']), modlist) def get_group_members(c, group): - if app.config.get('DISABLE_LDAP'): + if app.config.get("DISABLE_LDAP"): return [] - lfilter = '(&(cn={}){})'.format(group, app.config['LDAP_GROUP_FILTER']) - data = c.search_s(app.config['LDAP_GROUP_BASE'], ldap.SCOPE_SUBTREE, - lfilter, tuple(['uniqueMember',])) + lfilter = "(&(cn={}){})".format(group, app.config["LDAP_GROUP_FILTER"]) + data = c.search_s( + app.config["LDAP_GROUP_BASE"], + ldap.SCOPE_SUBTREE, + lfilter, + tuple( + [ + "uniqueMember", + ] + ), + ) members = [] for dn, obj in data: for k, v in obj.items(): if k == "uniqueMember": for iv in v: - part,uid,index = ldap.dn.str2dn(iv)[0][0] - if not part == 'uid' or not index == 1: - raise ValueError("First part type {} or index {} seem wrong for DN {}".format(part,index,iv)) + part, uid, index = ldap.dn.str2dn(iv)[0][0] + if not part == "uid" or not index == 1: + raise ValueError( + "First part type {} or index {} seem wrong for DN {}".format( + part, index, iv + ) + ) members.append(uid) return members + def get_member_fields(c, member, fields): - if app.config.get('DISABLE_LDAP'): + if app.config.get("DISABLE_LDAP"): import collections + return collections.defaultdict(str) if isinstance(fields, str): - fields = [fields,] + fields = [ + fields, + ] fields_needed = set(fields) fields_out = {} if cache_enabled: for field in fields: - field_cache = mc.get('kasownik-ldap-member-{}/{}' - .format(member, field)) + field_cache = mc.get("kasownik-ldap-member-{}/{}".format(member, field)) if field_cache is not None: fields_out[field] = field_cache fields_needed.remove(field) - member = member.replace('(', '').replace(')', '') - lfilter = '(&(uid={}){})'.format(member, app.config['LDAP_USER_FILTER']) - data = c.search_s(app.config['LDAP_USER_BASE'], ldap.SCOPE_SUBTREE, - lfilter, tuple(fields)) + member = member.replace("(", "").replace(")", "") + lfilter = "(&(uid={}){})".format(member, app.config["LDAP_USER_FILTER"]) + data = c.search_s( + app.config["LDAP_USER_BASE"], ldap.SCOPE_SUBTREE, lfilter, tuple(fields) + ) for dn, obj in data: for k, v in obj.items(): - v = v[0].decode('utf-8') + v = v[0].decode("utf-8") if k in fields_needed: fields_out[k] = v if cache_enabled: - mc.set('kasownik-ldap-member-{}/{}' - .format(member, field), v) + mc.set("kasownik-ldap-member-{}/{}".format(member, field), v) for k in fields_needed - set(fields_out.keys()): fields_out[k] = None diff --git a/web/webapp/forms.py b/web/webapp/forms.py index 9606bae..ff52742 100644 --- a/web/webapp/forms.py +++ b/web/webapp/forms.py @@ -11,7 +11,7 @@ # 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 +# 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 @@ -22,31 +22,46 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -from wtforms import Form, BooleanField, TextField, PasswordField, SelectMultipleField, FormField, validators, widgets +from wtforms import ( + Form, + BooleanField, + TextField, + PasswordField, + SelectMultipleField, + FormField, + validators, + widgets, +) from flask_wtf import Form as FlaskForm + class MultiCheckboxField(SelectMultipleField): widget = widgets.ListWidget(prefix_label=False) option_widget = widgets.CheckboxInput() + class LoginForm(FlaskForm): - username = TextField('Username', [validators.Required()]) - password = PasswordField('Password', [validators.Required()]) + username = TextField("Username", [validators.Required()]) + password = PasswordField("Password", [validators.Required()]) + class ContactEmailSettingsForm(FlaskForm): local = BooleanField("") ldap = BooleanField("") custom = TextField("Custom address:") + class LDAPSyncForm(FlaskForm): fatty_to_add = MultiCheckboxField("Fatty to add", choices=[]) fatty_to_remove = MultiCheckboxField("Fatty to remove", choices=[]) starving_to_add = MultiCheckboxField("Starving to add", choices=[]) starving_to_remove = MultiCheckboxField("Starving to remove", choices=[]) + class SpamForm(FlaskForm): dry_run = BooleanField("Dry run") members = MultiCheckboxField("Members to spam", coerce=int) + class AdminProfileEdit(FlaskForm): alias = TextField("Alias") diff --git a/web/webapp/logic.py b/web/webapp/logic.py index 780bd1c..975cf7f 100644 --- a/web/webapp/logic.py +++ b/web/webapp/logic.py @@ -11,7 +11,7 @@ # 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 +# 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 @@ -27,9 +27,14 @@ from webapp import app, db from . import models + def get_unmatched_transfers(): - return models.Transfer.query.filter_by(member_transfers=None,ignore=False) \ - .order_by(models.Transfer.date.asc()).all() + return ( + models.Transfer.query.filter_by(member_transfers=None, ignore=False) + .order_by(models.Transfer.date.asc()) + .all() + ) + def try_automatch(transfers): matched = [] @@ -48,7 +53,7 @@ def try_automatch(transfers): for m in range(months): mt = models.MemberTransfer(None, year, month, transfer) member.transfers.append(mt) - year, month = member._yearmonth_increment((year,month)) + year, month = member._yearmonth_increment((year, month)) matched.append(transfer) else: unmatched.append(transfer) diff --git a/web/webapp/models.py b/web/webapp/models.py index 6787507..de6b15e 100644 --- a/web/webapp/models.py +++ b/web/webapp/models.py @@ -36,7 +36,6 @@ from webapp import app, db, cache, cache_enabled from . import directory - class APIKey(db.Model): id = db.Column(db.Integer, primary_key=True) secret = db.Column(db.String(64)) @@ -44,13 +43,13 @@ class APIKey(db.Model): description = db.Column(db.Text) def __repr__(self): - return '' % (self.member, self.description) + return "" % (self.member, self.description) class MemberTransfer(db.Model): __tablename__ = "member_transfer" id = db.Column(db.Integer, primary_key=True) - member_id = db.Column('member', db.Integer, db.ForeignKey("member.id")) + member_id = db.Column("member", db.Integer, db.ForeignKey("member.id")) transfer_id = db.Column(db.Integer, db.ForeignKey("transfer.id")) year = db.Column(db.Integer) month = db.Column(db.Integer) @@ -63,17 +62,24 @@ class MemberTransfer(db.Model): self.transfer = transfer from webapp import api + cache.delete_memoized(api._stats_for_month, year, month) cache.delete_memoized(api.api_cashflow, year, month) def __repr__(self): - return '' % (self.year, self.month, self.member, self.transfer) + return "" % ( + self.year, + self.month, + self.member, + self.transfer, + ) class PaymentStatus(enum.Enum): - never_paid = 1 # never paid membership fees - unpaid = 2 # more than 3 fees unapid - okay = 3 # fees paid + never_paid = 1 # never paid membership fees + unpaid = 2 # more than 3 fees unapid + okay = 3 # fees paid + class PaymentPolicy(enum.Enum): normal = "Normal" @@ -81,19 +87,24 @@ class PaymentPolicy(enum.Enum): potato = "Potato" disabled = "Disabled" + class MembershipType(enum.Enum): fatty = "Fatty" starving = "Starving" supporting = "Supporting" + class Member(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(64), unique=True) type = db.Column(db.Enum("starving", "fatty", "supporting", name="member_types")) - transfers = db.relationship("MemberTransfer", order_by=[ - db.asc(MemberTransfer.year), db.asc(MemberTransfer.month)], backref='member') + transfers = db.relationship( + "MemberTransfer", + order_by=[db.asc(MemberTransfer.year), db.asc(MemberTransfer.month)], + backref="member", + ) - active = db.Column(db.Boolean) # DEPRECATED donut use + active = db.Column(db.Boolean) # DEPRECATED donut use api_keys = db.relationship("APIKey") join_year = db.Column(db.Integer) join_month = db.Column(db.Integer) @@ -102,8 +113,12 @@ class Member(db.Model): # Extended Grace Period - do not shut off account after grace period # Potato - do not ever shut off account, report falsified payment status # Disabled - manual disable override, regardless of payment extra - payment_policy = db.Column(db.Enum(*[p.value for p in PaymentPolicy.__members__.values()], - name='payment_policy_types')) + payment_policy = db.Column( + db.Enum( + *[p.value for p in PaymentPolicy.__members__.values()], + name="payment_policy_types" + ) + ) preferred_email = db.Column(db.String(64)) def mt_covers(self, mt): @@ -113,11 +128,11 @@ class Member(db.Model): ix = self.transfers.index(mt) if ix != 0: # check if the previous mt was covered by the same transfer - if self.transfers[ix-1].transfer.uid == mt.transfer.uid: + if self.transfers[ix - 1].transfer.uid == mt.transfer.uid: return None # check how many next mts use the same transfer rowspan = 0 - for ix2 in range(ix+1, len(self.transfers)): + for ix2 in range(ix + 1, len(self.transfers)): if self.transfers[ix2].transfer.uid == mt.transfer.uid: rowspan += 1 else: @@ -133,15 +148,15 @@ class Member(db.Model): @param(deep) - whether to do a subqueryload_all and load all transfer data """ if deep: - return cls.query.options(subqueryload_all( - cls.transfers, MemberTransfer.transfer)).order_by(cls.username) + return cls.query.options( + subqueryload_all(cls.transfers, MemberTransfer.transfer) + ).order_by(cls.username) else: return cls.query.order_by(cls.username) - def _yearmonth_increment(self, ym): y, m = ym - y2, m2 = y, m+1 + y2, m2 = y, m + 1 if m2 > 12: y2 += 1 m2 = 1 @@ -156,22 +171,25 @@ class Member(db.Model): now = now_date.year * 12 + (now_date.month - 1) status = {} - status['username'] = self.username - status['alias'] = self.alias - status['type'] = self.type - status['payment_policy'] = self.payment_policy + status["username"] = self.username + status["alias"] = self.alias + status["type"] = self.type + status["payment_policy"] = self.payment_policy # First check - did we actually get any transfers? - if not self.transfers or self.transfers[0].transfer.uid == app.config['DUMMY_TRANSFER_UID']: - status['payment_status'] = PaymentStatus.never_paid.value - status['months_due'] = None - status['last_paid'] = (None, None) + if ( + not self.transfers + or self.transfers[0].transfer.uid == app.config["DUMMY_TRANSFER_UID"] + ): + status["payment_status"] = PaymentStatus.never_paid.value + status["months_due"] = None + status["last_paid"] = (None, None) if self.join_year is not None and self.join_month is not None: - status['joined'] = (self.join_year, self.join_month) - status['next_unpaid'] = status['joined'] + status["joined"] = (self.join_year, self.join_month) + status["next_unpaid"] = status["joined"] else: - status['joined'] = (None, None) - status['next_unpaid'] = (None, None) - status['left'] = False + status["joined"] = (None, None) + status["next_unpaid"] = (None, None) + status["left"] = False self._apply_judgement(status) return status @@ -181,11 +199,11 @@ class Member(db.Model): else: joined = (self.transfers[0].year, self.transfers[0].month) joined_scalar = self._yearmonth_scalar(joined) - status['joined'] = joined + status["joined"] = joined if len(self.transfers[-1].transfer.uid) == 64: - status['last_transfer_bank'] = 'mBank' + status["last_transfer_bank"] = "mBank" else: - status['last_transfer_bank'] = 'IdeaBank' + status["last_transfer_bank"] = "IdeaBank" most_recent_transfer = (0, 0) unpaid_months = 0 @@ -204,13 +222,13 @@ class Member(db.Model): most_recent_scalar = self._yearmonth_scalar(most_recent_transfer) # Is this transfer a „not a member anymore” transfer? - if this_uid == app.config['DUMMY_TRANSFER_UID']: + if this_uid == app.config["DUMMY_TRANSFER_UID"]: active_payment = False continue # Is this the first transfer? See if it was done on time if previous_uid is None: - unpaid_months += (this_scalar - joined_scalar) + unpaid_months += this_scalar - joined_scalar # Apply any missing payments if active_payment and previous_uid is not None: @@ -229,56 +247,60 @@ class Member(db.Model): # Apply missing payments from now if active_payment: previous_scalar = self._yearmonth_scalar(previous_transfer) - unpaid_months += (now - previous_scalar) + unpaid_months += now - previous_scalar - fees = app.config['MEMBERSHIP_FEES'] + fees = app.config["MEMBERSHIP_FEES"] - status['months_due'] = unpaid_months - status['money_due'] = fees.get(self.type, 0) * unpaid_months - status['payment_status'] = PaymentStatus.okay.value if unpaid_months < 4 else PaymentStatus.unpaid.value - status['last_paid'] = most_recent_transfer - status['left'] = not active_payment + status["months_due"] = unpaid_months + status["money_due"] = fees.get(self.type, 0) * unpaid_months + status["payment_status"] = ( + PaymentStatus.okay.value + if unpaid_months < 4 + else PaymentStatus.unpaid.value + ) + status["last_paid"] = most_recent_transfer + status["left"] = not active_payment if not active_payment: - status['next_unpaid'] = (now_date.year, now_date.month) + status["next_unpaid"] = (now_date.year, now_date.month) else: - status['next_unpaid'] = self._yearmonth_increment(status['last_paid']) + status["next_unpaid"] = self._yearmonth_increment(status["last_paid"]) self._apply_judgement(status) return status def get_local_email(self): - return '{}@hackerspace.pl'.format(self.username) + return "{}@hackerspace.pl".format(self.username) def get_ldap_email(self): - mra = directory.get_member_fields(g.ldap, self.username,'mailRoutingAddress') - mra = mra['mailRoutingAddress'] + mra = directory.get_member_fields(g.ldap, self.username, "mailRoutingAddress") + mra = mra["mailRoutingAddress"] if not mra: return None return mra def get_custom_email(self): - if self.preferred_email not in ['local', 'ldap', '', None]: + if self.preferred_email not in ["local", "ldap", "", None]: return self.preferred_email else: return None def uses_local_email(self): - return self.preferred_email == 'local' + return self.preferred_email == "local" def uses_ldap_email(self): - return self.preferred_email == 'ldap' + return self.preferred_email == "ldap" def uses_custom_email(self): return self.get_custom_email() is not None - def get_contact_email(self, adrtype = None): + def get_contact_email(self, adrtype=None): email = None - if (self.uses_ldap_email() and adrtype is None) or adrtype == 'ldap': + if (self.uses_ldap_email() and adrtype is None) or adrtype == "ldap": email = self.get_ldap_email() - elif (self.uses_local_email() and adrtype is None) or adrtype == 'local': + elif (self.uses_local_email() and adrtype is None) or adrtype == "local": email = self.get_local_email() - elif (self.uses_custom_email() and adrtype is None) or adrtype == 'custom': + elif (self.uses_custom_email() and adrtype is None) or adrtype == "custom": email = self.preferred_email if email is None: @@ -288,7 +310,7 @@ class Member(db.Model): def get_status(self, force_refresh=False): """It's better to call this after doing a full select of data.""" - cache_key = 'kasownik-payment_status-{}'.format(self.username) + cache_key = "kasownik-payment_status-{}".format(self.username) cache_data = cache.get(cache_key) if cache_data and cache_enabled and not force_refresh: data = json.loads(cache_data) @@ -297,41 +319,43 @@ class Member(db.Model): cache_data = self._get_status_uncached() from webapp import api + cache.delete_memoized(api.api_months_due, self.username) cache.set(cache_key, json.dumps(cache_data)) return cache_data def _apply_judgement(self, status): - if status['left']: - status['judgement'] = False + if status["left"]: + status["judgement"] = False return - policy = status['payment_policy'] - if policy == 'Normal': - if status['payment_status'] == PaymentStatus.okay.value \ - and status['last_paid'][0] is not None: - status['judgement'] = True + policy = status["payment_policy"] + if policy == "Normal": + if ( + status["payment_status"] == PaymentStatus.okay.value + and status["last_paid"][0] is not None + ): + status["judgement"] = True else: - status['judgement'] = False - elif policy == 'Extended Grace Period': - status['judgement'] = True - elif policy == 'Potato': - status['judgement'] = True - status['months_due'] = 0 + status["judgement"] = False + elif policy == "Extended Grace Period": + status["judgement"] = True + elif policy == "Potato": + status["judgement"] = True + status["months_due"] = 0 else: - status['judgement'] = False + status["judgement"] = False def get_months_due(self): status = self.get_status() - return status['months_due'] + return status["months_due"] def get_last_paid(self): status = self.get_status() - return status['last_paid'] + return status["last_paid"] def get_next_unpaid(self): status = self.get_status() - return status['next_unpaid'] - + return status["next_unpaid"] def __init__(self, _id, _username, _type, _active): self.id = _id @@ -347,7 +371,7 @@ class Member(db.Model): return self.username def __repr__(self): - return '' % self.username + return "" % self.username class Transfer(db.Model): @@ -360,7 +384,9 @@ class Transfer(db.Model): date = db.Column(db.Date) ignore = db.Column(db.Boolean) - def __init__(self, _id, _uid, _account_from, _name_from, _amount, _title, _date, _ignore): + def __init__( + self, _id, _uid, _account_from, _name_from, _amount, _title, _date, _ignore + ): self.id = _id self.uid = _uid self.account_from = _account_from @@ -374,37 +400,57 @@ class Transfer(db.Model): return self.uid[:16] def parse_title(self): - m = re.match(r"^([a-z0-9ąężźćóżłśń\-_\.]+)[ -]+(fatty|starving|superfatty|supporting|supporter)[ -]+([0-9a-z\-_ąężźćóżłśń \(\),/\.]+$)", self.title.strip().lower()) + m = re.match( + r"^([a-z0-9ąężźćóżłśń\-_\.]+)[ -]+(fatty|starving|superfatty|supporting|supporter)[ -]+([0-9a-z\-_ąężźćóżłśń \(\),/\.]+$)", + self.title.strip().lower(), + ) if not m: return (None, None, None) member, _type, title = m.group(1), m.group(2), m.group(3) - if title in [u"składka", u"opłata", u"opłata miesięczna", "skladka"]: + if title in ["składka", "opłata", "opłata miesięczna", "skladka"]: return (member, _type, None) return member, _type, title - MATCH_OK, MATCH_WRONG_TYPE, MATCH_NO_USER, MATCH_UNPARSEABLE, MATCH_KNOWN_UNPARSEABLE = range(5) + ( + MATCH_OK, + MATCH_WRONG_TYPE, + MATCH_NO_USER, + MATCH_UNPARSEABLE, + MATCH_KNOWN_UNPARSEABLE, + ) = range(5) + def get_matchability(self): title = self.parse_title() if not title[0]: similar = self.get_similar().first() if similar: - return self.MATCH_KNOWN_UNPARSEABLE, similar.member_transfers[0].member, 0 + return ( + self.MATCH_KNOWN_UNPARSEABLE, + similar.member_transfers[0].member, + 0, + ) return self.MATCH_UNPARSEABLE, self.title, 0 member_name = title[0] - member = Member.query.filter(or_(Member.username==member_name, Member.alias==member_name)).first() + member = Member.query.filter( + or_(Member.username == member_name, Member.alias == member_name) + ).first() if not member: return self.MATCH_NO_USER, member_name, 0 if title[2]: return self.MATCH_WRONG_TYPE, member, 0 - fees = app.config['MEMBERSHIP_FEES'] + fees = app.config["MEMBERSHIP_FEES"] for type_name, type_amount in fees.items(): - if title[1] == type_name and self.amount >= (type_amount*100) and (self.amount % (type_amount*100)) == 0: - return self.MATCH_OK, member, int(self.amount/(type_amount*100)) + if ( + title[1] == type_name + and self.amount >= (type_amount * 100) + and (self.amount % (type_amount * 100)) == 0 + ): + return self.MATCH_OK, member, int(self.amount / (type_amount * 100)) return self.MATCH_WRONG_TYPE, member, 0 @@ -412,9 +458,12 @@ class Transfer(db.Model): """Returns query of transfers with same account_from / name_from field.""" return Transfer.query.filter( - (Transfer.name_from.ilike(self.name_from or '') | - (Transfer.account_from == self.account_from)) & - (Transfer.member_transfers != None)).order_by(Transfer.date.desc()) + ( + Transfer.name_from.ilike(self.name_from or "") + | (Transfer.account_from == self.account_from) + ) + & (Transfer.member_transfers != None) + ).order_by(Transfer.date.desc()) def __repr__(self): - return '' % (self.uid, self.title, self.date) + return "" % (self.uid, self.title, self.date) diff --git a/web/webapp/views.py b/web/webapp/views.py index 96503df..b599cc5 100644 --- a/web/webapp/views.py +++ b/web/webapp/views.py @@ -25,29 +25,41 @@ import requests import os.path -from flask import Response, request, redirect, flash, render_template, url_for, abort, g, jsonify +from flask import ( + Response, + request, + redirect, + flash, + render_template, + url_for, + abort, + g, + jsonify, +) from flask_login import login_user, login_required, logout_user, current_user from webapp import app, forms, User, models, cache, db from . import directory -@app.route('/') -def stats(): - return render_template('stats.html') -@app.route('/cursed-plot.json') +@app.route("/") +def stats(): + return render_template("stats.html") + + +@app.route("/cursed-plot.json") def plot(): - with open(os.path.join(os.path.dirname(__file__), 'cursed-query.sql')) as fd: - cursor = db.session.execute(fd.read(), { - 'EURPLN_RATE': 4.3, - 'START_DATE': '2018-01-01' - }) + with open(os.path.join(os.path.dirname(__file__), "cursed-query.sql")) as fd: + cursor = db.session.execute( + fd.read(), {"EURPLN_RATE": 4.3, "START_DATE": "2018-01-01"} + ) result = cursor.fetchall() columns = cursor.keys() print(columns) return jsonify([dict(zip(columns, element)) for element in result]) -@app.route('/memberlist') + +@app.route("/memberlist") @login_required @cache.cached() def memberlist(): @@ -55,20 +67,22 @@ def memberlist(): result = [] for member in members: element = member.get_status() - if not element['judgement']: + if not element["judgement"]: continue result.append(element) - return render_template('memberlist.html', active_members=result) + return render_template("memberlist.html", active_members=result) -@app.route('/profile', methods=['POST', 'GET']) + +@app.route("/profile", methods=["POST", "GET"]) @login_required def self_profile(): member = current_user.get_model() if not member: abort(404) status = member.get_status() - cn = directory.get_member_fields(g.ldap, member.username, 'cn')['cn'] + cn = directory.get_member_fields(g.ldap, member.username, "cn")["cn"] - return render_template("admin_member.html", member=member, status=status, - cn=cn, admin=False) + return render_template( + "admin_member.html", member=member, status=status, cn=cn, admin=False + ) diff --git a/web/webapp/wsgi.py b/web/webapp/wsgi.py index 08bbac6..7a24cea 100755 --- a/web/webapp/wsgi.py +++ b/web/webapp/wsgi.py @@ -1,8 +1,10 @@ # FIXME we need to upgrade Flask_SQLAlchemy to get rid of deprecation warnings -#import warnings -#from flask.exthook import ExtDeprecationWarning -#warnings.simplefilter("ignore", ExtDeprecationWarning) +# import warnings +# from flask.exthook import ExtDeprecationWarning +# warnings.simplefilter("ignore", ExtDeprecationWarning) import logging + logging.basicConfig(level=logging.INFO) import webapp + app = webapp.create_app()