From 2517e385a6f22cc4ea7e29d0bc24424d1a3506a2 Mon Sep 17 00:00:00 2001 From: Aevann Date: Sat, 2 Mar 2024 19:02:05 +0200 Subject: [PATCH] add bank statement --- files/assets/css/main.css | 8 +++++ files/assets/images/{WPD => }/coins.webp | Bin files/assets/images/rDrama/coins.webp | Bin 51146 -> 0 bytes files/classes/__init__.py | 1 + files/classes/comment.py | 2 +- files/classes/currency_logs.py | 29 +++++++++++++++++ files/classes/post.py | 2 +- files/classes/user.py | 35 ++++++++++++++++++-- files/helpers/actions.py | 4 ++- files/helpers/alerts.py | 14 ++++++-- files/helpers/cron.py | 2 +- files/helpers/lottery.py | 4 +-- files/helpers/roulette.py | 6 ++-- files/helpers/slots.py | 13 ++++++++ files/helpers/treasure.py | 2 +- files/helpers/twentyone.py | 14 +++++++- files/routes/admin.py | 6 ++-- files/routes/asset_submissions.py | 2 +- files/routes/awards.py | 12 +++---- files/routes/groups.py | 2 +- files/routes/hats.py | 4 +-- files/routes/holes.py | 2 +- files/routes/polls.py | 4 +-- files/routes/settings.py | 2 +- files/routes/users.py | 23 ++++++++++--- files/templates/bank_statement.html | 38 ++++++++++++++++++++++ files/templates/header.html | 4 +-- files/templates/lottery.html | 4 +-- files/templates/userpage/banner.html | 4 +-- migrations/20240302-add-currency-logs.sql | 26 +++++++++++++++ 30 files changed, 226 insertions(+), 43 deletions(-) rename files/assets/images/{WPD => }/coins.webp (100%) delete mode 100644 files/assets/images/rDrama/coins.webp create mode 100644 files/classes/currency_logs.py create mode 100644 files/templates/bank_statement.html create mode 100644 migrations/20240302-add-currency-logs.sql diff --git a/files/assets/css/main.css b/files/assets/css/main.css index 7fabd8d49..ffa94348b 100644 --- a/files/assets/css/main.css +++ b/files/assets/css/main.css @@ -7758,3 +7758,11 @@ img[alpha]:not([alt*="#"]) { td[data-time] { white-space: pre; } + +.bg-green { + background-color: #0a6936 !important; +} + +.bg-red { + background-color: #71000b !important; +} diff --git a/files/assets/images/WPD/coins.webp b/files/assets/images/coins.webp similarity index 100% rename from files/assets/images/WPD/coins.webp rename to files/assets/images/coins.webp diff --git a/files/assets/images/rDrama/coins.webp b/files/assets/images/rDrama/coins.webp deleted file mode 100644 index b57f5ba8bd2c89c22423ab31d4fee79bd34b4d30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 51146 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 V$dcPrLbX)ygnqgeD!*{R{{bthis comment' @property @lazy diff --git a/files/classes/currency_logs.py b/files/classes/currency_logs.py new file mode 100644 index 000000000..50c538c29 --- /dev/null +++ b/files/classes/currency_logs.py @@ -0,0 +1,29 @@ +import time +from sqlalchemy import Column, ForeignKey +from sqlalchemy.sql.sqltypes import * + +from files.classes import Base +from files.helpers.lazy import lazy +from files.helpers.sorting_and_time import make_age_string + +class CurrencyLog(Base): + __tablename__ = "currency_logs" + id = Column(Integer, primary_key=True) + user_id = Column(Integer, ForeignKey("users.id")) + created_utc = Column(Integer) + currency = Column(String) + amount = Column(Integer) + reason = Column(String) + balance = Column(Integer) + + def __init__(self, *args, **kwargs): + if "created_utc" not in kwargs: kwargs["created_utc"] = int(time.time()) + super().__init__(*args, **kwargs) + + def __repr__(self): + return f"<{self.__class__.__name__}(id={self.id})>" + + @property + @lazy + def age_string(self): + return make_age_string(self.created_utc) diff --git a/files/classes/post.py b/files/classes/post.py index a67b37a52..5b02bc8d3 100644 --- a/files/classes/post.py +++ b/files/classes/post.py @@ -156,7 +156,7 @@ class Post(Base): @property @lazy def textlink(self): - return f"[{self.title}]({self.shortlink})" + return f'{self.title}' @property @lazy diff --git a/files/classes/user.py b/files/classes/user.py index 68276b19a..f2e2112ac 100644 --- a/files/classes/user.py +++ b/files/classes/user.py @@ -14,6 +14,7 @@ from files.classes import Base from files.classes.casino_game import CasinoGame from files.classes.group import * from files.classes.hole import Hole +from files.classes.currency_logs import CurrencyLog from files.helpers.config.const import * from files.helpers.config.modaction_types import * from files.helpers.config.awards import AWARDS_ENABLED, HOUSE_AWARDS @@ -213,7 +214,7 @@ class User(Base): def __repr__(self): return f"<{self.__class__.__name__}(id={self.id}, username={self.username})>" - def pay_account(self, currency, amount): + def pay_account(self, currency, amount, reason=None): if self.id in {AUTOJANNY_ID, LONGPOSTBOT_ID, ZOZBOT_ID}: return @@ -238,7 +239,20 @@ class User(Base): else: user_query.update({ User.marseybux: User.marseybux + amount }) - def charge_account(self, currency, amount, **kwargs): + if reason and amount: + currency_log = CurrencyLog( + user_id=self.id, + currency=currency, + amount=amount, + reason=reason, + ) + g.db.add(currency_log) + if currency == 'coins': + currency_log.balance = self.coins + else: + currency_log.balance = self.marseybux + + def charge_account(self, currency, amount, reason=None, **kwargs): if self.admin_level >= PERMS['INFINITE_CURRENCY']: return (True, amount) @@ -256,12 +270,14 @@ class User(Base): user_query.update({ User.coins: User.coins - amount }) succeeded = True charged_coins = amount + logs = ('coins', amount) elif currency == 'marseybux': account_balance = self.marseybux if not should_check_balance or account_balance >= amount: user_query.update({ User.marseybux: User.marseybux - amount }) succeeded = True + logs = ('marseybux', amount) elif currency == 'coins/marseybux': if self.marseybux >= amount: subtracted_mbux = amount @@ -278,9 +294,24 @@ class User(Base): }) succeeded = True charged_coins = subtracted_coins + logs = (('coins', subtracted_coins), ('marseybux', subtracted_mbux)) if succeeded: g.db.add(self) + if reason: + for currency, amount in logs: + if not amount: continue + currency_log = CurrencyLog( + user_id=self.id, + currency=currency, + amount=-amount, + reason=reason, + ) + g.db.add(currency_log) + if currency == 'coins': + currency_log.balance = self.coins + else: + currency_log.balance = self.marseybux return (succeeded, charged_coins) diff --git a/files/helpers/actions.py b/files/helpers/actions.py index 3ef8e4390..94e0b98e7 100644 --- a/files/helpers/actions.py +++ b/files/helpers/actions.py @@ -166,7 +166,7 @@ def execute_snappy(post, v): g.db.add(award_object) awarded_coins = int(AWARDS["glowie"]['price'] * COSMETIC_AWARD_COIN_AWARD_PCT) - post.author.pay_account('coins', awarded_coins) + post.author.pay_account('coins', awarded_coins, f"glowie award on {post.textlink}") msg = f"@Snappy has given {post.textlink} the Glowie Award and you have received {awarded_coins} coins as a result!" send_repeatable_notification(post.author.id, msg) @@ -475,6 +475,7 @@ def execute_antispam_post_check(title, v, url): return True def execute_antispam_duplicate_comment_check(v, body_html): + return if v.admin_level >= PERMS['BYPASS_ANTISPAM_CHECKS']: return if v.id in ANTISPAM_BYPASS_IDS: @@ -496,6 +497,7 @@ def execute_antispam_duplicate_comment_check(v, body_html): abort(403, "Too much spam!") def execute_antispam_comment_check(body, v): + return if v.admin_level >= PERMS['BYPASS_ANTISPAM_CHECKS']: return diff --git a/files/helpers/alerts.py b/files/helpers/alerts.py index c77b925eb..c32ec1440 100644 --- a/files/helpers/alerts.py +++ b/files/helpers/alerts.py @@ -177,6 +177,7 @@ def NOTIFY_USERS(text, v, oldtext=None, ghost=False, obj=None, followers_ping=Tr if FEATURES['PING_GROUPS']: cost = 0 + cost_groups = [] coin_receivers = set() for i in group_mention_regex.finditer(text): @@ -194,8 +195,11 @@ def NOTIFY_USERS(text, v, oldtext=None, ghost=False, obj=None, followers_ping=Tr cost = g.db.query(User).count() * 5 if cost > v.coins + v.marseybux: abort(403, f"You need {cost} currency to mention these ping groups!") - - v.charge_account('coins/marseybux', cost) + + reason = f"group pinging cost (!everyone)" + if obj: + reason += f" on {obj.textlink}" + v.charge_account('coins/marseybux', cost, reason) if obj: obj.ping_cost += cost return 'everyone' @@ -231,6 +235,7 @@ def NOTIFY_USERS(text, v, oldtext=None, ghost=False, obj=None, followers_ping=Tr if group and group.name == 'verifiedrich': abort(403, f"Only !verifiedrich members can mention it!") cost += len(members) * 5 + cost_groups.append(group.name) if cost > v.coins + v.marseybux: abort(403, f"You need {cost} currency to mention these ping groups!") @@ -239,7 +244,10 @@ def NOTIFY_USERS(text, v, oldtext=None, ghost=False, obj=None, followers_ping=Tr if charge: if cost: - v.charge_account('coins/marseybux', cost) + reason = f"group pinging cost (!" + ", !".join(cost_groups) + ")" + if obj: + reason += f" on {obj.textlink}" + v.charge_account('coins/marseybux', cost, reason) if obj: obj.ping_cost += cost diff --git a/files/helpers/cron.py b/files/helpers/cron.py index 3c15ed668..5d03c61d1 100644 --- a/files/helpers/cron.py +++ b/files/helpers/cron.py @@ -353,7 +353,7 @@ def _unpin_expired(): def _give_marseybux_salary(): for u in g.db.query(User).filter(User.admin_level > 0).all(): marseybux_salary = u.admin_level * 10000 - u.pay_account('marseybux', marseybux_salary) + u.pay_account('marseybux', marseybux_salary, "janny salary") send_repeatable_notification(u.id, f"You have received your monthly janny salary of {marseybux_salary} Marseybux!") def _expire_blocks_mutes_exiles(): diff --git a/files/helpers/lottery.py b/files/helpers/lottery.py index 162eac028..f8eb3fab3 100644 --- a/files/helpers/lottery.py +++ b/files/helpers/lottery.py @@ -47,7 +47,7 @@ def end_lottery_session(): winner = random.choice(raffle) active_lottery.winner_id = winner winning_user = next(filter(lambda x: x.id == winner, participating_users)) - winning_user.pay_account('coins', active_lottery.prize) + winning_user.pay_account('coins', active_lottery.prize, "lottery winnings") winning_user.total_lottery_winnings += active_lottery.prize badge_grant(user=winning_user, badge_id=LOTTERY_WINNER_BADGE_ID) @@ -109,7 +109,7 @@ def purchase_lottery_tickets(v, quantity=1): if (most_recent_lottery is None): return False, "There is no active lottery!" - if not v.charge_account('coins', LOTTERY_TICKET_COST * quantity)[0]: + if not v.charge_account('coins', LOTTERY_TICKET_COST * quantity, f'cost of {quantity} lottery tickets')[0]: return False, "You don't have enough coins" v.currently_held_lottery_tickets += quantity diff --git a/files/helpers/roulette.py b/files/helpers/roulette.py index e491bf75f..496f7d7fc 100644 --- a/files/helpers/roulette.py +++ b/files/helpers/roulette.py @@ -85,7 +85,7 @@ def get_active_roulette_games(): def charge_gambler(gambler, amount, currency): - charged = gambler.charge_account(currency, amount)[0] + charged = gambler.charge_account(currency, amount, "cost of roulette bet")[0] if not charged: raise Exception("Gambler cannot afford charge.") @@ -179,8 +179,8 @@ def spin_roulette_wheel(): coin_winnings = gambler_payout['coins'] procoin_winnings = gambler_payout['marseybux'] - gambler.pay_account('coins', coin_winnings) - gambler.pay_account('marseybux', procoin_winnings) + gambler.pay_account('coins', coin_winnings, "roulette winnings") + gambler.pay_account('marseybux', procoin_winnings, "roulette winnings") # Notify the winners. notification_text = f"Winning number: {number}\n\nCongratulations! One or more of your roulette bets paid off!\n\n" diff --git a/files/helpers/slots.py b/files/helpers/slots.py index 996b8aa13..d34194fc5 100644 --- a/files/helpers/slots.py +++ b/files/helpers/slots.py @@ -51,6 +51,19 @@ def casino_slot_pull(gambler, wager_value, currency): g.db.add(casino_game) g.db.flush() + if casino_game.winnings: + currency_log = CurrencyLog( + user_id=gambler.id, + currency=currency, + amount=-casino_game.winnings, + reason="slots bet", + ) + g.db.add(currency_log) + if currency == 'coins': + currency_log.balance = gambler.coins + else: + currency_log.balance = gambler.marseybux + return casino_game.id, casino_game.game_state else: return None, "{}", diff --git a/files/helpers/treasure.py b/files/helpers/treasure.py index b58c0f391..889f1cc8d 100644 --- a/files/helpers/treasure.py +++ b/files/helpers/treasure.py @@ -40,5 +40,5 @@ def check_for_treasure(from_comment, in_text): from_comment.treasure_amount = f'l{ticket_count}' return - user.pay_account('coins', amount) + user.pay_account('coins', amount, f"found treasure in {from_comment.textlink}") from_comment.treasure_amount = str(amount) diff --git a/files/helpers/twentyone.py b/files/helpers/twentyone.py index 390157f1a..f27fde8b2 100644 --- a/files/helpers/twentyone.py +++ b/files/helpers/twentyone.py @@ -317,9 +317,21 @@ def handle_payout(gambler, state, game): elif split_status == BlackjackStatus.PUSHED: payout += game.wager - gambler.pay_account(game.currency, payout) + if game.winnings: + currency_log = CurrencyLog( + user_id=gambler.id, + currency=game.currency, + amount=-game.winnings, + reason="blackjack bet", + ) + g.db.add(currency_log) + if currency == 'coins': + currency_log.balance = gambler.coins + else: + currency_log.balance = gambler.marseybux + if status in {BlackjackStatus.BLACKJACK, BlackjackStatus.WON} or split_status in {BlackjackStatus.WON}: distribute_wager_badges(gambler, game.wager, won=True) elif status == BlackjackStatus.LOST or split_status == BlackjackStatus.LOST: diff --git a/files/routes/admin.py b/files/routes/admin.py index 8b25db09f..08b1f952e 100644 --- a/files/routes/admin.py +++ b/files/routes/admin.py @@ -188,7 +188,7 @@ def distribute(v, kind, option_id): cid = notif_comment(text) for vote in votes: u = vote.user - u.pay_account('coins', coinsperperson) + u.pay_account('coins', coinsperperson, f"bet winnings on {parent.textlink}") add_notif(cid, u.id, text, pushnotif_url=parent.permalink) text = f"You lost the {POLL_BET_COINS} coins you bet on {parent.textlink} :marseylaugh:" @@ -2163,7 +2163,7 @@ def mark_effortpost(pid, v): coins = (p.upvotes + p.downvotes) * mul - p.author.pay_account('coins', coins) + p.author.pay_account('coins', coins, f"retroactive efortpost gains of {post.textlink}") if v.id != p.author_id: send_repeatable_notification(p.author_id, f":marseyclapping: @{v.username} (a site admin) has marked {p.textlink} as an effortpost, it now gets x{mul} coins from votes. You have received {coins} coins retroactively, thanks! :!marseyclapping:") @@ -2200,7 +2200,7 @@ def unmark_effortpost(pid, v): coins = (p.upvotes + p.downvotes) * mul - p.author.charge_account('coins', coins) + p.author.charge_account('coins', coins, f"revocation of efortpost gains of {post.textlink}") if v.id != p.author_id: send_repeatable_notification(p.author_id, f":marseyitsover: @{v.username} (a site admin) has unmarked {p.textlink} as an effortpost. {coins} coins have been deducted from you. :!marseyitsover:") diff --git a/files/routes/asset_submissions.py b/files/routes/asset_submissions.py index 4711b31e4..ef25530ab 100644 --- a/files/routes/asset_submissions.py +++ b/files/routes/asset_submissions.py @@ -225,7 +225,7 @@ def approve_emoji(v, name): if 'pkmn' in emoji.tags: amount = 500 else: amount = 250 - author.pay_account('coins', amount) + author.pay_account('coins', amount, f"reward for making :{emoji.name}:") g.db.add(author) if v.id != author.id: diff --git a/files/routes/awards.py b/files/routes/awards.py index 7035f435a..e053c7662 100644 --- a/files/routes/awards.py +++ b/files/routes/awards.py @@ -64,7 +64,7 @@ def buy_award(v, kind, AWARDS): else: currency = 'coins/marseybux' - charged = v.charge_account(currency, price) + charged = v.charge_account(currency, price, f"{kind} award cost") if not charged[0]: abort(400, f"Not enough {currency}!") @@ -238,8 +238,8 @@ def award_thing(v, thing_type, id): if kind == 'shit': awarded_coins = int(AWARDS[kind]['price'] * COSMETIC_AWARD_COIN_AWARD_PCT) - v.charge_account('coins', awarded_coins, should_check_balance=False) - obj.author.pay_account('coins', awarded_coins) + v.charge_account('coins', awarded_coins, f"shit award deflected theft on {obj.textlink}", should_check_balance=False) + obj.author.pay_account('coins', awarded_coins, f"shit award deflected theft on {obj.textlink}") elif kind != 'spider': if AWARDS[kind]['cosmetic'] and not AWARDS[kind]['included_in_lootbox']: awarded_coins = int(AWARDS[kind]['price'] * COSMETIC_AWARD_COIN_AWARD_PCT) @@ -248,8 +248,8 @@ def award_thing(v, thing_type, id): if awarded_coins: if kind == 'shit': - author.charge_account('coins', awarded_coins, should_check_balance=False) - v.pay_account('coins', awarded_coins) + author.charge_account('coins', awarded_coins, f"shit award theft on {obj.textlink}", should_check_balance=False) + v.pay_account('coins', awarded_coins, f"shit award theft on {obj.textlink}") else: author.pay_account('coins', awarded_coins) @@ -479,7 +479,7 @@ def award_thing(v, thing_type, id): author.patron = 1 if author.patron_utc: author.patron_utc += 2629746 else: author.patron_utc = int(time.time()) + 2629746 - author.pay_account('marseybux', 1250) + author.pay_account('marseybux', 1250, f"benefactor award on {obj.textlink}") badge_grant(user=v, badge_id=103) elif kind == "rehab": if author.rehab: author.rehab += 86400 diff --git a/files/routes/groups.py b/files/routes/groups.py index 71b273692..baad74709 100644 --- a/files/routes/groups.py +++ b/files/routes/groups.py @@ -35,7 +35,7 @@ def create_group(v): if name in {'everyone', 'jannies', 'holejannies', 'followers', 'commenters'} or g.db.get(Group, name): abort(400, "This group already exists!") - if not v.charge_account('coins/marseybux', GROUP_COST)[0]: + if not v.charge_account('coins/marseybux', GROUP_COST, f"cost of creating !{name}")[0]: abort(403, "You don't have enough coins or marseybux!") g.db.add(v) diff --git a/files/routes/hats.py b/files/routes/hats.py index dc34b1471..3c0f65804 100644 --- a/files/routes/hats.py +++ b/files/routes/hats.py @@ -44,12 +44,12 @@ def buy_hat(v, hat_id): if not hat.is_purchasable: abort(403, "This hat is not for sale!") - charged = v.charge_account('coins/marseybux', hat.price) + charged = v.charge_account('coins/marseybux', hat.price, f"{hat.name} hat cost") if not charged[0]: abort(400, "Not enough coins/marseybux!") v.coins_spent_on_hats += charged[1] - hat.author.pay_account('coins', hat.price * 0.1) + hat.author.pay_account('coins', hat.price * 0.1, f"royalties for `{hat.name}`") new_hat = Hat(user_id=v.id, hat_id=hat.id) g.db.add(new_hat) diff --git a/files/routes/holes.py b/files/routes/holes.py index 897692e4b..47ee405d4 100644 --- a/files/routes/holes.py +++ b/files/routes/holes.py @@ -393,7 +393,7 @@ def create_sub2(v): if not hole_group_name_regex.fullmatch(name): abort(400, "Name does not match the required format!") - if not v.charge_account('coins/marseybux', HOLE_COST)[0]: + if not v.charge_account('coins/marseybux', HOLE_COST, f"cost of creating /h/{name}")[0]: abort(400, "You don't have enough coins or marseybux!") hole = get_hole(name, graceful=True) diff --git a/files/routes/polls.py b/files/routes/polls.py index 22a16b44f..41b16ae7c 100644 --- a/files/routes/polls.py +++ b/files/routes/polls.py @@ -26,7 +26,7 @@ def vote_option(option_id, v): if option.exclusive == 2: if option.parent.total_bet_voted(v): abort(403, "You can't participate in a closed bet!") - if not v.charge_account('coins/marseybux', POLL_BET_COINS)[0]: + if not v.charge_account('coins/marseybux', POLL_BET_COINS, f"cost of bet on {option.parent.textlink}")[0]: abort(400, f"You don't have {POLL_BET_COINS} coins or marseybux!") g.db.add(v) @@ -79,7 +79,7 @@ def vote_option_comment(option_id, v): if option.exclusive == 2: if option.parent.total_bet_voted(v): abort(403, "You can't participate in a closed bet!") - if not v.charge_account('coins/marseybux', POLL_BET_COINS)[0]: + if not v.charge_account('coins/marseybux', POLL_BET_COINS, f"cost of bet on {option.parent.textlink}")[0]: abort(400, f"You don't have {POLL_BET_COINS} coins or marseybux!") g.db.add(v) diff --git a/files/routes/settings.py b/files/routes/settings.py index 246657eca..28e0a7a8f 100644 --- a/files/routes/settings.py +++ b/files/routes/settings.py @@ -381,7 +381,7 @@ def settings_personal_post(v): else: cost = HOUSE_JOIN_COST - success = v.charge_account('coins/marseybux', cost)[0] + success = v.charge_account('coins/marseybux', cost, "cost of changing houses")[0] if not success: abort(403) if house == "None": diff --git a/files/routes/users.py b/files/routes/users.py index 128d1cda2..20fc1fbdb 100644 --- a/files/routes/users.py +++ b/files/routes/users.py @@ -78,7 +78,7 @@ def claim_rewards_all_users(): marseybux = int(marseybux) text = f"You have received {marseybux} Marseybux! You can use them to buy awards or hats in the [shop](/shop/awards) or gamble them in the [casino](/casino)." - user.pay_account('marseybux', marseybux) + user.pay_account('marseybux', marseybux, f"{patron.lower()} reward") send_repeatable_notification(user.id, text) g.db.add(user) @@ -147,13 +147,13 @@ def transfer_currency(v, username, currency_name, apply_tax): notif_text += f"\n\n> {reason}" log_message += f"\n\n> {reason}" - if not v.charge_account(currency_name, amount)[0]: + if not v.charge_account(currency_name, amount, f"gift to @{username}")[0]: abort(400, f"You don't have enough {currency_name}") if currency_name == 'marseybux': - receiver.pay_account('marseybux', amount - tax) + receiver.pay_account('marseybux', amount - tax, f"gift from @{v.username}") elif currency_name == 'coins': - receiver.pay_account('coins', amount - tax) + receiver.pay_account('coins', amount - tax, f"gift from @{v.username}") else: raise ValueError(f"Invalid currency '{currency_name}' got when transferring {amount} from {v.id} to {receiver.id}") @@ -1549,3 +1549,18 @@ def usersong(username): @auth_required def user_effortposts(v, username): return redirect(f'/search/posts?q=author:{username}+effortpost:true') + +@app.get("/bank_statement") +@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400) +@limiter.limit(DEFAULT_RATELIMIT, deduct_when=lambda response: response.status_code < 400, key_func=get_ID) +@auth_required +def currency_log(v): + page = get_page() + + logs = g.db.query(CurrencyLog).filter_by(user_id=v.id) + + total = logs.count() + + logs = logs.order_by(CurrencyLog.created_utc.desc()).offset(PAGE_SIZE * (page - 1)).limit(PAGE_SIZE).all() + + return render_template("bank_statement.html", v=v, logs=logs, page=page, total=total) diff --git a/files/templates/bank_statement.html b/files/templates/bank_statement.html new file mode 100644 index 000000000..c46bd2b97 --- /dev/null +++ b/files/templates/bank_statement.html @@ -0,0 +1,38 @@ +{% extends "default.html" %} +{% block pagetitle %}Bank Statement{% endblock %} +{% block content %} +

+
+
+

Bank Statement

+
+
Balance
+ +
+ {% for log in logs %} +
+
+
+ {{log.currency}} +
+
+
+
+ {{log.amount}} {{log.currency}}   {{log.reason | safe}} +
+
+ {{log.age_string}} +
+
+
+ {{log.balance}} {{log.currency}} +
+
+ {% else %} +
There's nothing here right now.
+ {% endfor %} +
+ {% include "pagination.html" %} +
+
+{% endblock %} diff --git a/files/templates/header.html b/files/templates/header.html index 3feb1e401..8cf0d477e 100644 --- a/files/templates/header.html +++ b/files/templates/header.html @@ -337,7 +337,7 @@
{{v.username}}
-
coins{{v.coins}}{% if not FEATURES['MARSEYBUX'] %} Coin{{macros.plural(v.coins)}}{% endif %}
+
coins{{v.coins}}{% if not FEATURES['MARSEYBUX'] %} Coin{{macros.plural(v.coins)}}{% endif %}
{% if FEATURES['MARSEYBUX'] %}
marseybux{{v.marseybux}}
{% endif %} @@ -415,7 +415,7 @@ {% if v %}
{{u.coins}} - coins + coins {% if FEATURES['MARSEYBUX'] %} {{u.marseybux}} @@ -415,7 +415,7 @@ {{u.coins}} - coins + coins {% if FEATURES['MARSEYBUX'] %} {{u.marseybux}} diff --git a/migrations/20240302-add-currency-logs.sql b/migrations/20240302-add-currency-logs.sql new file mode 100644 index 000000000..977d97def --- /dev/null +++ b/migrations/20240302-add-currency-logs.sql @@ -0,0 +1,26 @@ +create table currency_logs ( + id integer primary key, + user_id integer not null, + created_utc integer not null, + currency varchar(9) not null, + amount integer not null, + reason varchar(1000) not null, + balance integer not null +); + +CREATE SEQUENCE public.currency_logs_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + +ALTER SEQUENCE public.currency_logs_id_seq OWNED BY public.currency_logs.id; + +ALTER TABLE ONLY public.currency_logs ALTER COLUMN id SET DEFAULT nextval('public.currency_logs_id_seq'::regclass); + +alter table only currency_logs + add constraint currency_logs_user_fkey foreign key (user_id) references public.users(id); + +create index currency_logs_index on currency_logs using btree (user_id);