Compare commits

...

11 Commits

19 changed files with 686 additions and 664 deletions

View File

@ -1,4 +1,5 @@
Flask==0.10.1
Flask-Caching==1.3.3
Flask-Gravatar==0.4.1
Flask-Login==0.2.11
Flask-SQLAlchemy==2.0

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
@ -29,94 +29,76 @@ import requests
import sqltap.wsgi
from flask import Flask, redirect
from flask.ext.sqlalchemy import SQLAlchemy
from flask.ext.login import LoginManager, AnonymousUserMixin, login_required, current_user
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, AnonymousUserMixin, login_required, current_user
from flask_caching import Cache
from flaskext.gravatar import Gravatar
app = Flask(__name__)
app.config.from_object("config.CurrentConfig")
app.wsgi_app = sqltap.wsgi.SQLTapMiddleware(app.wsgi_app)
db = SQLAlchemy(app)
db = SQLAlchemy()
login_manager = LoginManager()
login_manager.init_app(app)
mc = memcache.Client(app.config['MEMCACHE_SERVERS'], debug=0)
cache = Cache()
gravatar = Gravatar(size=256, rating='g', default='retro',
force_default=False, use_ssl=True, base_url=None)
# TODO unsubscribe me from life
cache_enabled = False
gravatar = Gravatar(app, size=256, rating='g', default='retro', force_default=False, use_ssl=True, base_url=None)
mc = cache
import webapp.models
class AnonymousUser(AnonymousUserMixin):
def is_admin(self):
return False
import webapp.models # noqa
from webapp.auth import AnonymousUser, User
login_manager.anonymous_user = AnonymousUser
class User(object):
def __init__(self, username):
self.username = username.lower().strip()
self._admin = None
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return self.username
def is_admin(self):
if not self.is_authenticated():
return False
if self._admin is None:
r = requests.get('https://capacifier.hackerspace.pl/kasownik_access/'+
self.username)
self._admin = r.status_code == 200
return self._admin
@login_manager.user_loader
def load_user(username):
return User(username)
def admin_required(f):
@wraps(f)
def wrapper(*args, **kwargs):
if not current_user.is_admin():
return login_manager.unauthorized()
return f(*args, **kwargs)
return wrapper
import webapp.views
import webapp.api
@login_manager.unauthorized_handler
def unauthorized():
return redirect('/login')
@app.template_filter('inflect')
def inflect(v, one, two, five):
num = abs(v)
def admin_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not current_user.is_admin():
return login_manager.unauthorized()
return func(*args, **kwargs)
return wrapper
import webapp.views
import webapp.admin
import webapp.api
if num == 0:
return '%d %s' % (v, five)
elif num == 1:
return '%d %s' % (v, one)
elif num <= 4:
return '%d %s' % (v, two)
else:
return '%d %s' % (v, five)
def init():
pass
db.init_app(app)
login_manager.init_app(app)
gravatar.init_app(app)
cache.init_app(app)
# Initialize middleware
app.wsgi_app = sqltap.wsgi.SQLTapMiddleware(app.wsgi_app)
# Register blueprints
app.register_blueprint(webapp.admin.bp)
app.register_blueprint(webapp.api.bp)
# Custom filters
@app.template_filter('inflect')
def inflect(v, one, two, five):
num = abs(v)
if num == 0:
return '%d %s' % (v, five)
elif num == 1:
return '%d %s' % (v, one)
elif num <= 4:
return '%d %s' % (v, two)
return '%d %s' % (v, five)

302
web/webapp/admin.py Normal file
View File

@ -0,0 +1,302 @@
# - * - coding=utf-8 - * -
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
from flask_login import login_required
from webapp import forms, db, models, admin_required
import directory
import logic
bp = Blueprint('admin', __name__)
@bp.route("/admin")
@admin_required
@login_required
def index():
members = [m.get_status() for m in models.Member.get_members(True)]
for member in members:
due = member['months_due']
if due < 1:
member['color'] = "00FF00"
elif due < 3:
member['color'] = "E0941B"
else:
member['color'] = "FF0000"
active_members = filter(lambda m: m['judgement'], members)
inactive_members = filter(lambda m: not m['judgement'], members)
diff = directory.get_ldap_group_diff(members)
if diff is not None:
flash("LDAP sync required")
return render_template("admin_index.html",
active_members=active_members,
inactive_members=inactive_members)
@bp.route("/admin/ldapsync", methods=["POST", "GET"])
@admin_required
@login_required
def admin_ldap_sync():
members = [m.get_status() for m in models.Member.get_members(True)]
diff = directory.get_ldap_group_diff(members)
if diff is None:
return render_template("admin_ldap_sync.html", form=False)
form = forms.LDAPSyncForm(request.form)
form.fatty_to_add.choices = zip(diff['fatty_to_add'], diff['fatty_to_add'])
form.fatty_to_add.default = diff['fatty_to_add']
form.fatty_to_remove.choices = zip(diff['fatty_to_remove'], diff['fatty_to_remove'])
form.fatty_to_remove.default = diff['fatty_to_remove']
form.starving_to_add.choices = zip(diff['starving_to_add'], diff['starving_to_add'])
form.starving_to_add.default = diff['starving_to_add']
form.starving_to_remove.choices = zip(diff['starving_to_remove'], diff['starving_to_remove'])
form.starving_to_remove.default = diff['starving_to_remove']
form.process(request.form)
if form.validate():
changes = {'fatty': {}, 'starving': {}}
changes['fatty']['add'] = form.fatty_to_add.data
changes['fatty']['remove'] = form.fatty_to_remove.data
changes['starving']['add'] = form.starving_to_add.data
changes['starving']['remove'] = form.starving_to_remove.data
directory.update_member_groups(g.ldap, changes)
return render_template("admin_ldap_sync.html", form=form)
@bp.route("/admin/csv")
@admin_required
@login_required
def admin_csv():
members = []
for m in models.Member.get_members(True):
member = m.get_status()
if member['type'] == 'supporting':
continue
member['contact_email'] = m.get_contact_email()
member['cn'] = directory.get_member_fields(g.ldap, member['username'], 'cn')['cn']
members.append(member)
active_members = filter(lambda m: m['judgement'], members)
output = render_template("admin_csv.html", active_members=active_members)
return Response(output)
@bp.route('/admin/member/<membername>')
@login_required
@admin_required
def admin_member(membername):
member = models.Member.get_members(True).filter_by(username=membername).first()
if not member:
abort(404)
status = member.get_status()
cn = directory.get_member_fields(g.ldap, member.username, 'cn')['cn']
return render_template("admin_member.html", member=member, status=status,
cn=cn, admin=True)
@bp.route("/admin/member/<membername>/policy:<policy>")
@login_required
@admin_required
def admin_member_set_policy(membername, policy):
member = models.Member.query.filter_by(username=membername).first()
member.payment_policy = models.PaymentPolicy[policy].value
db.session.add(member)
db.session.commit()
return redirect(request.referrer)
@bp.route("/admin/member/<membername>/membership:<membershiptype>")
@login_required
@admin_required
def admin_member_set_membership(membername, membershiptype):
member = models.Member.query.filter_by(username=membername).first()
member.type = models.MembershipType[membershiptype].name
db.session.add(member)
db.session.commit()
return redirect(request.referrer)
@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)
db.session.add(member)
db.session.commit()
flash('Member created')
return redirect(request.referrer)
@bp.route("/admin/match")
@login_required
@admin_required
def match_index():
transfers_unmatched = logic.get_unmatched_transfers()
return render_template("match.html", transfers_unmatched=transfers_unmatched)
@bp.route("/admin/match/auto", methods=["GET"])
@login_required
@admin_required
def match_auto():
matched = 0
left = 0
transfers_unmatched = logic.get_unmatched_transfers()
affected_members = []
for transfer in transfers_unmatched:
matchability, member, months = transfer.get_matchability()
try:
print "[i] Matching transfer {} for {:.2f}PLN by member {}, {} months".format(
transfer.id, transfer.amount/100, member.username, months)
except AttributeError:
print "[e] Member data invalid, WTF - {}".format(repr(member))
continue
if matchability == models.Transfer.MATCH_OK:
if member.transfers:
year, month = member.get_next_unpaid()
if None in (year, month):
print "[w] next_unpaid borked, skipping"
continue
else:
year, month = transfer.date.year, transfer.date.month
for m in range(months):
mt = models.MemberTransfer(None, year, month, transfer)
member.transfers.append(mt)
db.session.add(mt)
flash("Matched transfer {} for {:.2f}PLN to member {} for month {}-{}".format(transfer.id, transfer.amount/100, member.username, year, month))
year, month = member._yearmonth_increment((year,month))
matched += 1
affected_members.append(member)
else:
left += 1
db.session.commit()
for member in affected_members:
member.get_status(force_refresh=True)
flash("Matched %i, %i left" % (matched, left))
return redirect(url_for(".match_index"))
@bp.route("/admin/match/manual", methods=["GET"])
@login_required
@admin_required
def match_manual():
transfers_unmatched = logic.get_unmatched_transfers()
return render_template("match_manual.html", transfers_unmatched=transfers_unmatched)
@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')
return redirect(url_for(".match_manual"))
transfer.ignore = True
db.session.commit()
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')
return redirect(url_for(".match_manual"))
transfer = models.Transfer.query.filter_by(uid=uid).first()
if not transfer:
flash('No transfer found', 'danger')
return redirect(url_for(".match_manual"))
for _ in range(months):
year, month = member.get_next_unpaid()
mt = models.MemberTransfer(None, year, month, transfer)
member.transfers.append(mt)
db.session.add(mt)
db.session.commit()
member.get_status(force_refresh=True)
flash('OK, %d get' % transfer.amount)
return redirect(url_for(".match_manual"))
@bp.route("/admin/match/", methods=["POST"])
@login_required
@admin_required
def match_user_transfer():
username = request.form["username"]
uid = request.form["uid"]
member = models.Member.query.filter_by(username=username).first()
if not member:
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')
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.query.filter_by(
active=True, payment_policy=models.PaymentPolicy.normal.value).all()
form = forms.SpamForm()
form.members.choices = [(member.id, member) for member in members]
form.members.default = [member.id for member in members]
form.process(request.form)
if form.validate():
spam = []
for member in members:
if member.id not in form.members.data:
continue
content = render_template(
'mailing/due.txt',
member=member,
status=member.get_status(),
transfers=member.transfers[:5],
now=now)
# Just ignore empty messages
if not content.strip():
continue
msg = MIMEText(content, "plain", "utf-8")
msg["From"] = "Kasownik Hackerspace'owy <kasownik@hackerspace.pl>"
msg["Subject"] = "Stan składek na dzień %s" % now.strftime("%d/%m/%Y")
msg["To"] = member.get_contact_email()
spam.append(msg)
if form.dry_run.data:
readable = [
msg.as_string().split('\n\n')[0] + '\n\n'
+ msg.get_payload(decode=True) for msg in spam]
return Response('\n====\n'.join(readable), mimetype='text/text')
for msg in spam:
p = Popen(["/usr/sbin/sendmail", "-t"], stdin=PIPE)
p.communicate(msg.as_string())
flash('%d messages sent!' % len(spam))
return redirect(url_for('.index'))
return render_template('admin_spam.html', form=form)

View File

@ -12,7 +12,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
@ -26,12 +26,16 @@
import hmac
import json
import datetime
import logging
from functools import wraps
from sqlalchemy import and_
from flask import request, abort, Response
from flask import request, abort, Response, Blueprint
from webapp import models, app, mc
from webapp import models, app, cache
logger = logging.getLogger(__name__)
bp = Blueprint('api', __name__)
class APIError(Exception):
def __init__(self, message, code=500):
@ -50,27 +54,23 @@ def _public_api_method(path):
content = original(*args, **kwargs)
status = "ok"
code = 200
except APIError as e:
content = e.message
code = e.code
except APIError as exc:
content = exc.message
code = exc.code
status = "error"
except Exception as e:
raise
content = "Internal server error."
code = 500
status = "error"
last_transfer = models.Transfer.query.order_by(models.Transfer.date.desc()).first()
modified = str(last_transfer.date)
r = {}
r["status"] = status
r["content"] = content
r["modified"] = modified
return Response(json.dumps(r), mimetype="application/json"), code
return app.route("/api/" + path + ".json", methods=["GET"])(wrapper_json)
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
}
return Response(json.dumps(resp), mimetype="application/json"), code
return bp.route("/api/" + path + ".json", methods=["GET"])(wrapper_json)
return decorator2
def _private_api_method(path):
"""A decorator that adds a private, HMACed, POST based method at /api/path.
@ -110,13 +110,12 @@ def _private_api_method(path):
request.decoded = json.loads(request.data.decode("base64"))
else:
request.decoded = {}
except Exception as e:
print request.data
print e
except Exception:
logger.exception('Request decode failed')
abort(400)
return json.dumps(original(*args, **kwargs))
return app.route("/api/" + path, methods=["POST"])(wrapper)
return bp.route("/api/" + path, methods=["POST"])(wrapper)
return decorator
@_private_api_method("list_members")
@ -156,12 +155,8 @@ def api_member():
return response
@cache.memoize()
def _stats_for_month(year, month):
cache_key = 'kasownik-stats_for_month-{}-{}'.format(year, month)
cache_data = mc.get(cache_key)
if cache_data:
cache_data = json.loads(cache_data)
return cache_data[0], cache_data[1]
# TODO: export this to the config
money_required = 4217+615+615
money_paid = 0
@ -171,7 +166,6 @@ def _stats_for_month(year, month):
amount_all = mt.transfer.amount
amount = amount_all / len(mt.transfer.member_transfers)
money_paid += amount
mc.set(cache_key, json.dumps([money_required, money_paid/100]))
return money_required, money_paid/100
@_public_api_method("month/<year>/<month>")
@ -180,7 +174,7 @@ def api_month(year=None, month=None):
return dict(required=money_required, paid=money_paid)
@_public_api_method("mana")
def api_manamana(year=None, month=None):
def api_manamana():
"""To-odee doo-dee-doo!"""
now = datetime.datetime.now()
money_required, money_paid = _stats_for_month(now.year, now.month)
@ -195,11 +189,8 @@ def api_judgement(membername):
return judgement
@_public_api_method("months_due/<membername>")
@cache.memoize()
def api_months_due(membername):
cache_key = 'kasownik-months_due-{}'.format(membername)
cache_data = mc.get(cache_key)
if cache_data:
return cache_data
member = models.Member.query.filter_by(username=membername).first()
if not member:
raise APIError("No such member.", 404)
@ -211,26 +202,17 @@ def api_months_due(membername):
if year and member.active == False:
raise APIError("No longer a member.", 410)
due = member.get_months_due()
#now = datetime.datetime.now()
#then_timestamp = year * 12 + (month-1)
#now_timestamp = now.year * 12 + (now.month-1)
mc.set(cache_key, due)
return due
@_public_api_method("cashflow/<int:year>/<int:month>")
@cache.memoize()
def api_cashflow(year, month):
cache_key = 'kasownik-cashflow-{}-{}'.format(year, month)
cache_data = mc.get(cache_key)
if cache_data:
amount_in = cache_data
else:
start = datetime.date(year=year, month=month, day=1)
month += 1
if month > 12:
month = 1
year += 1
end = datetime.date(year=year, month=month, day=1)
transfers = models.Transfer.query.filter(and_(models.Transfer.date >= start, models.Transfer.date < end, models.Transfer.ignore == False)).all()
amount_in = sum(t.amount for t in transfers)
mc.set(cache_key, amount_in)
start = datetime.date(year=year, month=month, day=1)
month += 1
if month > 12:
month = 1
year += 1
end = datetime.date(year=year, month=month, day=1)
transfers = models.Transfer.query.filter(and_(models.Transfer.date >= start, models.Transfer.date < end, models.Transfer.ignore == False)).all()
amount_in = sum(t.amount for t in transfers)
return {"in": amount_in/100, "out": -1}

39
web/webapp/auth.py Normal file
View File

@ -0,0 +1,39 @@
import requests
from flask_login import AnonymousUserMixin
from webapp import models
class AnonymousUser(AnonymousUserMixin):
def is_admin(self):
return False
class User(object):
def __init__(self, username):
self.username = username.lower().strip()
self._admin = None
def is_authenticated(self):
return True
def is_active(self):
return True
def is_anonymous(self):
return False
def get_id(self):
return self.username
def is_admin(self):
if not self.is_authenticated():
return False
if self._admin is None:
req = requests.get(
'https://capacifier.hackerspace.pl/kasownik_access/%s'
% self.username)
self._admin = req.status_code == 200
return self._admin
def get_model(self, deep=True):
return models.Member.get_members(deep) \
.filter_by(username=self.username).first()

View File

@ -38,16 +38,22 @@ def connect():
app.config['LDAP_BIND_PASSWORD'])
return c
if not app.config.get('DISABLE_LDAP'):
@app.before_request
def _setup_ldap():
@app.before_request
def _setup_ldap():
if not app.config.get('DISABLE_LDAP'):
g.ldap = connect()
else:
g.ldap = None
@app.teardown_request
def _destroy_ldap(exception=None):
@app.teardown_request
def _destroy_ldap(exception=None):
if g.ldap:
g.ldap.unbind_s()
def get_ldap_group_diff(members):
if app.config.get('DISABLE_LDAP'):
return None
active_members = 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']])
@ -118,6 +124,10 @@ def get_group_members(c, group):
return members
def get_member_fields(c, member, fields):
if app.config.get('DISABLE_LDAP'):
import collections
return collections.defaultdict(str)
if isinstance(fields, str):
fields = [fields,]
fields_needed = set(fields)

View File

@ -29,16 +29,16 @@ class MultiCheckboxField(SelectMultipleField):
widget = widgets.ListWidget(prefix_label=False)
option_widget = widgets.CheckboxInput()
class LoginForm(Form):
class LoginForm(FlaskForm):
username = TextField('Username', [validators.Required()])
password = PasswordField('Password', [validators.Required()])
class ContactEmailSettingsForm(Form):
class ContactEmailSettingsForm(FlaskForm):
local = BooleanField("")
ldap = BooleanField("")
custom = TextField("Custom address:")
class LDAPSyncForm(Form):
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=[])

View File

@ -35,7 +35,7 @@ from sqlalchemy.orm import subqueryload_all
from sqlalchemy.sql.expression import or_
from flask import g
from webapp import app, db, mc, cache_enabled
from webapp import app, db, cache, cache_enabled
import directory
@ -61,8 +61,10 @@ class MemberTransfer(db.Model):
self.year = year
self.month = month
self.transfer = transfer
mc.delete('kasownik-stats_for_month-{}-{}'.format(year, month))
mc.delete('kasownik-cashflow-{}-{}'.format(year, month))
from webapp import api
cache.delete_memoized(api._stats_for_month, year, month)
cache.delete_memoized(api.api_cashflow, year, month)
class PaymentStatus(enum.Enum):
@ -85,7 +87,9 @@ 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)])
transfers = db.relationship("MemberTransfer", order_by=[
db.asc(MemberTransfer.year), db.asc(MemberTransfer.month)])
# old field
active = db.Column(db.Boolean)
api_keys = db.relationship("APIKey")
@ -122,15 +126,15 @@ class Member(db.Model):
return rowspan + 1
@classmethod
def get_members(kls, deep=False):
def get_members(cls, deep=False):
"""Gets all members as an SQLAlchemy query.
@param(deep) - whether to do a subqueryload_all and load all transfer data
"""
if deep:
return kls.query.options(subqueryload_all(kls.transfers,
MemberTransfer.transfer)).order_by(kls.username)
return cls.query.options(subqueryload_all(
cls.transfers, MemberTransfer.transfer)).order_by(cls.username)
else:
return kls.query.order_by(kls.username)
return cls.query.order_by(cls.username)
def _yearmonth_increment(self, ym):
@ -220,7 +224,6 @@ class Member(db.Model):
previous_transfer = this_transfer
previous_uid = this_uid
# Apply missing payments from now
if active_payment:
previous_scalar = self._yearmonth_scalar(previous_transfer)
@ -284,17 +287,19 @@ class Member(db.Model):
return email
def get_status(self, force_refresh = False):
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_data = mc.get(cache_key)
cache_data = cache.get(cache_key)
if cache_data and cache_enabled and not force_refresh:
data = json.loads(cache_data)
return data
else:
cache_data = self._get_status_uncached()
mc.delete('kasownik-months_due-{}'.format(self.username))
mc.set(cache_key, json.dumps(cache_data))
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):
@ -304,7 +309,8 @@ class Member(db.Model):
return
policy = status['payment_policy']
if policy == 'Normal':
if status['payment_status'] == PaymentStatus.okay.value and status['last_paid'][0] is not None:
if status['payment_status'] == PaymentStatus.okay.value \
and status['last_paid'][0] is not None:
status['judgement'] = True
else:
status['judgement'] = False
@ -384,7 +390,7 @@ class Transfer(db.Model):
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

View File

@ -5,112 +5,68 @@
<div class="container">
<div class="row">
<div class="col-md-2 operations">
<h4>Active operations:</h4>
<div class="col-md-3 operations">
<h4>Available operations:</h4>
<p>
<a href="/admin/match"><b>Match transfers</b></a>
<a href="/admin/ldapsync"><b>Synchronize LDAP groups</b></a>
<a href="/admin/spam"><b>Spam members</b></a>
</p>
<ul class="list-group">
<a href="/admin/match" class="list-group-item">Match transfers</a>
<a href="/admin/ldapsync" class="list-group-item">Synchronize LDAP groups</a>
<a href="/admin/spam" class="list-group-item">Spam members</a>
</ul>
</div>
<div class="col-md-10">
<div class="col-md-9">
{% block admin_content %}
<div class="row">
{% for group in active_members|groupby("type") %}
<div class="col-md-6">
<h2>Active members, {{ group.grouper }}:</h2>
<table class="table table-striped">
<tr>
<th>#</th>
<th>LDAP Username</th>
<th>Months Due</th>
<th>Payment Policy</th>
</tr>
{% for member in group.list %}
<tr>
<td>{{loop.index}}.</td>
<td>
<a href="/admin/member/{{member.username}}">
<b>{{member.username}}</b>
</a>
</td>
<td>
<span class="badge" style="background-color: #{{member.color}}">
{{member.months_due}}
</span>
{% if member.last_transfer_bank != 'IdeaBank' %}
<span class="badge" style="background-color: red">
{{member.last_transfer_bank}}
</span>
{% endif %}
</td>
<td>{% include "button_payment_policy.html" %}</td>
</tr>
{% endfor %}
</table>
<h2>Active members, {{ group.grouper }}:</h2>
{{ members_list(group.list) }}
</div>
{% endfor %}
<div class="col-md-6">
<h2>Inactive-wannabes:</h2>
<table class="table table-striped">
<tr>
<th>#</th>
<th>LDAP Username</th>
<th>Months Due</th>
<th>Payment Policy</th>
</tr>
{% for member in inactive_members|selectattr("months_due") %}
<tr>
<td>{{loop.index}}.</td>
<td>
<a href="/admin/member/{{member.username}}">
<b>{{member.username}}</b>
</a>
</td>
<td>
<span class="badge" style="background-color: #{{member.color}}">
{{member.months_due}}
</span>
{% if member.last_transfer_bank != 'IdeaBank' %}
<span class="badge" style="background-color: red">
{{member.last_transfer_bank}}
</span>
{% endif %}
</td>
<td>{% include "button_payment_policy.html" %}</td>
</tr>
{% endfor %}
</table>
{{ members_list(inactive_members|selectattr("months_due")) }}
</div>
<div class="col-md-6">
<h2>Inactive members:</h2>
<table class="table table-striped">
<tr>
<th>#</th>
<th>LDAP Username</th>
<th>Months Due</th>
<th>Payment Policy</th>
</tr>
{% for member in inactive_members|rejectattr("months_due") %}
<tr>
<td>{{loop.index}}.</td>
<td>
<a href="/admin/member/{{member.username}}">
<b>{{member.username}}</b>
</a>
</td>
<td>
<span class="badge" style="background-color: #{{member.color}}">
{{member.months_due}}
</span>
</td>
<td>{% include "button_payment_policy.html" %}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
<h2>Inactive members:</h2>
{{ members_list(inactive_members|rejectattr("months_due")) }}
</div>
{% endblock %}
</div>
</div>
</div>
</div>
{% endblock %}
{% macro members_list(members) %}
<table class="table table-striped">
<thead>
<tr>
<th>#</th>
<th>LDAP Username</th>
<th>Months Due</th>
<th>Payment Policy</th>
</tr>
</thead>
{% for member in members %}
<tr>
<td>{{loop.index}}.</td>
<td>
<a href="/admin/member/{{member.username}}">
<b>{{member.username}}</b>
</a>
</td>
<td>
<span class="badge" style="background-color: #{{member.color}}">
{{member.months_due}}
</span>
{% if member.active and member.last_transfer_bank != 'IdeaBank' %}
<span class="badge" style="background-color: red">
{{member.last_transfer_bank}}
</span>
{% endif %}
</td>
<td>{% include "button_payment_policy.html" %}</td>
</tr>
{% endfor %}
</table>
{% endmacro %}

View File

@ -1,52 +1,35 @@
{% extends "root.html" %}
{% extends "admin_index.html" %}
{% set active_page = "admin" %}
{% block title %}Admin LDAP Sync{% endblock %}
{% block content %}
<div class="container">
<div class="row">
<div class="col-md-2 operations">
<h4>Active operations:</h4>
<h4>Available operations:</h4>
<p>
<a href="/admin/match"><b>Match transfers</b></a>
<a href="/admin/ldapsync"><b>Synchronize LDAP groups</b></a>
</p>
{% block admin_content %}
{% if not form %}
<h2>No sync required - groups are up to date.</h2>
{% else %}
<form method="post" action="/admin/ldapsync">
{{ form.hidden_tag() }}
<div class="col-md-12">
<input type="submit" value="Sync" />
</div>
<div class="col-md-10">
<div class="row">
{% if not form %}
<div class="col-md-12">
<h2>No sync required - groups are up to date.</h2>
</div>
{% else %}
<form method="post" action="/admin/ldapsync">
<div class="col-md-12">
<input type="submit" value="Sync" />
</div>
<div class="col-md-3">
<h2>Fatty to add:</h2>
{{ form.fatty_to_add() }}
</div>
<div class="col-md-3">
<h2>Fatty to remove:</h2>
{{ form.fatty_to_remove() }}
</div>
<div class="col-md-3">
<h2>Starving to add:</h2>
{{ form.starving_to_add() }}
</div>
<div class="col-md-3">
<h2>Starving to remove:</h2>
{{ form.starving_to_remove() }}
</div>
<div class="col-md-12">
<input type="submit" value="Sync" />
</div>
</form>
{% endif %}
</div>
</div>
</div>
</div>
<div class="col-md-3">
<h2>Fatty to add:</h2>
{{ form.fatty_to_add() }}
</div>
<div class="col-md-3">
<h2>Fatty to remove:</h2>
{{ form.fatty_to_remove() }}
</div>
<div class="col-md-3">
<h2>Starving to add:</h2>
{{ form.starving_to_add() }}
</div>
<div class="col-md-3">
<h2>Starving to remove:</h2>
{{ form.starving_to_remove() }}
</div>
<div class="col-md-12">
<input type="submit" value="Sync" />
</div>
</form>
{% endif %}
{% endblock %}

View File

@ -72,6 +72,7 @@
</p>
<h3>Transfers</h3>
<table class="table table-striped">
<thead>
<tr>
<th>UID</th>
<th>Amount</th>
@ -79,6 +80,7 @@
<th>Date</th>
<th>Covers</th>
</tr>
</thead>
{% set rowspan_left = -1 %}
{% for mt in member.transfers %}
{% if mt.transfer.uid != "NOTAMEMBER" %}

View File

@ -1,19 +1,17 @@
{% extends "root.html" %}
{% extends "admin_index.html" %}
{% set active_page = "admin" %}
{% block title %}Admin LDAP Sync{% endblock %}
{% block content %}
{% block title %}Admin Spam Mailing™{% endblock %}
<div class="container">
<div class="row">
<div class="col-md-12">
<form method="post" action="/admin/spam/">
<button>Spam</button>
{% block admin_content %}
<form method="post" action="/admin/spam/" class="form-inline">
{{ form.hidden_tag() }}
<button class="btn btn-warning">Spam</button>
<div class="checkbox">
{{ form.dry_run() }} {{ form.dry_run.label }}
{{ form.hidden_tag() }}
{{ form.members() }}
</form>
</div>
</div>
</div>
</div>
<p>
{{ form.members() }}
</p>
</form>
{% endblock %}

View File

@ -2,12 +2,13 @@
{% block title %}Sign in{% endblock %}
{% block content %}
<div class="container">
<form class="form-signin" method="post" action="/login">
<label for="inputEmail" class="sr-only">Username</label>
<input type="text" id="username" class="form-control" name="username" placeholder="Username" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="password" class="form-control" name="password" placeholder="Password" required>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<form class="form-signin" method="post" action="/login">
{{ form.hidden_tag() }}
<label for="inputEmail" class="sr-only">Username</label>
<input type="text" id="username" class="form-control" name="username" placeholder="Username" required autofocus>
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" id="password" class="form-control" name="password" placeholder="Password" required>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</div>
{% endblock %}

View File

@ -1,28 +1,32 @@
{% extends "root.html" %}
{% extends "admin_index.html" %}
{% block title %}Match transfers{% endblock %}
{% block content %}
{% block admin_content %}
<div class="container">
<h2>Match transfers</h2>
<h2>Matching operations</h2>
<a href="/admin/match/auto">Match all easily matchable transfers</a><br />
<a href="/admin/match/manual">Match manually all unmatched transfers</a><br />
<h2>Unmatched transfers - troublesome</h2>
<ul>
{% for t in transfers_unmatched %}
{% if t.get_matchability()[0] > 0 %}
<li>{{ t.title }}</li>
{% endif %}
{% endfor %}
</ul>
<h2>Unmatched transfers - easily matchable</h2>
<ul>
{% for t in transfers_unmatched %}
{% set details = t.get_matchability() %}
{% if details[0] == 0 %}
<li><em>{{ t.title }}</em>: {{ details[2] }} months from {{ details[1].get_next_unpaid() }} for {{ details[1].username }}</li>
{% endif %}
{% endfor %}
</ul>
</div>
<p>
<h3>Matching operations</h3>
<a href="/admin/match/auto">Match all easily matchable transfers</a><br />
<a href="/admin/match/manual">Match manually all unmatched transfers</a><br />
</p>
<p>
<h3>Unmatched transfers - troublesome</h3>
<ul>
{% for t in transfers_unmatched %}
{% if t.get_matchability()[0] > 0 %}
<li>{{ t.title }}</li>
{% endif %}
{% endfor %}
</ul>
</p>
<p>
<h3>Unmatched transfers - easily matchable</h3>
<ul>
{% for t in transfers_unmatched %}
{% set details = t.get_matchability() %}
{% if details[0] == 0 %}
<li><em>{{ t.title }}</em>: {{ details[2] }} months from {{ details[1].get_next_unpaid() }} for {{ details[1].username }}</li>
{% endif %}
{% endfor %}
</ul>
</p>
{% endblock %}

View File

@ -1,7 +1,6 @@
{% extends "root.html" %}
{% extends "admin_index.html" %}
{% block title %}manual match{% endblock %}
{% block content %}
<div class="container">
{% block admin_content %}
<script>
function payment(months, uid, username)
{
@ -18,48 +17,76 @@
{% set extra = t.get_matchability()[1] %}
{% if matchability > 0 %}
<div id="uid-{{t.uid}}">
<h2>{{t.title}}</h2>
Amount: {{t.amount/100}} <br />
Title: {{t.title}} <br />
<div id="uid-{{t.uid}}" class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">
{{t.title}} <small>({{t.amount/100}} PLN)</small>
<a href="{{ url_for("admin.ignore", uid=t.uid) }}" title="Ignore this transfer" class="label label-default pull-right" style="color: white; margin-left:5px">&times;</a>
{% if matchability == 1 %}
<span class="label label-danger pull-right">unknown type</span>
{% elif matchability == 2 %}
<span class="label label-danger pull-right">unknown member</span>
{% elif matchability == 3 %}
<span class="label label-danger pull-right">unparseable</span>
{% endif %}
</h3>
</div>
{% if matchability == 1 %}
<h3>Previous transfers...</h3>
<ul>
{% for member_transfer in extra.transfers %}
<li>{{ member_transfer.transfer.title }} for <b>{{ member_transfer.transfer.amount/100 }}</b> </li>
{% endfor %}
</ul>
<!--<div class="panel-body">
</div>
-->
<table class="table">
<thead>
<tr><th>Previous transfers</th></tr>
</thead>
{% for member_transfer in extra.transfers[:3] %}
<tr><td>{{ member_transfer.transfer.title }} for <b>{{ member_transfer.transfer.amount/100 }}</b> </td></tr>
{% else %}
<tr><td>Nothing...</td></tr>
{% endfor %}
</table>
{% set fattycount = t.amount/10000 %}
{% set starvingcount = t.amount/5000 %}
<b>Unknown type... </b><br />
<a href="javascript:payment(1, '{{t.uid}}', '{{ extra.username }}')">This is a one-time payment.</a><br />
<a href="javascript:payment({{fattycount|int}}, '{{t.uid}}', '{{ extra.username }}')">This is a {{fattycount|int}}-time payment. (fatty)</a><br />
<a href="javascript:payment({{starvingcount|int}}, '{{t.uid}}', '{{ extra.username }}')">This is a {{starvingcount|int}}-time payment. (starving)</a><br />
<a href="javascript:payment(prompt('months?', '0'), '{{t.uid}}', '{{ extra.username }}')">This is a N-time payment. (superfatty?)</a><br />
<div class="list-group">
<a href="javascript:payment(1, '{{t.uid}}', '{{ extra.username }}')" class="list-group-item">This is a one-time payment.</a>
<a href="javascript:payment({{fattycount|int}}, '{{t.uid}}', '{{ extra.username }}')" class="list-group-item">This is a {{fattycount|int}}-time payment. (fatty)</a>
<a href="javascript:payment({{starvingcount|int}}, '{{t.uid}}', '{{ extra.username }}')" class="list-group-item">This is a {{starvingcount|int}}-time payment. (starving)</a>
<a href="javascript:payment(prompt('months?', '0'), '{{t.uid}}', '{{ extra.username }}')" class="list-group-item">This is a N-time payment. (superfatty?)</a>
</div>
{% elif matchability == 2 %}
<b>Unknown member...</b><br />
<a href="/admin/member/add/starving/{{extra}}">Add member {{extra}} - starving</a><br />
<a href="/admin/member/add/fatty/{{extra}}">Add member {{extra}} - fatty</a><br />
<a href="/admin/member/add/supporting/{{extra}}">Add member {{extra}} - supporting</a>
<div class="list-group">
<a href="/admin/member/add/starving/{{extra}}" class="list-group-item">Add member <b>{{extra}}</b> - starving</a>
<a href="/admin/member/add/fatty/{{extra}}" class="list-group-item">Add member <b>{{extra}}</b> - fatty</a>
<a href="/admin/member/add/supporting/{{extra}}" class="list-group-item">Add member <b>{{extra}}</b> - supporting</a>
</div>
<div class="panel-body">
<form method="POST" action="/admin/match/">
username: <input name="username" />
<div class="input-group">
<input type="text" class="form-control" placeholder="Username" name="username" />
<span class="input-group-btn">
<button class="btn btn-default" type="button">Match other username</button>
</span>
</div>
<input type="hidden" name="uid" value="{{t.uid}}" />
<input type="submit" value="match other username" />
</form>
</div>
{% elif matchability == 3 %}
<b>Unparseable...</b>
<div class="panel-body">
<form method="POST" action="/admin/match/">
username: <input name="username" />
<div class="input-group">
<input type="text" class="form-control" placeholder="Username" name="username" />
<span class="input-group-btn">
<button class="btn btn-default" type="button">Match</button>
</span>
</div>
<input type="hidden" name="uid" value="{{t.uid}}" />
<input type="submit" value="match" />
</form>
</div>
{% endif %}
</div>
{% endif %}
{% endfor %}
</div>
{% endblock %}

View File

@ -5,6 +5,8 @@
<script>
function payment(months, uid, username)
{
window.location = "/admin/match/" + username + "/" + months + "/" + uid;
return; // FIXME
var xhr = new XMLHttpRequest();
xhr.open("GET", "/admin/match/" + username + "/" + months + "/" + uid, true);
xhr.send();

View File

@ -7,14 +7,17 @@
<div class="col-md-4">
<h1>Active Members</h1>
<p>Membership fees in order, has full access to the space and private mailing lists.</p>
<small>Total: {{ active_members|count }}</small>
</div>
<div class="col-md-8">
<table class="table">
<thead>
<tr>
<th>Name</th>
<th>Membership type</th>
<th>Member since</th>
</tr>
</thead>
{% for member in active_members %}
<tr>
<td>{{member['username']}}</td>

View File

@ -63,11 +63,11 @@
</div>
</nav>
{% with messages = get_flashed_messages() %}
{% with messages = get_flashed_messages(with_categories=true) %}
{% if messages %}
<div class="flashes">
{% for message in messages %}
<div class="alert alert-info alert-dismissible" role="alert">
{% for category, message in messages %}
<div class="alert alert-{{ 'info' if category == 'message' else category }} alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
{{message}}
</div>
@ -76,7 +76,7 @@
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
{% block content %}{% endblock %}
<!-- Bootstrap core JavaScript
================================================== -->

View File

@ -13,7 +13,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
@ -24,20 +24,13 @@
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
import sys
import datetime
import json
import requests
import re
from email.mime.text import MIMEText
from subprocess import Popen, PIPE
from webapp import app, forms, User, db, models, mc, cache_enabled, admin_required
from flask.ext.login import login_user, login_required, logout_user, current_user
from flask import Response, request, redirect, flash, render_template, url_for, abort, g
import logic
from flask_login import login_user, login_required, logout_user, current_user
from webapp import app, forms, User, models, cache
import directory
import traceback
@app.route('/')
def stats():
@ -45,314 +38,45 @@ def stats():
@app.route('/memberlist')
@login_required
@cache.cached()
def memberlist():
cache_key = 'kasownik-view-memberlist'
cache_data = mc.get(cache_key)
if not cache_data or not cache_enabled:
members = models.Member.get_members(True)
cache_data = []
for member in members:
element = member.get_status()
if not element['judgement']:
continue
cache_data.append(element)
mc.set(cache_key, cache_data)
return render_template('memberlist.html',
active_members=cache_data)
members = models.Member.get_members(True)
result = []
for member in members:
element = member.get_status()
if not element['judgement']:
continue
result.append(element)
return render_template('memberlist.html', active_members=result)
@app.route('/profile', methods=['POST', 'GET'])
@login_required
def self_profile():
member = models.Member.get_members(True).filter_by(username=current_user.username).first()
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']
#cesform = forms.ContactEmailSettingsForm(request.form)
#if request.method == "POST" and cesform.validate():
# pe = request.form['preferred_email']
# member.preferred_email = request.form['preferred_email']
#db.session.add(member)
#db.session.commit()
return render_template("admin_member.html", member=member, status=status,
cn=cn, admin=False)
@app.route("/admin")
@admin_required
@login_required
def admin_index():
members = [m.get_status() for m in models.Member.get_members(True)]
for member in members:
due = member['months_due']
if due < 1:
member['color'] = "00FF00"
elif due < 3:
member['color'] = "E0941B"
else:
member['color'] = "FF0000"
active_members = filter(lambda m: m['judgement'], members)
inactive_members = filter(lambda m: not m['judgement'], members)
diff = directory.get_ldap_group_diff(members)
if diff is not None:
flash("LDAP sync required")
return render_template("admin_index.html",
active_members=active_members,
inactive_members=inactive_members)
@app.route("/admin/ldapsync", methods=["POST", "GET"])
@admin_required
@login_required
def admin_ldap_sync():
members = [m.get_status() for m in models.Member.get_members(True)]
diff = directory.get_ldap_group_diff(members)
if diff is None:
return render_template("admin_ldap_sync.html", form=False)
form = forms.LDAPSyncForm(request.form)
form.fatty_to_add.choices = zip(diff['fatty_to_add'],diff['fatty_to_add'])
form.fatty_to_add.default = diff['fatty_to_add']
form.fatty_to_remove.choices = zip(diff['fatty_to_remove'],diff['fatty_to_remove'])
form.fatty_to_remove.default = diff['fatty_to_remove']
form.starving_to_add.choices = zip(diff['starving_to_add'],diff['starving_to_add'])
form.starving_to_add.default = diff['starving_to_add']
form.starving_to_remove.choices = zip(diff['starving_to_remove'],diff['starving_to_remove'])
form.starving_to_remove.default = diff['starving_to_remove']
form.process(request.form)
if request.method == "POST" and form.validate():
changes = {'fatty': {}, 'starving': {}}
changes['fatty']['add'] = form.fatty_to_add.data
changes['fatty']['remove'] = form.fatty_to_remove.data
changes['starving']['add'] = form.starving_to_add.data
changes['starving']['remove'] = form.starving_to_remove.data
directory.update_member_groups(g.ldap, changes)
return render_template("admin_ldap_sync.html", form=form)
@app.route("/admin/csv")
@admin_required
@login_required
def admin_csv():
members = []
for m in models.Member.get_members(True):
member = m.get_status()
if member['type'] == 'supporting':
continue
member['contact_email'] = m.get_contact_email()
member['cn'] = directory.get_member_fields(g.ldap, member['username'], 'cn')['cn']
members.append(member)
active_members = filter(lambda m: m['judgement'], members)
output = render_template("admin_csv.html", active_members=active_members)
return Response(output)
@app.route('/admin/member/<membername>')
@login_required
@admin_required
def admin_member(membername):
member = models.Member.get_members(True).filter_by(username=membername).first()
if not member:
abort(404)
status = member.get_status()
cn = directory.get_member_fields(g.ldap, member.username, 'cn')['cn']
return render_template("admin_member.html", member=member, status=status,
cn=cn, admin=True)
@app.route("/admin/member/<membername>/policy:<policy>")
@login_required
@admin_required
def admin_member_set_policy(membername,policy):
member = models.Member.query.filter_by(username=membername).first()
member.payment_policy = models.PaymentPolicy[policy].value
db.session.add(member)
db.session.commit()
return redirect(request.referrer)
@app.route("/admin/member/<membername>/membership:<membershiptype>")
@login_required
@admin_required
def admin_member_set_membership(membername,membershiptype):
member = models.Member.query.filter_by(username=membername).first()
member.type = models.MembershipType[membershiptype].name
db.session.add(member)
db.session.commit()
return redirect(request.referrer)
@app.route("/admin/member/add/<membershiptype>/<username>")
@login_required
@admin_required
def add_member(membershiptype, username):
member = models.Member(None, username, models.MembershipType[membershiptype].name, True)
db.session.add(member)
db.session.commit()
return "ok"
@app.route("/admin/match")
@login_required
@admin_required
def admin_match():
transfers_unmatched = logic.get_unmatched_transfers()
return render_template("match.html", transfers_unmatched=transfers_unmatched)
@app.route("/admin/match/auto", methods=["GET"])
@login_required
@admin_required
def admin_match_auto():
matched = 0
left = 0
transfers_unmatched = logic.get_unmatched_transfers()
affected_members = []
for transfer in transfers_unmatched:
matchability, member, months = transfer.get_matchability()
try:
print "[i] Matching transfer {} for {:.2f}PLN by member {}, {} months".format(transfer.id, transfer.amount/100, member.username, months)
except AttributeError:
print "[e] Member data invalid, WTF - {}".format(repr(member))
continue
if matchability == models.Transfer.MATCH_OK:
if len(member.transfers) > 0:
year, month = member.get_next_unpaid()
if None in (year, month):
print "[w] next_unpaid borked, skipping"
continue
else:
year, month = transfer.date.year, transfer.date.month
for m in range(months):
mt = models.MemberTransfer(None, year, month, transfer)
member.transfers.append(mt)
db.session.add(mt)
flash("Matched transfer {} for {:.2f}PLN to member {} for month {}-{}".format(transfer.id, transfer.amount/100, member.username, year, month))
year, month = member._yearmonth_increment((year,month))
matched += 1
affected_members.append(member)
else:
left += 1
db.session.commit()
for member in affected_members:
member.get_status(force_refresh=True)
flash("Matched %i, %i left" % (matched, left))
return redirect(url_for("admin_match"))
@app.route("/admin/match/manual", methods=["GET"])
@login_required
@admin_required
def match_manual():
transfers_unmatched = logic.get_unmatched_transfers()
return render_template("match_manual.html", transfers_unmatched=transfers_unmatched)
@app.route("/admin/match/<username>/<int:months>/<path:uid>")
@login_required
@admin_required
def match(username, uid, months):
member = models.Member.query.filter_by(username=username).first()
if not member:
return "no member"
transfer = models.Transfer.query.filter_by(uid=uid).first()
if not transfer:
return "no transfer"
for _ in range(months):
year, month = member.get_next_unpaid()
mt = models.MemberTransfer(None, year, month, transfer)
member.transfers.append(mt)
db.session.add(mt)
db.session.commit()
member.get_status(force_refresh=True)
return "ok, %i PLN get!" % transfer.amount
@app.route("/admin/match/", methods=["POST"])
@login_required
@admin_required
def match_user_transfer():
username = request.form["username"]
uid = request.form["uid"]
member = models.Member.query.filter_by(username=username).first()
if not member:
return "no such member! :("
transfer = models.Transfer.query.filter_by(uid=uid).first()
if not transfer:
return "no transfer"
return render_template("match_user_transfer.html", member=member, transfer=transfer)
@app.route("/admin/spam/", methods=["GET", "POST"])
@login_required
@admin_required
def sendspam():
now = datetime.datetime.now()
members = models.Member.query.filter_by(
active=True, payment_policy=models.PaymentPolicy.normal.value).all()
form = forms.SpamForm()
form.members.choices = [(member.id, member) for member in members]
form.members.default = [member.id for member in members]
form.process(request.form)
if request.method == 'POST' and form.validate():
spam = []
for member in members:
if member.id not in form.members.data:
continue
content = render_template(
'mailing/due.txt',
member=member,
status=member.get_status(),
transfers=member.transfers[:5],
now=now)
# Just ignore empty messages
if not content.strip():
continue
msg = MIMEText(content, "plain", "utf-8")
msg["From"] = "Faszysta Hackerspace'owy <fascist@hackerspace.pl>"
msg["Subject"] = "Stan składek na dzień %s" % now.strftime("%d/%m/%Y")
msg["To"] = member.get_contact_email()
spam.append(msg)
if form.dry_run.data:
readable = [
msg.as_string().split('\n\n')[0] + '\n\n' + msg.get_payload(decode=True) for msg in spam]
return Response('\n====\n'.join(readable), mimetype='text/text')
for msg in spam:
p = Popen(["/usr/sbin/sendmail", "-t"], stdin=PIPE)
p.communicate(msg.as_string())
flash('%d messages sent!' % len(spam))
return redirect(url_for('admin_index'))
return render_template('admin_spam.html', form=form)
@app.route("/login", methods=["POST", "GET"])
def login():
form = forms.LoginForm(request.form)
if request.method == "POST" and form.validate():
if requests.post("https://auth.hackerspace.pl/",
dict(login=form.username.data, password=form.password.data)).status_code == 200:
if requests.post("https://auth.hackerspace.pl/", {
'login': form.username.data,
'password': form.password.data}).status_code == 200:
user = User(form.username.data)
login_user(user)
flash('Logged in succesfully')
if user.is_admin():
return redirect(request.args.get("next") or url_for("admin_index"))
else:
return redirect(request.args.get("next") or url_for("self_profile"))
return redirect(request.args.get("next") or url_for("admin.index"))
return redirect(request.args.get("next") or url_for("self_profile"))
return render_template("login.html", form=form)