Skip to content

Commit

Permalink
Refactor the way it stores the prices
Browse files Browse the repository at this point in the history
The exact same product might have slightly different names in different stores.
So we need to make a more complex relation

Product -> ProductNameLocation -> ProductPrice
  • Loading branch information
vijoin committed Dec 18, 2023
1 parent b3e593e commit 083b3f8
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 57 deletions.
18 changes: 16 additions & 2 deletions products/admin.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
from django.contrib import admin
from .models import Product, ProductCategory, ProductBrand
from .models import Product, ProductCategory, ProductBrand, ProductNameLocation, ProductPrice, Location


class ProductAdmin(admin.ModelAdmin):
list_display = ["name", "brand", "category"]
list_display = ["generic_name", "brand", "category"]

class ProductCategoryAdmin(admin.ModelAdmin):
list_display = ["name", "description"]

class ProductBrandAdmin(admin.ModelAdmin):
list_display = ["name", "description"]

class ProductNameLocationAdmin(admin.ModelAdmin):
list_display = ["product", "location", "name"]

class ProductPriceAdmin(admin.ModelAdmin):
list_display = ["product", "timestamp", "price"]

class LocationAdmin(admin.ModelAdmin):
list_display = ["name", "base_url", "get_product_url"]


admin.site.register(Product, ProductAdmin)
admin.site.register(ProductCategory, ProductCategoryAdmin)
admin.site.register(ProductBrand, ProductBrandAdmin)
admin.site.register(ProductNameLocation, ProductNameLocationAdmin)
admin.site.register(ProductPrice, ProductPriceAdmin)
admin.site.register(Location, LocationAdmin)

26 changes: 22 additions & 4 deletions products/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Generated by Django 5.0 on 2023-12-16 22:55
# Generated by Django 5.0 on 2023-12-17 18:01

import django.db.models.deletion
from django.db import migrations, models
Expand All @@ -12,6 +12,15 @@ class Migration(migrations.Migration):
]

operations = [
migrations.CreateModel(
name='Location',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('base_url', models.CharField(blank=True, max_length=200)),
('get_product_url', models.CharField(blank=True, max_length=200)),
],
),
migrations.CreateModel(
name='ProductBrand',
fields=[
Expand All @@ -32,20 +41,29 @@ class Migration(migrations.Migration):
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('code', models.CharField(max_length=10)),
('generic_name', models.CharField(max_length=200)),
('description', models.TextField()),
('brand', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.productbrand')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='products', to='products.productcategory')),
],
),
migrations.CreateModel(
name='ProductNameLocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200)),
('location', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_names', to='products.location')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_name_at_locations', to='products.product')),
],
),
migrations.CreateModel(
name='ProductPrice',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('timestamp', models.DateTimeField(auto_now_add=True)),
('location', models.CharField(max_length=200)),
('price', models.DecimalField(decimal_places=2, max_digits=6)),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prices', to='products.product')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='prices', to='products.productnamelocation')),
],
options={
'ordering': ('-timestamp',),
Expand Down
96 changes: 72 additions & 24 deletions products/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,60 +20,108 @@ def __str__(self):
return self.name


class Product(models.Model):
class Location(models.Model):
name = models.CharField(max_length=200)
base_url = models.CharField(max_length=200, blank=True)
get_product_url = models.CharField(max_length=200, blank=True)

def __str__(self):
return self.name

class Product(models.Model):
code = models.CharField(max_length=10)
generic_name = models.CharField(max_length=200)
description = models.TextField()
category = models.ForeignKey(ProductCategory, on_delete=models.CASCADE, related_name="products")
brand = models.ForeignKey(ProductBrand, on_delete=models.CASCADE, related_name="products")

def __str__(self):
return self.name
return f"{self.generic_name} ({self.brand.name})"

@property
def latest_max_price(self):
latest_prices = self.get_latest_prices()
# @property
# def latest_max_price(self):
# latest_prices = self.get_latest_prices()

# Get the maximum value of the latest prices
max_latest_price = latest_prices.aggregate(Max('latest_price', output_field=DecimalField()))['latest_price__max']
# # Get the maximum value of the latest prices
# max_latest_price = latest_prices.aggregate(Max('latest_price', output_field=DecimalField()))['latest_price__max']

return max_latest_price
# return max_latest_price

@property
def latest_min_price(self):
latest_prices = self.get_latest_prices()
# @property
# def latest_min_price(self):
# latest_prices = self.get_latest_prices()

# Get the maximum value of the latest prices
min_latest_price = latest_prices.aggregate(Min('latest_price', output_field=DecimalField()))['latest_price__min']
# # Get the maximum value of the latest prices
# min_latest_price = latest_prices.aggregate(Min('latest_price', output_field=DecimalField()))['latest_price__min']

return min_latest_price
# return min_latest_price

def get_latest_prices(self):
"""Get most recent prices for each location"""
# def get_latest_prices(self):
# """Get most recent prices for each location"""

# # Query to get distinct locations from ProductPrice model
# # distinct_locations = ProductPrice.objects.values('location').distinct()
# distinct_locations = ProductPrice.objects.all()

# Query to get distinct locations from ProductPrice model
distinct_locations = ProductPrice.objects.values('location').distinct()
# # Subquery to get the latest timestamp for each location
# latest_timestamp_subquery = ProductPrice.objects.filter(
# location=OuterRef('location')
# ).order_by('-timestamp').values('timestamp')[:1]

# # Query to get the latest price for each location
# latest_prices = ProductPrice.objects.filter(
# location__in=Subquery(distinct_locations.values('location')),
# timestamp=Subquery(latest_timestamp_subquery)
# ).annotate(
# latest_price=Coalesce('price', 0)
# ).values('location', 'latest_price')

# return latest_prices

def get_latest_prices(self):
# Subquery to get the latest timestamp for each location
latest_timestamp_subquery = ProductPrice.objects.filter(
location=OuterRef('location')
product__product=self
).order_by('-timestamp').values('timestamp')[:1]

# Query to get the latest price for each location
latest_prices = ProductPrice.objects.filter(
location__in=Subquery(distinct_locations.values('location')),
product__product=self,
timestamp=Subquery(latest_timestamp_subquery)
).annotate(
latest_price=Coalesce('price', 0)
).values('location', 'latest_price')
latest_price=Coalesce('price', 0),
location_name=OuterRef('product__location__name')
).values('location_name', 'latest_price')

return latest_prices

def get_max_latest_price(self):
# Get the maximum value of the latest prices
max_latest_price = self.get_latest_prices().aggregate(Max('latest_price', output_field=DecimalField()))['latest_price__max']
return max_latest_price

def get_min_latest_price(self):
# Get the minimum value of the latest prices
min_latest_price = self.get_latest_prices().aggregate(Min('latest_price', output_field=DecimalField()))['latest_price__min']
return min_latest_price

class ProductNameLocation(models.Model):
"""Save different names at different stores for the same product"""
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name="product_name_at_locations")
location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="product_names")
name = models.CharField(max_length=200)

def __str__(self):
return f"{self.name} ({self.product.brand}) at {self.location}"

class ProductPrice(models.Model):
product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='prices')
product = models.ForeignKey(ProductNameLocation, on_delete=models.CASCADE, related_name='prices')
timestamp = models.DateTimeField(auto_now_add=True)
location = models.CharField(max_length=200)
# location = models.ForeignKey(Location, on_delete=models.CASCADE, related_name="product_prices_at_locations")
price = models.DecimalField(max_digits=6, decimal_places=2)

def __str__(self):
return f"{self.product.name}: {self.price} at {self.product.location.name}"

class Meta:
ordering = ("-timestamp",)
12 changes: 10 additions & 2 deletions products/templates/product_list.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,18 @@ <h2>Product List</h2>
<ul>
{% for product in products %}
<li>
<strong>Code:</strong> {{ product.code }}<br>
<strong>Category:</strong> {{ product.category }}<br>
<strong>Brand:</strong> {{ product.brand }}<br>
<strong>Name:</strong> {{ product.name }}<br>
<strong>Description:</strong> {{ product.description }}
<strong>Name:</strong> {{ product.generic_name }}<br>
<strong>Description:</strong> {{ product.description }}<br>
<strong>Prices:</strong>
<ul>
<li>TODO: Price 1</li>
<li>TODO: Price 2</li>
<li>TODO: Price 3</li>
</ul>

</li>
<br>
{% endfor %}
Expand Down
59 changes: 34 additions & 25 deletions products/tests.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,48 @@
from django.test import TestCase
from .models import Product, ProductBrand, ProductCategory, ProductPrice
from .models import Product, ProductBrand, ProductCategory, ProductPrice, ProductNameLocation, Location


class ProductTest(TestCase):
def setUp(self):
brand = ProductBrand.objects.create(name="Brand1", description="Brand1")
category = ProductCategory.objects.create(name="Category1", description="Category1")
Product.objects.create(name="Product1", brand=brand, category=category)
product = Product.objects.create(code="A01", generic_name="Product1", brand=brand, category=category)
location_disco = Location.objects.create(name="Disco")
location_tata = Location.objects.create(name="Ta-Ta")

def test_product_is_created(self):
product = Product.objects.get(name="Product1")
self.assertEqual(product.name, "Product1")
self.assertEqual(product.brand.name, "Brand1")
self.assertEqual(product.category.name, "Category1")
self.product_name_at_disco = ProductNameLocation.objects.create(product=product, location=location_disco, name="Product #1")
self.product_name_at_tata = ProductNameLocation.objects.create(product=product, location=location_tata, name="Product Number1")

# def test_product_is_created(self):
# product = Product.objects.get(generic_name="Product1")
# self.assertEqual(product.generic_name, "Product1")
# self.assertEqual(product.brand.name, "Brand1")
# self.assertEqual(product.category.name, "Category1")

def test_latest_max_and_min_price(self):
product = Product.objects.get(name="Product1")
product = Product.objects.get(generic_name="Product1")

price1 = ProductPrice.objects.create(product=product, location="Disco", price=99)
price2 = ProductPrice.objects.create(product=product, location="Ta-Ta", price=100)
product.prices.add(price1, price2)
price1 = ProductPrice.objects.create(product=self.product_name_at_disco, price=99)
price2 = ProductPrice.objects.create(product=self.product_name_at_tata, price=100)
# self.product_name_at_disco.prices.add(price1)

price3 = ProductPrice.objects.create(product=product, location="Disco", price=99)
price4 = ProductPrice.objects.create(product=product, location="Ta-Ta", price=98)
product.prices.add(price3, price4)
price4 = ProductPrice.objects.create(product=self.product_name_at_tata, price=98)
price3 = ProductPrice.objects.create(product=self.product_name_at_disco, price=99)
# product.prices.add(price3, price4)

price5 = ProductPrice.objects.create(product=product, location="Disco", price=96)
price6 = ProductPrice.objects.create(product=product, location="Ta-Ta", price=97)
product.prices.add(price5, price6)
self.assertEqual(product.get_min_latest_price(), 98)
self.assertEqual(product.get_max_latest_price(), 99)

self.assertEqual(product.latest_min_price, 96)
self.assertEqual(product.latest_max_price, 97)

def test_upload_xlsx(self):
# Create a XLSX file
# Send a POST Request imitating the form
# Validate that all products were created, counting them
raise NotImplementedError
# price5 = ProductPrice.objects.create(product=self.product_name_at_disco, price=96)
# price6 = ProductPrice.objects.create(product=self.product_name_at_tata, price=97)
# product.product_name_at_locations.prices.add(price5, price6)

# self.assertEqual(product.latest_min_price, 96)
# self.assertEqual(product.get_min_latest_price(), 96)
# self.assertEqual(product.get_max_latest_price(), 97)

# def test_upload_xlsx(self):
# # Create a XLSX file
# # Send a POST Request imitating the form
# # Validate that all products were created, counting them
# raise NotImplementedError

0 comments on commit 083b3f8

Please sign in to comment.