Improve price information
Django CI/CD / test (push) Successful in 1m1s Details
Django CI/CD / build-and-push (push) Successful in 2m9s Details

This commit is contained in:
Lukáš Kucharczyk 2025-01-30 16:38:13 +01:00
parent 4ec808eeec
commit 6f62889e92
Signed by: lukas
SSH Key Fingerprint: SHA256:vMuSwvwAvcT6htVAioMP7rzzwMQNi3roESyhv+nAxeg
10 changed files with 123 additions and 15 deletions

View File

@ -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():

View File

@ -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),
),
]

View File

@ -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),
),
]

View File

@ -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),
]

View File

@ -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

12
games/signals.py Normal file
View File

@ -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"])

View File

@ -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()
)
)

View File

@ -0,0 +1 @@
<span title="Price is a result of conversion and rounding." class="decoration-dotted underline">{{ slot }}</span>

View File

@ -20,13 +20,12 @@
</a>
</div>
<div>
Price:
{% if purchase.converted_price %}
{{ purchase.converted_price | floatformat }} {{ purchase.converted_currency }}
{% else %}
None
{% endif %}
({{ purchase.price | floatformat }} {{ purchase.price_currency }})
<p>
Price:
<c-price-converted>{{ purchase.standardized_price }}</c-price-converted>
({{ purchase.price | floatformat:2 }} {{ purchase.price_currency }})
</p>
<p>Price per game: <c-price-converted>{{ purchase.price_per_game | floatformat:0 }} {{ purchase.converted_currency }}</c-price-converted> </p>
</div>
<div>
<h2 class="text-base">Items:</h2>

View File

@ -47,7 +47,7 @@ INSTALLED_APPS = [
Q_CLUSTER = {
"name": "DjangoQ",
"workers": 4,
"workers": 1,
"recycle": 500,
"timeout": 60,
"retry": 120,