From 02e3ea133f739f3263f9c92fd8e6adc19fb634cf Mon Sep 17 00:00:00 2001 From: Murilo Rosa Date: Mon, 18 Mar 2024 01:56:51 +0000 Subject: [PATCH] Implement balance as prefix-sum --- development_notebook/2024-03-17.md | 35 +++++++++ development_notebook/tools.md | 50 +++++++++--- negligent_octopus/core/admin.py | 11 ++- .../0002_transaction_account_add_balance.py | 44 +++++++++++ negligent_octopus/core/models.py | 77 ++++++++++--------- 5 files changed, 169 insertions(+), 48 deletions(-) create mode 100644 development_notebook/2024-03-17.md create mode 100644 negligent_octopus/core/migrations/0002_transaction_account_add_balance.py diff --git a/development_notebook/2024-03-17.md b/development_notebook/2024-03-17.md new file mode 100644 index 0000000..c6a67aa --- /dev/null +++ b/development_notebook/2024-03-17.md @@ -0,0 +1,35 @@ +# Current State + +See [[2024-03-06]] for previous log. + +Deployed to production at [negligentoctopus.pythonanywhere.com](negligentoctopus.pythonanywhere.com). + +Project has been fully configured from cookiecutter-django using MySQL. + +Core models are being defined and added to the django-admin interface to be tested. + +## Update + +Account and transactions are implemented with balance as prefix-sum. + +# Today + +Define automatic balance tracking in account. + +Prefix-sum: This requires a strict ordering. When adding, deleting or changing the amount: Just add it according to the ordering, then update balance on all transactions that follow. The problem here is that we'll change many transactions, so having such a big lock is not a good ideia, and without having a lock we might access a transaction before it is updated. + - Strict Ordering: is implemented by standard on django model Meta.order\_by + +## Work Log +__InProgress__ + +__ToDo__ +* Implement changeable to amt +* Implement changeable to timestamp +* Implement initial balance to accounts - change balance on transation to be relative and set a balance property | requires change to update\_after and prev to use new field + +__Done__ +* Implement balance as prefix-sum. +* Migrate. +* Test on admin. + +# To Do diff --git a/development_notebook/tools.md b/development_notebook/tools.md index 1bac951..e71209b 100644 --- a/development_notebook/tools.md +++ b/development_notebook/tools.md @@ -2,33 +2,65 @@ ## tmux +prefix: `Ctrl + a` or `Ctrl + b` + ### Session -tmux list-sessions -tmux attach-session -t \ +tmux ps + ? list sessions + +tmux at -t \ + ? Enter session ### Windows -`Ctrl + b` then \ - ? Change window +`prefix` then `[hjkl] + ? Change window using VIM keys -`Ctrl + b` then `%` +`prefix` then `%` ? Split horizontal -`Ctrl + b` then `"` +`prefix` then `"` ? Split vertical -`Ctrl + b` then `Alt+[Arrow UDLR]` -`Ctrl + b` then `:resize-pane -[UDLR] N` +`prefix` then `[HJKL]` ? Resize window by `N` to [Up, Down, Left, Right] ## NVim +`Ctrl + n` or `Ctrl + \` + ? Cycle autocomplete options + +`,` then `d` + ? Jump to definition + - Hint: duplicate window using `:sp` before jump + :sp \ ? Split vertical :vsp \ ? Split horizontal -### TODO -> Include plugin commands +### Windows +`Ctrl + W` then `[hjkl]` + ? Move cursor to window + +`Ctrl + W` then `[HJKL]` + ? Move window + +`Ctrl + W` then `[+ -]` + ? Resize window vertically + +`Ctrl + W` then `[\< \>]` + ? Resize window horizontally + +`Ctrl + W` then `\_` + ? Resize to near full-screen + +`Ctrl + W` then `\=` + ? Resize to equal parts + +### NERDTree +`:NERDTree` + ## Bash diff --git a/negligent_octopus/core/admin.py b/negligent_octopus/core/admin.py index e29ac49..119bf04 100644 --- a/negligent_octopus/core/admin.py +++ b/negligent_octopus/core/admin.py @@ -13,10 +13,19 @@ class AccountAdmin(admin.ModelAdmin): "is_removed", "modified", ] # TODO: Check modified catches transaction changes + readonly_fields = ["balance"] @admin.register(Transaction) class TransactionAdmin(admin.ModelAdmin): - list_display = ["title", "account", "get_account_owner", "amount", "timestamp"] + list_display = [ + "title", + "account", + "get_account_owner", + "amount", + "timestamp", + "balance", + ] search_fields = ["account__owner__username", "account__name", "title"] list_filter = ["account__owner", "account", "timestamp"] + readonly_fields = ["balance"] diff --git a/negligent_octopus/core/migrations/0002_transaction_account_add_balance.py b/negligent_octopus/core/migrations/0002_transaction_account_add_balance.py new file mode 100644 index 0000000..6ad6ae5 --- /dev/null +++ b/negligent_octopus/core/migrations/0002_transaction_account_add_balance.py @@ -0,0 +1,44 @@ +# Generated by Django 4.2.10 on 2024-03-18 00:12 + +from django.db import migrations, models +import django.utils.timezone + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0001_initial"), + ] + + operations = [ + migrations.AlterModelOptions( + name="transaction", + options={ + "ordering": ["-timestamp", "-created"], + "verbose_name": "Transaction", + "verbose_name_plural": "Transactions", + }, + ), + migrations.RemoveField( + model_name="account", + name="balance", + ), + migrations.AddField( + model_name="transaction", + name="balance", + field=models.FloatField(editable=False, null=True), + ), + migrations.AddField( + model_name="transaction", + name="timestamp", + field=models.DateTimeField(default=django.utils.timezone.now), + ), + migrations.AlterUniqueTogether( + name="transaction", + unique_together={("timestamp", "created")}, + ), + migrations.RemoveField( + model_name="transaction", + name="date", + ), + ] diff --git a/negligent_octopus/core/models.py b/negligent_octopus/core/models.py index 9c63457..fbfe9bc 100644 --- a/negligent_octopus/core/models.py +++ b/negligent_octopus/core/models.py @@ -1,9 +1,9 @@ from django.db import models from django.db import transaction as db_transaction from django.utils import timezone +from django.utils.functional import cached_property from model_utils.models import SoftDeletableModel from model_utils.models import TimeStampedModel -from model_utils.tracker import FieldTracker from negligent_octopus.users.models import User @@ -12,9 +12,9 @@ class Account(TimeStampedModel, SoftDeletableModel): owner = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=255) - @property + @cached_property def balance(self): - return self.transaction_set.last().balance + return self.transaction_set.first().balance def __str__(self): return str(self.name) @@ -26,56 +26,57 @@ class Meta(TimeStampedModel.Meta, SoftDeletableModel.Meta): class Transaction(TimeStampedModel): + account = models.ForeignKey(Account, on_delete=models.CASCADE) amount = models.FloatField() timestamp = models.DateTimeField(default=timezone.now) - account = models.ForeignKey(Account, on_delete=models.CASCADE) - balance = models.FloatField(default=0.0, editable=False) + balance = models.FloatField(editable=False, null=True) title = models.CharField(max_length=127) description = models.TextField(blank=True) - tracker = FieldTracker( - fields=["amount", "timestamp"], - ) # We need to consider changes to the timestamp - # and how to update balances based on these - def get_account_owner(self): return str(self.account.owner) @db_transaction.atomic def save(self, *args, **kwargs): - universe = self.account.transaction_set - - last_transaction = ( - universe.filter( - timestamp__lte=self.timestamp, - ) - .order_by("-created") - .first() - ) - self.balance = last_transaction.balance + self.amount + old_model = self.__class__.objects.get(pk=self.pk) + if old_model.timestamp != self.timestamp or old_model.account != self.account: + msg = "Cannot change fields 'account' or 'timestamp'." + raise ValueError(msg) # TODO: Change fields account or timestamp + + if old_model.amount != self.amount: + msg = "Cannot change field 'amount'." + raise ValueError(msg) + # TODO: Id what trans is (del, change, create) and calculate for approp amt + + if self.balance is None: + self.balance = self.amount + last_transaction = self.account.transaction_set.filter( + models.Q(timestamp__lte=self.timestamp) + & models.Q(created__lt=self.created), + ).first() + if last_transaction is not None: + self.balance += last_transaction.balance + + universe = self.account.transaction_set.filter( + models.Q(timestamp__gte=self.timestamp) + & models.Q(created__gt=self.created), + ).reverse() + + previous = universe.first() + for transaction in universe: + if transaction == universe.first(): + continue + transaction.balance = transaction.amount + previous.balance + transaction.save() + previous = transaction + super().save(*args, **kwargs) - last_transaction = self - - to_update = universe.filter( - timestamp__gte=self.timestamp, - ).exclude( - created__lt=self.created, - ) - for transaction in to_update: - transaction.balance = last_transaction.balance + transaction.amount - transaction.save() - last_transaction = transaction - - def delete(self, *args, **kwargs): - self.amount = -self.amount - self.save() - super().delete(*args, **kwargs) def __str__(self): - return self.title + return str(self.title) class Meta(TimeStampedModel.Meta): verbose_name = "Transaction" verbose_name_plural = "Transactions" - ordering = ["timestamp", "-modified"] + ordering = ["-timestamp", "-created"] unique_together = ["timestamp", "created"]