refactor: random cleanups, migrate to Flask-Cache

refucktor
informatic 2018-03-12 23:40:34 +01:00
parent 4e0680cbba
commit c83712b244
7 changed files with 120 additions and 119 deletions

View File

@ -1,4 +1,5 @@
Flask==0.10.1
Flask-Cache==0.13.1
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,32 +29,34 @@ 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_cache 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
import webapp.models # noqa
class AnonymousUser(AnonymousUserMixin):
def is_admin(self):
return False
login_manager.anonymous_user = AnonymousUser
class User(object):
def __init__(self, username):
self.username = username.lower().strip()
@ -76,9 +78,10 @@ class User(object):
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
req = requests.get(
'https://capacifier.hackerspace.pl/kasownik_access/%s'
% self.username)
self._admin = req.status_code == 200
return self._admin
@ -86,13 +89,16 @@ class User(object):
def load_user(username):
return User(username)
@login_manager.unauthorized_handler
def unauthorized():
return redirect('/login')
def admin_required(f):
@wraps(f)
def admin_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if not current_user.is_admin():
return login_manager.unauthorized()
return f(*args, **kwargs)
return func(*args, **kwargs)
return wrapper
@ -101,23 +107,29 @@ import webapp.admin
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)
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():
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)
# 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)

View File

@ -5,7 +5,7 @@ from email.mime.text import MIMEText
from flask import render_template, request, flash, g, Response, \
redirect, url_for, abort, Blueprint
from flask.ext.login import login_required
from flask_login import login_required
from webapp import forms, db, models, admin_required
import directory

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,15 @@
import hmac
import json
import datetime
import logging
from functools import wraps
from sqlalchemy import and_
from flask import request, abort, Response
from webapp import models, app, mc
from webapp import models, app, cache
logger = logging.getLogger(__name__)
class APIError(Exception):
def __init__(self, message, code=500):
@ -50,27 +53,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
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 app.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,9 +109,8 @@ 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))
@ -156,12 +154,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 +165,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 +173,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 +188,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 +201,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}

View File

@ -38,13 +38,16 @@ 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):
@ -121,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

@ -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,8 @@ 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))
cache.delete_memoized('_stats_for_month', year, month)
cache.delete_memoized('api_cashflow', year, month)
class PaymentStatus(enum.Enum):
@ -85,7 +85,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 +124,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 +222,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 +285,18 @@ 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))
cache.delete_memoized('api_months_due', self.username)
cache.set(cache_key, json.dumps(cache_data))
return cache_data
def _apply_judgement(self, status):
@ -304,7 +306,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 +387,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

@ -32,7 +32,7 @@ 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 webapp import app, forms, User, db, models, cache, 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 directory
@ -43,20 +43,17 @@ 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