feature/pricelist add server pagination for balance #17
|
@ -118,8 +118,12 @@ def modify_user(user, password, new_password=None):
|
||||||
messageController.send_message(messageController.Message(user, text, subject))
|
messageController.send_message(messageController.Message(user, text, subject))
|
||||||
|
|
||||||
|
|
||||||
def get_users():
|
def get_users(userids=None):
|
||||||
|
|||||||
return User.query.all()
|
query = User.query
|
||||||
|
if userids:
|
||||||
|
query.filter(User.userid in userids)
|
||||||
|
query = query.order_by(User.lastname.asc(), User.firstname.asc())
|
||||||
|
return query.all()
|
||||||
|
|
||||||
|
|
||||||
def get_user_by_role(role: Role):
|
def get_user_by_role(role: Role):
|
||||||
|
@ -175,8 +179,8 @@ def register(data):
|
||||||
allowed_keys = User().serialize().keys()
|
allowed_keys = User().serialize().keys()
|
||||||
values = {key: value for key, value in data.items() if key in allowed_keys}
|
values = {key: value for key, value in data.items() if key in allowed_keys}
|
||||||
roles = values.pop("roles", [])
|
roles = values.pop("roles", [])
|
||||||
if "birthday" in values:
|
if "birthday" in data:
|
||||||
values["birthday"] = from_iso_format(values["birthday"]).date()
|
values["birthday"] = from_iso_format(data["birthday"]).date()
|
||||||
user = User(**values)
|
user = User(**values)
|
||||||
set_roles(user, roles)
|
set_roles(user, roles)
|
||||||
|
|
||||||
|
@ -195,6 +199,8 @@ def register(data):
|
||||||
)
|
)
|
||||||
messageController.send_message(messageController.Message(user, text, subject))
|
messageController.send_message(messageController.Message(user, text, subject))
|
||||||
|
|
||||||
|
find_user(user.userid)
|
||||||
ferfissimo
commented
Sieht für mich ziemlich unnötig aus, gibt es einen Grund das hier aufzurufen? Sieht für mich ziemlich unnötig aus, gibt es einen Grund das hier aufzurufen?
crimsen
commented
Damit werden gleich Userattributes geupdatet. Ohne diese Funktion, gibt es keine "DN" und es erschien mir einfacher, das einfach damit aufzurufen. (Benutze ich auch in run_flaschengeist ldap_sync) Damit werden gleich _Userattributes geupdatet_. Ohne diese Funktion, gibt es keine "DN" und es erschien mir einfacher, das einfach damit aufzurufen. (Benutze ich auch in run_flaschengeist ldap_sync)
crimsen
commented
Allgemein, sollte das eigentlich in einem anderen commit rein. Allgemein, sollte das eigentlich in einem anderen commit rein.
|
|||||||
|
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -104,6 +104,11 @@ class User(db.Model, ModelSerializeMixin):
|
||||||
def has_permission(self, permission):
|
def has_permission(self, permission):
|
||||||
return permission in self.get_permissions()
|
return permission in self.get_permissions()
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
ferfissimo
commented
Sieht nach debugging aus? Ist das für logs gedacht? Dann vielleicht lieber Sieht nach debugging aus? Ist das für logs gedacht? Dann vielleicht lieber `__str__` als `__repr__` (repr sollte eindeutig sein, das string ist aber nicht per-se eindeutig).
crimsen
commented
Ja dies war für debugging. Evtl. könnten wir sowas ja auch Einführen dass wir Ja dies war für debugging. Evtl. könnten wir sowas ja auch Einführen dass wir `__str__` und `__repr__` für models einführen. (Liest sich schöner im Log)
|
|||||||
|
return (
|
||||||
|
f"User({self.userid}, {self.firstname}, {self.lastname}, {self.mail}, {self.display_name}, {self.birthday})"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class _UserAttribute(db.Model, ModelSerializeMixin):
|
class _UserAttribute(db.Model, ModelSerializeMixin):
|
||||||
__tablename__ = "user_attribute"
|
__tablename__ = "user_attribute"
|
||||||
|
|
|
@ -3,12 +3,13 @@
|
||||||
# English: Debit -> from account
|
# English: Debit -> from account
|
||||||
# Credit -> to account
|
# Credit -> to account
|
||||||
|
|
||||||
from sqlalchemy import func
|
from sqlalchemy import func, case, and_
|
||||||
|
from sqlalchemy.ext.hybrid import hybrid_property
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from werkzeug.exceptions import BadRequest, NotFound, Conflict
|
from werkzeug.exceptions import BadRequest, NotFound, Conflict
|
||||||
|
|
||||||
from flaschengeist.database import db
|
from flaschengeist.database import db
|
||||||
from flaschengeist.models.user import User
|
from flaschengeist.models.user import User, _UserAttribute
|
||||||
|
|
||||||
from .models import Transaction
|
from .models import Transaction
|
||||||
from . import permissions, BalancePlugin
|
from . import permissions, BalancePlugin
|
||||||
|
@ -38,27 +39,114 @@ def get_balance(user, start: datetime = None, end: datetime = None):
|
||||||
return credit, debit, credit - debit
|
return credit, debit, credit - debit
|
||||||
|
|
||||||
|
|
||||||
def get_balances(start: datetime = None, end: datetime = None):
|
def get_balances(start: datetime = None, end: datetime = None, limit=None, offset=None, descending=None, sortBy=None):
|
||||||
ferfissimo
commented
Der Ansatz ist cool! NICE 👍 Der Ansatz ist cool! NICE 👍
crimsen
commented
Noch irgendwas hier zu machen? Noch irgendwas hier zu machen?
|
|||||||
debit = db.session.query(Transaction.sender_id, func.sum(Transaction.amount)).filter(Transaction.sender_ != None)
|
class _User(User):
|
||||||
credit = db.session.query(Transaction.receiver_id, func.sum(Transaction.amount)).filter(
|
_debit = db.relationship(Transaction, back_populates="sender_", foreign_keys=[Transaction._sender_id])
|
||||||
Transaction.receiver_ != None
|
_credit = db.relationship(Transaction, back_populates="receiver_", foreign_keys=[Transaction._receiver_id])
|
||||||
)
|
|
||||||
if start:
|
|
||||||
debit = debit.filter(start <= Transaction.time)
|
|
||||||
credit = credit.filter(start <= Transaction.time)
|
|
||||||
if end:
|
|
||||||
debit = debit.filter(Transaction.time <= end)
|
|
||||||
credit = credit.filter(Transaction.time <= end)
|
|
||||||
|
|
||||||
debit = debit.group_by(Transaction._sender_id).all()
|
@hybrid_property
|
||||||
credit = credit.group_by(Transaction._receiver_id).all()
|
def debit(self):
|
||||||
|
return sum([cred.amount for cred in self._debit])
|
||||||
|
|
||||||
|
@debit.expression
|
||||||
|
def debit(cls):
|
||||||
|
a = (
|
||||||
|
db.select(func.sum(Transaction.amount))
|
||||||
|
.where(cls.id_ == Transaction._sender_id, Transaction.amount)
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
return case([(a, a)], else_=0)
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def credit(self):
|
||||||
|
return sum([cred.amount for cred in self._credit])
|
||||||
|
|
||||||
|
@credit.expression
|
||||||
|
def credit(cls):
|
||||||
|
b = (
|
||||||
|
db.select(func.sum(Transaction.amount))
|
||||||
|
.where(cls.id_ == Transaction._receiver_id, Transaction.amount)
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
return case([(b, b)], else_=0)
|
||||||
|
|
||||||
|
@hybrid_property
|
||||||
|
def limit(self):
|
||||||
|
return self.get_attribute("balance_limit", None)
|
||||||
|
|
||||||
|
@limit.expression
|
||||||
|
def limit(cls):
|
||||||
|
return (
|
||||||
|
db.select(_UserAttribute.value)
|
||||||
|
.where(and_(cls.id_ == _UserAttribute.user, _UserAttribute.name == "balance_limit"))
|
||||||
|
.scalar_subquery()
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_debit(self, start: datetime = None, end: datetime = None):
|
||||||
|
if start and end:
|
||||||
|
return sum([deb.amount for deb in self._debit if start <= deb.time and deb.time <= end])
|
||||||
|
if start:
|
||||||
|
return sum([deb.amount for deb in self._dedit if start <= deb.time])
|
||||||
|
if end:
|
||||||
|
return sum([deb.amount for deb in self._dedit if deb.time <= end])
|
||||||
|
return self.debit
|
||||||
|
|
||||||
|
def get_credit(self, start: datetime = None, end: datetime = None):
|
||||||
|
if start and end:
|
||||||
|
return sum([cred.amount for cred in self._credit if start <= cred.time and cred.time <= end])
|
||||||
|
if start:
|
||||||
|
return sum([cred.amount for cred in self._credit if start <= cred.time])
|
||||||
|
if end:
|
||||||
|
return sum([cred.amount for cred in self._credit if cred.time <= end])
|
||||||
|
return self.credit
|
||||||
|
|
||||||
|
query = _User.query
|
||||||
|
|
||||||
|
if start:
|
||||||
|
q1 = query.join(_User._credit).filter(start <= Transaction.time)
|
||||||
|
q2 = query.join(_User._debit).filter(start <= Transaction.time)
|
||||||
|
query = q1.union(q2)
|
||||||
|
if end:
|
||||||
|
q1 = query.join(_User._credit).filter(Transaction.time <= end)
|
||||||
|
q2 = query.join(_User._debit).filter(Transaction.time <= end)
|
||||||
|
query = q1.union(q2)
|
||||||
|
|
||||||
|
if sortBy == "balance":
|
||||||
|
if descending:
|
||||||
|
query = query.order_by((_User.credit - _User.debit).desc(), _User.lastname.asc(), _User.firstname.asc())
|
||||||
|
else:
|
||||||
|
query = query.order_by((_User.credit - _User.debit).asc(), _User.lastname.asc(), _User.firstname.asc())
|
||||||
|
elif sortBy == "limit":
|
||||||
|
if descending:
|
||||||
|
query = query.order_by(_User.limit.desc(), User.lastname.asc(), User.firstname.asc())
|
||||||
|
else:
|
||||||
|
query = query.order_by(_User.limit.asc(), User.lastname.asc(), User.firstname.asc())
|
||||||
|
elif sortBy == "firstname":
|
||||||
|
if descending:
|
||||||
|
query = query.order_by(User.firstname.desc(), User.lastname.desc())
|
||||||
|
else:
|
||||||
|
query = query.order_by(User.firstname.asc(), User.lastname.asc())
|
||||||
|
elif sortBy == "lastname":
|
||||||
|
if descending:
|
||||||
|
query = query.order_by(User.lastname.desc(), User.firstname.desc())
|
||||||
|
else:
|
||||||
|
query = query.order_by(User.lastname.asc(), User.firstname.asc())
|
||||||
|
|
||||||
|
count = None
|
||||||
|
if limit:
|
||||||
|
count = query.count()
|
||||||
|
query = query.limit(limit)
|
||||||
|
if offset:
|
||||||
|
query = query.offset(offset)
|
||||||
|
users = query
|
||||||
all = {}
|
all = {}
|
||||||
for uid, cred in credit:
|
|
||||||
all[uid] = [cred, 0]
|
for user in users:
|
||||||
for uid, deb in debit:
|
|
||||||
all.setdefault(uid, [0, 0])
|
all[user.userid] = [user.get_credit(start, end), 0]
|
||||||
all[uid][1] = deb
|
all[user.userid][1] = user.get_debit(start, end)
|
||||||
return all
|
|
||||||
|
return all, count
|
||||||
|
|
||||||
|
|
||||||
def send(sender: User, receiver, amount: float, author: User):
|
def send(sender: User, receiver, amount: float, author: User):
|
||||||
|
|
|
@ -110,8 +110,10 @@ def limits(current_session: Session):
|
||||||
Returns:
|
Returns:
|
||||||
JSON encoded array of userid with limit or HTTP-error
|
JSON encoded array of userid with limit or HTTP-error
|
||||||
"""
|
"""
|
||||||
|
userids = None
|
||||||
users = userController.get_users()
|
if "userids" in request.args:
|
||||||
ferfissimo
commented
Siehe unten: https://flaschengeist.dev/Flaschengeist/flaschengeist/pulls/17/files#issuecomment-164
|
|||||||
|
[x for x in request.args.get("userids").split(",") if x]
|
||||||
|
users = userController.get_users(userids=userids)
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
return jsonify([{"userid": user.userid, "limit": user.get_attribute("balance_limit")} for user in users])
|
return jsonify([{"userid": user.userid, "limit": user.get_attribute("balance_limit")} for user in users])
|
||||||
|
|
||||||
|
@ -311,5 +313,11 @@ def get_balances(current_session: Session):
|
||||||
Returns:
|
Returns:
|
||||||
JSON Array containing credit, debit and userid for each user or HTTP error
|
JSON Array containing credit, debit and userid for each user or HTTP error
|
||||||
"""
|
"""
|
||||||
balances = balance_controller.get_balances()
|
limit = request.args.get("limit", type=int)
|
||||||
return jsonify([{"userid": u, "credit": v[0], "debit": v[1]} for u, v in balances.items()])
|
offset = request.args.get("offset", type=int)
|
||||||
|
descending = request.args.get("descending", False, type=bool)
|
||||||
|
sortBy = request.args.get("sortBy", type=str)
|
||||||
|
balances, count = balance_controller.get_balances(limit=limit, offset=offset, descending=descending, sortBy=sortBy)
|
||||||
|
return jsonify(
|
||||||
|
{"balances": [{"userid": u, "credit": v[0], "debit": v[1]} for u, v in balances.items()], "count": count}
|
||||||
|
)
|
||||||
|
|
|
@ -69,7 +69,10 @@ def list_users(current_session):
|
||||||
JSON encoded array of `flaschengeist.models.user.User` or HTTP error
|
JSON encoded array of `flaschengeist.models.user.User` or HTTP error
|
||||||
"""
|
"""
|
||||||
logger.debug("Retrieve list of all users")
|
logger.debug("Retrieve list of all users")
|
||||||
users = userController.get_users()
|
userids = None
|
||||||
ferfissimo
commented
Wird die Änderung eigentlich irgendwo verwendet? Ich kann mir denken wofür die gedacht ist, aber ich bin mir nicht so sicher ob das sinnvoll ist. Wird die Änderung eigentlich irgendwo verwendet?
Die widerspricht dem bisherigen REST Ansatz, daher entweder einzelner Datensatz (`/user/xy`) oder einem Block (`/users` bez. `/users?limit...&offset...`).
Ich kann mir denken wofür die gedacht ist, aber ich bin mir nicht so sicher ob das sinnvoll ist.
crimsen
commented
Wird im Plugin verwendet. Damit braucht man nicht mehr alle Users laden (was im übrigen irgendwann ja sehr viele sein werden) sondern nur ein Bruchteil direkt über die userids. Wird im Plugin verwendet. Damit braucht man nicht mehr alle Users laden (was im übrigen irgendwann ja sehr viele sein werden) sondern nur ein Bruchteil direkt über die userids.
crimsen
commented
guckst du hier guckst du [hier](https://flaschengeist.dev/Flaschengeist/flaschengeist-frontend/src/commit/ade6d06eb6df430ed842a10d2bc867c8371c866d/api/src/stores/user.ts#L40)
|
|||||||
|
if "userids" in request.args:
|
||||||
|
userids = [x for x in request.args.get("userids").split(",") if x]
|
||||||
|
users = userController.get_users(userids=userids)
|
||||||
return jsonify(users)
|
return jsonify(users)
|
||||||
|
|
||||||
|
|
||||||
|
|
Siehe unten: https://flaschengeist.dev/Flaschengeist/flaschengeist/pulls/17/files#issuecomment-164
Bez. die funktion im controller ist schon ok, aber damit wird alles immer sortiert, nicht so sicher ob das sortieren hier immer sinnvoll ist.
Also a] ob es sinnvoll ist das immer zu tun und b] ob die sortierung hardcoded Sinn ergibt (z.b. meist sieht man ja die User mit dem display_name und da kann das z.b. manchmal zu komischen ergebnisse führen)
Ich dachte, ich arbeite schon mal vor, falls wir hier auch server side pagination brauchen. demzufolge, können wir das hier auch wieder raus nehmen.