From 3c28ecae77f7a1643d2d538860d3cc92c018b14d Mon Sep 17 00:00:00 2001 From: Murilo Rosa Date: Fri, 15 Mar 2024 14:27:32 +0000 Subject: [PATCH] save state using transactions with balance --- development_notebook/2024-03-07.md | 29 +++++++++++++++ development_notebook/tools.md | 1 + negligent_octopus/core/admin.py | 10 ++++-- negligent_octopus/core/models.py | 58 ++++++++++++++++++++++-------- 4 files changed, 81 insertions(+), 17 deletions(-) create mode 100644 development_notebook/2024-03-07.md diff --git a/development_notebook/2024-03-07.md b/development_notebook/2024-03-07.md new file mode 100644 index 0000000..5bc7777 --- /dev/null +++ b/development_notebook/2024-03-07.md @@ -0,0 +1,29 @@ +# 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. As development environment we are remotely accesing a linux system and using tmux and nvim, with the relevant plugins for code review. + +Core models are being defined and added to the django-admin interface to be tested. + +# Today + +Define automatic balance tracking in account. + +## Work Log +__InProgress__ +* Use django's signal post-save to update accounts balance. + +__ToDo__ +* Migrate. +* Test on admin. + +__Done__ + +__Discarded__ +~~Define balance property, as well as relevant new db fields - assuming fields defined in Transaction. Define fields and functions in Transaction. Check FieldTracker in model\_utils.~~ This is overcomplicated. Strategy changed to above. + +# To Do +[[Future Work]] Consider that it would be a high usability information to have current balance on each transaction. One possibility would be same as balance in account (We could send a event to account and use it as a manager). Another way would be doing it lazily from our current balance backwards (but this assumes end of timeframe is last transaction - which might not be true). diff --git a/development_notebook/tools.md b/development_notebook/tools.md index 96e2cc7..1bac951 100644 --- a/development_notebook/tools.md +++ b/development_notebook/tools.md @@ -16,6 +16,7 @@ tmux attach-session -t \ `Ctrl + b` then `"` ? Split vertical +`Ctrl + b` then `Alt+[Arrow UDLR]` `Ctrl + b` then `:resize-pane -[UDLR] N` ? Resize window by `N` to [Up, Down, Left, Right] diff --git a/negligent_octopus/core/admin.py b/negligent_octopus/core/admin.py index 2fcea2f..e29ac49 100644 --- a/negligent_octopus/core/admin.py +++ b/negligent_octopus/core/admin.py @@ -8,11 +8,15 @@ class AccountAdmin(admin.ModelAdmin): list_display = ["name", "owner", "balance", "modified", "created", "is_removed"] search_fields = ["name", "owner__username"] - list_filter = ["is_removed", "modified"] + list_filter = [ + "owner", + "is_removed", + "modified", + ] # TODO: Check modified catches transaction changes @admin.register(Transaction) class TransactionAdmin(admin.ModelAdmin): - list_display = ["title", "account", "get_account_owner", "date", "amount"] + list_display = ["title", "account", "get_account_owner", "amount", "timestamp"] search_fields = ["account__owner__username", "account__name", "title"] - list_filter = ["account__owner", "account", "date"] + list_filter = ["account__owner", "account", "timestamp"] diff --git a/negligent_octopus/core/models.py b/negligent_octopus/core/models.py index 683ee21..9c63457 100644 --- a/negligent_octopus/core/models.py +++ b/negligent_octopus/core/models.py @@ -1,7 +1,9 @@ from django.db import models +from django.db import transaction as db_transaction from django.utils import timezone 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 @@ -9,22 +11,13 @@ class Account(TimeStampedModel, SoftDeletableModel): owner = models.ForeignKey(User, on_delete=models.CASCADE) name = models.CharField(max_length=255) - calculated_balance = models.FloatField(default=0.0, editable=False) - calculated_at = models.DateTimeField(auto_now_add=True) @property def balance(self): - # Set calculated_at to now (now_at) - # Get relevant transaction (Get creation or modified after now_at) - # For each do a running sum - # taking in consideration delta of all modifications after now_at - # Check for any modification on is_removed (just negative of last calculation?) - # Update calculated_balance and return it - # #IMPROVE : Evaluate using atomic or another techinique to prevent conflicts - pass + return self.transaction_set.last().balance def __str__(self): - return self.name + return str(self.name) class Meta(TimeStampedModel.Meta, SoftDeletableModel.Meta): verbose_name = "Account" @@ -33,19 +26,56 @@ class Meta(TimeStampedModel.Meta, SoftDeletableModel.Meta): class Transaction(TimeStampedModel): - account = models.ForeignKey(Account, on_delete=models.CASCADE) amount = models.FloatField() - date = models.DateField(default=timezone.now) + timestamp = models.DateTimeField(default=timezone.now) + account = models.ForeignKey(Account, on_delete=models.CASCADE) + balance = models.FloatField(default=0.0, editable=False) 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 + 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 class Meta(TimeStampedModel.Meta): verbose_name = "Transaction" verbose_name_plural = "Transactions" - ordering = ["date", "-modified"] + ordering = ["timestamp", "-modified"] + unique_together = ["timestamp", "created"]