*: 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"))