diff --git a/games/apps.py b/games/apps.py index cc6588c..0cc802d 100644 --- a/games/apps.py +++ b/games/apps.py @@ -11,6 +11,8 @@ class GamesConfig(AppConfig): name = "games" def ready(self): + import games.signals # noqa: F401 + post_migrate.connect(schedule_tasks, sender=self) @@ -26,6 +28,14 @@ def schedule_tasks(sender, **kwargs): next_run=now() + timedelta(seconds=30), ) + if not Schedule.objects.filter(name="Update price per game").exists(): + schedule( + "games.tasks.calculate_price_per_game", + name="Update price per game", + schedule_type=Schedule.MINUTES, + next_run=now() + timedelta(seconds=30), + ) + from games.models import ExchangeRate if not ExchangeRate.objects.exists(): diff --git a/games/migrations/0002_purchase_price_per_game.py b/games/migrations/0002_purchase_price_per_game.py new file mode 100644 index 0000000..efeb68a --- /dev/null +++ b/games/migrations/0002_purchase_price_per_game.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.5 on 2025-01-30 11:04 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='purchase', + name='price_per_game', + field=models.FloatField(null=True), + ), + ] diff --git a/games/migrations/0003_purchase_updated_at.py b/games/migrations/0003_purchase_updated_at.py new file mode 100644 index 0000000..720ea6f --- /dev/null +++ b/games/migrations/0003_purchase_updated_at.py @@ -0,0 +1,18 @@ +# Generated by Django 5.1.5 on 2025-01-30 11:12 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('games', '0002_purchase_price_per_game'), + ] + + operations = [ + migrations.AddField( + model_name='purchase', + name='updated_at', + field=models.DateTimeField(auto_now=True), + ), + ] diff --git a/games/migrations/0004_purchase_num_purchases.py b/games/migrations/0004_purchase_num_purchases.py new file mode 100644 index 0000000..83e6182 --- /dev/null +++ b/games/migrations/0004_purchase_num_purchases.py @@ -0,0 +1,28 @@ +# Generated by Django 5.1.5 on 2025-01-30 11:57 + +from django.db import migrations, models +from django.db.models import Count + + +def initialize_num_purchases(apps, schema_editor): + Purchase = apps.get_model("games", "Purchase") + purchases = Purchase.objects.annotate(num_games=Count("games")) + + for purchase in purchases: + purchase.num_purchases = purchase.num_games + purchase.save(update_fields=["num_purchases"]) + + +class Migration(migrations.Migration): + dependencies = [ + ("games", "0003_purchase_updated_at"), + ] + + operations = [ + migrations.AddField( + model_name="purchase", + name="num_purchases", + field=models.IntegerField(default=0), + ), + migrations.RunPython(initialize_num_purchases), + ] diff --git a/games/models.py b/games/models.py index a61de92..15e0cd3 100644 --- a/games/models.py +++ b/games/models.py @@ -116,6 +116,8 @@ class Purchase(models.Model): price_currency = models.CharField(max_length=3, default="USD") converted_price = models.FloatField(null=True) converted_currency = models.CharField(max_length=3, null=True) + price_per_game = models.FloatField(null=True) + num_purchases = models.IntegerField(default=0) ownership_type = models.CharField( max_length=2, choices=OWNERSHIP_TYPES, default=DIGITAL ) @@ -130,19 +132,16 @@ class Purchase(models.Model): related_name="related_purchases", ) created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) @property def standardized_price(self): return ( - f"{floatformat(self.converted_price)} {self.converted_currency}" + f"{floatformat(self.converted_price, 0)} {self.converted_currency}" if self.converted_price else None ) - @property - def num_purchases(self): - return self.games.count() - @property def has_one_item(self): return self.games.count() == 1 diff --git a/games/signals.py b/games/signals.py new file mode 100644 index 0000000..032dbcc --- /dev/null +++ b/games/signals.py @@ -0,0 +1,12 @@ +from django.db.models.signals import m2m_changed +from django.dispatch import receiver +from django.utils.timezone import now + +from games.models import Purchase + + +@receiver(m2m_changed, sender=Purchase.games.through) +def update_num_purchases(sender, instance, **kwargs): + instance.num_purchases = instance.games.count() + instance.updated_at = now() + instance.save(update_fields=["num_purchases"]) diff --git a/games/tasks.py b/games/tasks.py index 85750a8..3c960ec 100644 --- a/games/tasks.py +++ b/games/tasks.py @@ -1,4 +1,8 @@ import requests +from django.db.models import ExpressionWrapper, F, FloatField, Q +from django.template.defaultfilters import floatformat +from django.utils.timezone import now +from django_q.models import Task from games.models import ExchangeRate, Purchase @@ -51,7 +55,7 @@ def convert_prices(): currency_from=currency_from, currency_to=currency_to, year=year, - rate=rate, + rate=floatformat(rate, 2), ) else: print("Could not get an exchange rate.") @@ -61,5 +65,24 @@ def convert_prices(): ) if exchange_rate: save_converted_info( - purchase, purchase.price * exchange_rate.rate, currency_to + purchase, + floatformat(purchase.price * exchange_rate.rate, 0), + currency_to, ) + + +def calculate_price_per_game(): + try: + last_task = Task.objects.filter(group="Update price per game").first() + last_run = last_task.started + except Task.DoesNotExist or AttributeError: + last_run = now() + purchases = Purchase.objects.filter(converted_price__isnull=False).filter( + Q(updated_at__gte=last_run) | Q(price_per_game__isnull=True) + ) + print(f"Updating {purchases.count()} purchases.") + purchases.update( + price_per_game=ExpressionWrapper( + F("converted_price") / F("num_purchases"), output_field=FloatField() + ) + ) diff --git a/games/templates/cotton/price_converted.html b/games/templates/cotton/price_converted.html new file mode 100644 index 0000000..5e0adcb --- /dev/null +++ b/games/templates/cotton/price_converted.html @@ -0,0 +1 @@ +{{ slot }} \ No newline at end of file diff --git a/games/templates/view_purchase.html b/games/templates/view_purchase.html index a9a6aa6..040c06d 100644 --- a/games/templates/view_purchase.html +++ b/games/templates/view_purchase.html @@ -20,13 +20,12 @@
+ Price:
+
Price per game: