[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
|
||||
#proxy = false
|
||||
# Set root path, prefixes all routes
|
||||
#root = /
|
||||
#root = /api
|
||||
# Set secret key
|
||||
secret_key = "V3ryS3cr3t"
|
||||
|
||||
|
@ -45,8 +45,8 @@ enabled = true
|
|||
# SSL or STARTLS
|
||||
# CRYPT = SSL
|
||||
|
||||
[balance]
|
||||
enabled = true
|
||||
|
||||
[geruecht]
|
||||
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