*: run black

This commit is contained in:
radex 2024-07-08 22:01:00 +02:00
parent 505074064d
commit f5d4c0c6e1
Signed by: radex
SSH key fingerprint: SHA256:hvqRXAGG1h89yqnS+cyFTLKQbzjWD4uXIqw7Y+0ws30
14 changed files with 541 additions and 313 deletions

View file

@ -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"])

View file

@ -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,

View file

@ -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")

View file

@ -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()

View file

@ -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/<membername>', methods=['GET', 'POST'])
@bp.route("/admin/member/<membername>", 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/<membername>/policy:<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/<membername>/membership:<membershiptype>")
@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/<membershiptype>/<username>")
@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/<path:uid>")
@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/<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:
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)

View file

@ -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/<path>.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/<year>/<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/<membername>")
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/<membername>")
@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()

View file

@ -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()

View file

@ -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.")

View file

@ -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

View file

@ -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")

View file

@ -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)

View file

@ -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 '<APIKey for %r %r>' % (self.member, self.description)
return "<APIKey for %r %r>" % (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 '<MemberTransfer %d/%d %s %r>' % (self.year, self.month, self.member, self.transfer)
return "<MemberTransfer %d/%d %s %r>" % (
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 '<Member %s>' % self.username
return "<Member %s>" % 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 '<Transfer %s %r %s>' % (self.uid, self.title, self.date)
return "<Transfer %s %r %s>" % (self.uid, self.title, self.date)

View file

@ -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
)

View file

@ -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()