[Plugin] Added balance plugin
Extends the users plugin. * Users can have an account with a balance * Users can send from them to others * Admins can set the balance * Admins can set limits
This commit is contained in:
parent
e0d3b211bb
commit
32783041d8
|
@ -4,7 +4,7 @@ auth = "auth_plain"
|
||||||
# Enable if you run flaschengeist behind a proxy, e.g. nginx + gunicorn
|
# Enable if you run flaschengeist behind a proxy, e.g. nginx + gunicorn
|
||||||
#proxy = false
|
#proxy = false
|
||||||
# Set root path, prefixes all routes
|
# Set root path, prefixes all routes
|
||||||
#root = /
|
#root = /api
|
||||||
# Set secret key
|
# Set secret key
|
||||||
secret_key = "V3ryS3cr3t"
|
secret_key = "V3ryS3cr3t"
|
||||||
|
|
||||||
|
@ -45,8 +45,8 @@ enabled = true
|
||||||
# SSL or STARTLS
|
# SSL or STARTLS
|
||||||
# CRYPT = SSL
|
# CRYPT = SSL
|
||||||
|
|
||||||
|
[balance]
|
||||||
|
enabled = true
|
||||||
|
|
||||||
[geruecht]
|
[geruecht]
|
||||||
enabled = false
|
enabled = false
|
||||||
|
|
||||||
[schubu]
|
|
||||||
enabled = false
|
|
||||||
|
|
|
@ -0,0 +1,166 @@
|
||||||
|
"""Balance plugin
|
||||||
|
|
||||||
|
Extends users plugin with balance functions
|
||||||
|
"""
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from flask import Blueprint, jsonify, request
|
||||||
|
from werkzeug.exceptions import Forbidden, BadRequest
|
||||||
|
from backports.datetime_fromisoformat import MonkeyPatch
|
||||||
|
|
||||||
|
from flaschengeist.plugins import Plugin
|
||||||
|
from flaschengeist.models.session import Session
|
||||||
|
from flaschengeist.decorator import login_required
|
||||||
|
from flaschengeist.controller import userController
|
||||||
|
|
||||||
|
from . import balance_controller, permissions
|
||||||
|
|
||||||
|
MonkeyPatch.patch_fromisoformat()
|
||||||
|
balance_bp = Blueprint("balance", __name__)
|
||||||
|
|
||||||
|
|
||||||
|
class BalancePlugin(Plugin):
|
||||||
|
def __init__(self, config):
|
||||||
|
super().__init__(blueprint=balance_bp, permissions=permissions.permissions)
|
||||||
|
|
||||||
|
def install(self):
|
||||||
|
from flaschengeist.database import db
|
||||||
|
from .models import Transaction
|
||||||
|
|
||||||
|
db.create_all()
|
||||||
|
|
||||||
|
|
||||||
|
@balance_bp.route("/users/<userid>/balance/limit", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def get_limit(userid, current_session: Session):
|
||||||
|
"""Get set limit of an user
|
||||||
|
|
||||||
|
Route: ``/users/<userid>/balance/limit`` | Method: ``GET``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
userid: Userid identifying the user
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON object containing the limit (or Null if no limit set) or HTTP error
|
||||||
|
"""
|
||||||
|
user = userController.get_user(userid)
|
||||||
|
if (user != current_session._user and not current_session._user.has_permission(permissions.SET_LIMIT)) or (
|
||||||
|
user == current_session._user and not user.has_permission(permissions.SHOW)
|
||||||
|
):
|
||||||
|
raise Forbidden
|
||||||
|
|
||||||
|
return jsonify({"limit": balance_controller.get_limit(user)})
|
||||||
|
|
||||||
|
|
||||||
|
@balance_bp.route("/users/<userid>/balance/limit", methods=["PUT"])
|
||||||
|
@login_required()
|
||||||
|
def set_limit(userid, current_session: Session):
|
||||||
|
"""Set the limit of an user
|
||||||
|
|
||||||
|
Route: ``/users/<userid>/balance/limit`` | Method: ``PUT``
|
||||||
|
|
||||||
|
POST-data: ``{limit: float}``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
userid: Userid identifying the user
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HTTP-200 or HTTP error
|
||||||
|
"""
|
||||||
|
user = userController.get_user(userid)
|
||||||
|
if not current_session._user.has_permission(permissions.SET_LIMIT):
|
||||||
|
raise Forbidden
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
try:
|
||||||
|
limit = data["limit"]
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
raise BadRequest
|
||||||
|
balance_controller.set_limit(user, limit)
|
||||||
|
|
||||||
|
|
||||||
|
@balance_bp.route("/users/<userid>/balance", methods=["GET"])
|
||||||
|
@login_required()
|
||||||
|
def get_balance(userid, current_session: Session):
|
||||||
|
"""Get balance of user, optionally filtered
|
||||||
|
|
||||||
|
Route: ``/users/<userid>/balance`` | Method: ``GET``
|
||||||
|
|
||||||
|
GET-parameters: ```{from?: string, to?: string}```
|
||||||
|
|
||||||
|
Args:
|
||||||
|
userid: Userid of user to get balance from
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON object containing credit, debit and balance or HTTP error
|
||||||
|
"""
|
||||||
|
if (userid == current_session._user.userid and not current_session._user.has_permission(permissions.SHOW)) or (
|
||||||
|
userid != current_session._user.userid and not current_session._user.has_permission(permissions.SHOW_OTHER)
|
||||||
|
):
|
||||||
|
raise Forbidden
|
||||||
|
|
||||||
|
# Might raise NotFound
|
||||||
|
user = userController.get_user(userid)
|
||||||
|
|
||||||
|
start = request.args.get("from")
|
||||||
|
if start:
|
||||||
|
start = datetime.fromisoformat(start).replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
start = datetime.fromtimestamp(0, tz=timezone.utc)
|
||||||
|
|
||||||
|
end = request.args.get("to")
|
||||||
|
if end:
|
||||||
|
end = datetime.fromisoformat(end).replace(tzinfo=timezone.utc)
|
||||||
|
else:
|
||||||
|
end = datetime.now(tz=timezone.utc)
|
||||||
|
|
||||||
|
balance = balance_controller.get(user, start, end)
|
||||||
|
return jsonify({"credit": balance[0], "debit": balance[1], "balance": balance[2]})
|
||||||
|
|
||||||
|
|
||||||
|
@balance_bp.route("/users/<userid>/balance", methods=["PUT"])
|
||||||
|
@login_required()
|
||||||
|
def change_balance(userid, current_session: Session):
|
||||||
|
"""Change balance of an user
|
||||||
|
If ``sender`` is preset in POST-data, the action is handled as a transfer from ``sender`` to user.
|
||||||
|
|
||||||
|
Route: ``/users/<userid>/balance`` | Method: ``PUT``
|
||||||
|
|
||||||
|
POST-data: ``{amount: float, sender: string}``
|
||||||
|
|
||||||
|
Args:
|
||||||
|
userid: userid identifying user to change balance
|
||||||
|
current_session: Session sent with Authorization Header
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
JSON object containing credit, debit and balance or HTTP error
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = request.get_json()
|
||||||
|
try:
|
||||||
|
amount = data["amount"]
|
||||||
|
except (TypeError, KeyError):
|
||||||
|
raise BadRequest
|
||||||
|
|
||||||
|
sender = data.get("sender", None)
|
||||||
|
user = userController.get_user(userid)
|
||||||
|
|
||||||
|
if sender:
|
||||||
|
sender = userController.get_user(sender)
|
||||||
|
if sender == user:
|
||||||
|
raise BadRequest
|
||||||
|
|
||||||
|
if (sender == current_session._user and sender.has_permission(permissions.SEND)) or (
|
||||||
|
sender != current_session._user and current_session._user.has_permission(permissions.SEND_OTHER)
|
||||||
|
):
|
||||||
|
return balance_controller.send(sender, user, data["amount"], current_session._user)
|
||||||
|
|
||||||
|
elif (amount < 0 and current_session._user.has_permission(permissions.SUB)) or (
|
||||||
|
amount > 0 and current_session._user.has_permission(permissions.ADD)
|
||||||
|
):
|
||||||
|
return balance_controller.change_balance(user, data["amount"], current_session._user)
|
||||||
|
|
||||||
|
raise Forbidden
|
|
@ -0,0 +1,82 @@
|
||||||
|
from flaschengeist.models.user import User
|
||||||
|
from sqlalchemy import func
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from werkzeug.exceptions import BadRequest
|
||||||
|
|
||||||
|
from flaschengeist import logger
|
||||||
|
from flaschengeist.database import db
|
||||||
|
|
||||||
|
from .models import Transaction
|
||||||
|
from . import permissions
|
||||||
|
|
||||||
|
|
||||||
|
__attribute_limit = "balance_limit"
|
||||||
|
|
||||||
|
|
||||||
|
def set_limit(user: User, limit: float):
|
||||||
|
user.set_attribute(__attribute_limit, limit)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def get_limit(user: User) -> float:
|
||||||
|
return user.get_attribute(__attribute_limit, default=None)
|
||||||
|
|
||||||
|
|
||||||
|
def get(user, start: datetime, end: datetime):
|
||||||
|
credit = (
|
||||||
|
db.session.query(func.sum(Transaction.amount))
|
||||||
|
.filter(Transaction.receiver == user)
|
||||||
|
.filter(start <= Transaction.time)
|
||||||
|
.filter(Transaction.time <= end)
|
||||||
|
.scalar()
|
||||||
|
) or 0
|
||||||
|
logger.debug(credit)
|
||||||
|
if credit is None:
|
||||||
|
credit = 0
|
||||||
|
debit = (
|
||||||
|
db.session.query(func.sum(Transaction.amount))
|
||||||
|
.filter(Transaction.sender == user and start <= Transaction.time <= end)
|
||||||
|
.all()[0][0]
|
||||||
|
)
|
||||||
|
if debit is None:
|
||||||
|
debit = 0
|
||||||
|
return credit, debit, credit - debit
|
||||||
|
|
||||||
|
|
||||||
|
def send(sender: User, receiver, amount: float, author: User):
|
||||||
|
"""Send credit from one user to an other
|
||||||
|
|
||||||
|
Args:
|
||||||
|
sender: User who sends the amount
|
||||||
|
receiver: User who receives the amount
|
||||||
|
amount: Amount to send
|
||||||
|
author: User authoring this transaction
|
||||||
|
Raises:
|
||||||
|
BadRequest if amount <= 0
|
||||||
|
"""
|
||||||
|
if amount <= 0:
|
||||||
|
raise BadRequest
|
||||||
|
|
||||||
|
if sender.has_attribute(__attribute_limit):
|
||||||
|
if (get(sender)[2] - amount) < sender.get_attribute(__attribute_limit) and not author.has_permission(
|
||||||
|
permissions.EXCEED_LIMIT
|
||||||
|
):
|
||||||
|
raise BadRequest("Limit exceeded")
|
||||||
|
|
||||||
|
transaction = Transaction(sender=sender, receiver=receiver, amount=amount, author=author)
|
||||||
|
db.session.add(transaction)
|
||||||
|
db.session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def change_balance(user, amount: float, author):
|
||||||
|
"""Change balance of user
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: User to change balance
|
||||||
|
amount: Amount to change balance
|
||||||
|
author: User authoring this transaction
|
||||||
|
"""
|
||||||
|
sender = user if amount < 0 else None
|
||||||
|
receiver = user if amount > 0 else None
|
||||||
|
send(sender, receiver, abs(amount), author)
|
|
@ -0,0 +1,22 @@
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from flaschengeist.models.user import User
|
||||||
|
|
||||||
|
from flaschengeist.database import db
|
||||||
|
from flaschengeist.models import ModelSerializeMixin, UtcDateTime
|
||||||
|
|
||||||
|
|
||||||
|
class Transaction(db.Model, ModelSerializeMixin):
|
||||||
|
__tablename__ = "balance_transaction"
|
||||||
|
time: datetime = db.Column(UtcDateTime, nullable=False, default=datetime.now(tz=timezone.utc))
|
||||||
|
amount: float = db.Column(db.Numeric(precision=5, scale=2, asdecimal=False), nullable=False)
|
||||||
|
|
||||||
|
_receiver_id = db.Column("receiver_id", db.Integer, db.ForeignKey("user.id"))
|
||||||
|
_sender_id = db.Column("sender_id", db.Integer, db.ForeignKey("user.id"))
|
||||||
|
_author_id = db.Column("author_id", db.Integer, db.ForeignKey("user.id"), nullable=False)
|
||||||
|
|
||||||
|
sender: User = db.relationship("User", foreign_keys=[_sender_id])
|
||||||
|
receiver: User = db.relationship("User", foreign_keys=[_receiver_id])
|
||||||
|
author: User = db.relationship("User", foreign_keys=[_author_id])
|
||||||
|
|
||||||
|
_id = db.Column("id", db.Integer, primary_key=True)
|
|
@ -0,0 +1,17 @@
|
||||||
|
# Show own and others balance
|
||||||
|
SHOW = "balance_show"
|
||||||
|
SHOW_OTHER = "balance_show_others"
|
||||||
|
# Credit balance
|
||||||
|
ADD = "balance_add"
|
||||||
|
# Debit balance
|
||||||
|
SUB = "balance_sub"
|
||||||
|
# Send from to other
|
||||||
|
SEND = "balance_send"
|
||||||
|
# Send from other to another
|
||||||
|
SEND_OTHER = "balance_send_others"
|
||||||
|
# Can set limit for users
|
||||||
|
SET_LIMIT = "balance_set_limit"
|
||||||
|
# Allow sending / sub while exceeding the set limit
|
||||||
|
EXCEED_LIMIT = "balance_exceed_limit"
|
||||||
|
|
||||||
|
permissions = [value for key, value in globals().items() if not key.startswith("_")]
|
Loading…
Reference in New Issue