Skip to content

Commit

Permalink
feat(Price): new type field (#578)
Browse files Browse the repository at this point in the history
  • Loading branch information
raphodn authored Nov 26, 2024
1 parent 978a401 commit f6d189b
Show file tree
Hide file tree
Showing 11 changed files with 128 additions and 27 deletions.
4 changes: 4 additions & 0 deletions open_prices/api/prices/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ class Meta:
model = Price
fields = Price.CREATE_FIELDS

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.fields["type"].required = False


class PriceUpdateSerializer(serializers.ModelSerializer):
class Meta:
Expand Down
29 changes: 25 additions & 4 deletions open_prices/api/prices/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,21 +120,21 @@ def setUpTestData(cls):
owner=cls.user_session.user.user_id,
)
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
**PRICE_APPLES,
labels_tags=[],
origins_tags=["en:spain"],
owner=cls.user_session.user.user_id,
)
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
**PRICE_APPLES,
labels_tags=["en:organic"],
origins_tags=["en:unknown"],
owner=cls.user_session.user.user_id,
)
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
**PRICE_APPLES,
labels_tags=["en:organic"],
origins_tags=["en:france"],
Expand Down Expand Up @@ -465,6 +465,27 @@ def test_price_create_with_location_id(self):
)
self.assertEqual(response.status_code, 201)

def test_price_create_with_type(self):
data = self.data.copy()
# without type? see other tests
# correct type
response = self.client.post(
self.url,
{**data, "type": price_constants.TYPE_PRODUCT},
headers={"Authorization": f"Bearer {self.user_session.token}"},
content_type="application/json",
)
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data["type"], price_constants.TYPE_PRODUCT)
# wrong type
response = self.client.post(
self.url,
{**data, "type": price_constants.TYPE_CATEGORY},
headers={"Authorization": f"Bearer {self.user_session.token}"},
content_type="application/json",
)
self.assertEqual(response.status_code, 400)

def test_price_create_with_app_name(self):
for params, result in [
("?", "API"),
Expand Down Expand Up @@ -581,7 +602,7 @@ def setUpTestData(cls):
PriceFactory(product_code=cls.product.code, price=25)
PriceFactory(product_code=cls.product.code, price=30)
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:apples",
price=2,
price_per=price_constants.PRICE_PER_KILOGRAM,
Expand Down
11 changes: 10 additions & 1 deletion open_prices/api/prices/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
)
from open_prices.api.utils import get_source_from_request
from open_prices.common.authentication import CustomAuthentication
from open_prices.prices import constants as price_constants
from open_prices.prices.models import Price


Expand Down Expand Up @@ -56,10 +57,18 @@ def create(self, request: Request, *args, **kwargs):
# validate
serializer = self.get_serializer(data=request.data)
serializer.is_valid(raise_exception=True)
# get type
type = serializer.validated_data.get("type") or (
price_constants.TYPE_PRODUCT
if serializer.validated_data.get("product_code")
else price_constants.TYPE_CATEGORY
)
# get source
source = get_source_from_request(self.request)
# save
price = serializer.save(owner=self.request.user.user_id, source=source)
price = serializer.save(
owner=self.request.user.user_id, type=type, source=source
)
# return full price
return Response(
self.serializer_class(price).data, status=status.HTTP_201_CREATED
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class Migration(migrations.Migration):
name="type",
field=models.CharField(
choices=[("OSM", "OSM"), ("ONLINE", "ONLINE")],
default="OSM",
default="OSM", # used only for the migration
max_length=20,
),
preserve_default=False,
Expand Down
2 changes: 1 addition & 1 deletion open_prices/locations/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ def setUpTestData(cls):
owner=cls.user_2.user_id,
)
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
location_osm_id=cls.location.osm_id,
location_osm_type=cls.location.osm_type,
Expand Down
14 changes: 10 additions & 4 deletions open_prices/prices/constants.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
"""For raw products (fruits, vegetables, etc.), the price is either
per unit or per kilogram. This enum is used to store this information.
"""
TYPE_PRODUCT = "PRODUCT" # product_code
TYPE_CATEGORY = "CATEGORY" # OFF category_tag (raw product)
TYPE_LIST = [TYPE_PRODUCT, TYPE_CATEGORY]
TYPE_CHOICES = [(key, key) for key in TYPE_LIST]


"""
PRICE_PER
For raw products (TYPE_CATEGORY) (fruits, vegetables, etc.),
the price is either per unit or per kilogram.
"""
PRICE_PER_UNIT = "UNIT"
PRICE_PER_KILOGRAM = "KILOGRAM"

PRICE_PER_LIST = [PRICE_PER_UNIT, PRICE_PER_KILOGRAM]
PRICE_PER_CHOICES = [(key, key) for key in PRICE_PER_LIST]
19 changes: 18 additions & 1 deletion open_prices/prices/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,31 @@

from open_prices.common import constants
from open_prices.locations import constants as location_constants
from open_prices.prices import constants as price_constants
from open_prices.prices.models import Price


class PriceFactory(DjangoModelFactory):
class Meta:
model = Price

product_code = factory.Faker("ean13")
class Params:
product_code_faker = factory.Faker("ean13")
category_tag_faker = "en:mandarines"

type = price_constants.TYPE_PRODUCT # random.choice(price_constants.TYPE_LIST)

product_code = factory.LazyAttribute(
lambda x: x.product_code_faker
if x.type == price_constants.TYPE_PRODUCT
else None
)
category_tag = factory.LazyAttribute(
lambda x: x.category_tag_faker
if x.type == price_constants.TYPE_CATEGORY
else None
)

price = factory.LazyAttribute(lambda x: random.randrange(0, 100))
# currency = factory.Faker("currency_symbol")
currency = factory.fuzzy.FuzzyChoice(constants.CURRENCY_LIST)
Expand Down
29 changes: 29 additions & 0 deletions open_prices/prices/migrations/0004_price_type.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Generated by Django 5.1 on 2024-11-26 09:48

from django.db import migrations, models


def set_price_type_category(apps, schema_editor):
Price = apps.get_model("prices", "Price")
# Price.objects.filter(product_code__isnull=False).update(type="PRODUCT")
Price.objects.filter(category_tag__isnull=False).update(type="CATEGORY")


class Migration(migrations.Migration):
dependencies = [
("prices", "0003_price_receipt_quantity"),
]

operations = [
migrations.AddField(
model_name="price",
name="type",
field=models.CharField(
choices=[("PRODUCT", "PRODUCT"), ("CATEGORY", "CATEGORY")],
default="PRODUCT", # used only for the migration
max_length=20,
),
preserve_default=False,
),
migrations.RunPython(set_price_type_category),
]
15 changes: 15 additions & 0 deletions open_prices/prices/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ class Price(models.Model):
"receipt_quantity",
]
CREATE_FIELDS = UPDATE_FIELDS + [
"type", # optional in the serializer
"product_code",
"product_name",
"category_tag",
Expand All @@ -90,6 +91,8 @@ class Price(models.Model):
"currency",
] # "owner"

type = models.CharField(max_length=20, choices=price_constants.TYPE_CHOICES)

product_code = models.CharField(blank=True, null=True)
product_name = models.CharField(blank=True, null=True)
category_tag = models.CharField(blank=True, null=True)
Expand Down Expand Up @@ -184,6 +187,12 @@ def clean(self, *args, **kwargs):
# - if product_code is set, then category_tag/labels_tags/origins_tags should not be set # noqa
# - if product_code is set, then price_per should not be set
if self.product_code:
if self.type != price_constants.TYPE_PRODUCT:
validation_errors = utils.add_validation_error(
validation_errors,
"type",
"Should be set to 'PRODUCT' if `product_code` is filled",
)
if not isinstance(self.product_code, str):
validation_errors = utils.add_validation_error(
validation_errors, "product_code", "Should be a string"
Expand Down Expand Up @@ -229,6 +238,12 @@ def clean(self, *args, **kwargs):
# - if labels_tags is set, then all labels_tags should be valid taxonomy strings # noqa
# - if origins_tags is set, then all origins_tags should be valid taxonomy strings # noqa
elif self.category_tag:
if self.type != price_constants.TYPE_CATEGORY:
validation_errors = utils.add_validation_error(
validation_errors,
"type",
"Should be set to 'CATEGORY' if `category_tag` is filled",
)
category_taxonomy = get_taxonomy("category")
# category_tag can be provided by the mobile app in any language,
# with language prefix (ex: `fr: Boissons`). We need to map it to
Expand Down
28 changes: 14 additions & 14 deletions open_prices/prices/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,14 +103,14 @@ def test_price_without_product_validation(self):
)
# product_code not set
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
price=3,
price_per=price_constants.PRICE_PER_KILOGRAM,
)
with self.assertRaises(ValidationError) as cm:
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="test",
price=3,
price_per=price_constants.PRICE_PER_KILOGRAM,
Expand All @@ -120,13 +120,13 @@ def test_price_without_product_validation(self):
"Invalid value: 'test', expected value to be in 'lang:tag' format",
)
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="fr: Grenoble", # valid (even if not in the taxonomy)
price=3,
price_per=price_constants.PRICE_PER_KILOGRAM,
)
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
labels_tags=["en:organic"],
price=3,
Expand All @@ -135,7 +135,7 @@ def test_price_without_product_validation(self):
self.assertRaises(
ValidationError,
PriceFactory,
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
labels_tags="en:organic", # should be a list
price=3,
Expand All @@ -144,7 +144,7 @@ def test_price_without_product_validation(self):
self.assertRaises(
ValidationError,
PriceFactory,
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
labels_tags=[
"en:organic",
Expand All @@ -154,7 +154,7 @@ def test_price_without_product_validation(self):
price_per=price_constants.PRICE_PER_KILOGRAM,
)
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
labels_tags=["en:organic"],
origins_tags=["en:france"],
Expand All @@ -164,7 +164,7 @@ def test_price_without_product_validation(self):
self.assertRaises(
ValidationError,
PriceFactory,
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
labels_tags=["en:organic"],
origins_tags="en:france", # should be a list
Expand All @@ -174,7 +174,7 @@ def test_price_without_product_validation(self):
self.assertRaises(
ValidationError,
PriceFactory,
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
labels_tags=["en:organic"],
origins_tags=["en:france", "test"], # not valid
Expand All @@ -193,7 +193,7 @@ def test_price_category_validation(self):
("fr: Soupe aux lentilles", "en:lentil-soups"),
]:
price = PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag=input_category,
price=3,
price_per=price_constants.PRICE_PER_KILOGRAM,
Expand All @@ -208,7 +208,7 @@ def test_price_origin_validation(self):
(["fr: Fairyland"], ["fr:fairyland"]),
]:
price = PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
origins_tags=input_origin_tags,
price=3,
Expand All @@ -223,23 +223,23 @@ def test_price_price_validation(self):
self.assertRaises(ValidationError, PriceFactory, price=PRICE_NOT_OK)
# price_per
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
price=3,
price_per=price_constants.PRICE_PER_KILOGRAM,
)
self.assertRaises(
ValidationError,
PriceFactory,
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
price=3,
price_per=None,
)
self.assertRaises(
ValidationError,
PriceFactory,
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
price=3,
price_per="test",
Expand Down
2 changes: 1 addition & 1 deletion open_prices/stats/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def setUpTestData(cls):
owner=cls.user.user_id,
)
PriceFactory(
product_code=None,
type=price_constants.TYPE_CATEGORY,
category_tag="en:tomatoes",
location_osm_id=cls.location.osm_id,
location_osm_type=cls.location.osm_type,
Expand Down

0 comments on commit f6d189b

Please sign in to comment.