*: run black
This commit is contained in:
parent
505074064d
commit
f5d4c0c6e1
14 changed files with 541 additions and 313 deletions
|
@ -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"])
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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.")
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"))
|
||||