refactor: random cleanups, migrate to Flask-Cache
parent
4e0680cbba
commit
c83712b244
|
@ -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
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue