Skip to content

Commit 32f07b4

Browse files
Merge pull request #8 from mohamad-liyaghi/products
Products
2 parents c21ed18 + a94795f commit 32f07b4

25 files changed

+751
-1
lines changed

apps/products/__init__.py

Whitespace-only changes.

apps/products/admin.py

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
from django.contrib import admin
2+
from .models import Product
3+
4+
5+
@admin.register(Product)
6+
class ProductAdmin(admin.ModelAdmin):
7+
list_display = [
8+
"name",
9+
"restaurant",
10+
"price",
11+
"quantity",
12+
"max_quantity_per_order",
13+
"type",
14+
]
15+
list_filter = ["type"]
16+
search_fields = ["name", "restaurant__name"]
17+
ordering = ["type", "price"]
18+
readonly_fields = ["uuid"]
19+
fieldsets = [
20+
(
21+
None,
22+
{
23+
"fields": [
24+
"uuid",
25+
"restaurant",
26+
"name",
27+
"description",
28+
"price",
29+
"quantity",
30+
"max_quantity_per_order",
31+
"type",
32+
]
33+
},
34+
)
35+
]
36+
raw_id_fields = ["restaurant"]
37+
38+
def get_queryset(self, request):
39+
return super().get_queryset(request).select_related("restaurant")

apps/products/apps.py

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from django.apps import AppConfig
2+
3+
4+
class ProductsConfig(AppConfig):
5+
default_auto_field = "django.db.models.BigAutoField"
6+
name = "products"

apps/products/enums.py

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
from django.db import models
2+
3+
4+
class ProductType(models.TextChoices):
5+
FOOD = "f", "Food"
6+
DRINK = "d", "Drink"
7+
SALAD = "s", "Salad"
8+
OTHER = "o", "Other"
+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
# Generated by Django 5.0.7 on 2024-08-05 09:21
2+
3+
import django.db.models.deletion
4+
import uuid
5+
from django.db import migrations, models
6+
7+
8+
class Migration(migrations.Migration):
9+
initial = True
10+
11+
dependencies = [
12+
("restaurants", "0003_restaurant_is_soft_deleted"),
13+
]
14+
15+
operations = [
16+
migrations.CreateModel(
17+
name="Product",
18+
fields=[
19+
(
20+
"id",
21+
models.BigAutoField(
22+
auto_created=True,
23+
primary_key=True,
24+
serialize=False,
25+
verbose_name="ID",
26+
),
27+
),
28+
(
29+
"uuid",
30+
models.UUIDField(default=uuid.uuid4, editable=False, unique=True),
31+
),
32+
("name", models.CharField(max_length=255)),
33+
("description", models.TextField()),
34+
("price", models.DecimalField(decimal_places=2, max_digits=10)),
35+
("quantity", models.PositiveIntegerField()),
36+
("max_quantity_per_order", models.PositiveIntegerField()),
37+
(
38+
"type",
39+
models.CharField(
40+
choices=[
41+
("f", "Food"),
42+
("d", "Drink"),
43+
("s", "Salad"),
44+
("o", "Other"),
45+
],
46+
max_length=1,
47+
),
48+
),
49+
(
50+
"restaurant",
51+
models.ForeignKey(
52+
on_delete=django.db.models.deletion.CASCADE,
53+
related_name="products",
54+
to="restaurants.restaurant",
55+
),
56+
),
57+
],
58+
),
59+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.0.7 on 2024-08-05 09:58
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("products", "0001_initial"),
9+
]
10+
11+
operations = [
12+
migrations.AlterField(
13+
model_name="product",
14+
name="description",
15+
field=models.TextField(blank=True, null=True),
16+
),
17+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# Generated by Django 5.0.7 on 2024-08-05 10:17
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
dependencies = [
8+
("products", "0002_alter_product_description"),
9+
]
10+
11+
operations = [
12+
migrations.AddField(
13+
model_name="product",
14+
name="is_deleted",
15+
field=models.BooleanField(default=False),
16+
),
17+
]

apps/products/migrations/__init__.py

Whitespace-only changes.

apps/products/models.py

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from django.db import models
2+
from django.core.exceptions import ValidationError
3+
from uuid import uuid4
4+
from products.enums import ProductType
5+
from restaurants.models import Restaurant
6+
from restaurants.enums import RestaurantStatus
7+
8+
9+
class Product(models.Model):
10+
restaurant = models.ForeignKey(Restaurant, on_delete=models.CASCADE, related_name="products")
11+
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)
12+
name = models.CharField(max_length=255)
13+
description = models.TextField(blank=True, null=True)
14+
price = models.DecimalField(max_digits=10, decimal_places=2)
15+
quantity = models.PositiveIntegerField()
16+
max_quantity_per_order = models.PositiveIntegerField()
17+
type = models.CharField(max_length=1, choices=ProductType.choices)
18+
is_deleted = models.BooleanField(default=False)
19+
20+
@property
21+
def is_available(self):
22+
return self.quantity > 0
23+
24+
def __str__(self):
25+
return self.name
26+
27+
# restaurant should be approved
28+
def clean(self):
29+
if self.restaurant.status != RestaurantStatus.APPROVED:
30+
raise ValidationError("Restaurant is not approved")
31+
32+
def save(self, *args, **kwargs):
33+
self.full_clean()
34+
super().save(*args, **kwargs)

apps/products/serializers.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from rest_framework import serializers
2+
from products.models import Product
3+
from restaurants.serializers import RestaurantSerializer
4+
5+
6+
class ProductSerializer(serializers.ModelSerializer):
7+
restaurant = RestaurantSerializer(read_only=True)
8+
9+
class Meta:
10+
model = Product
11+
fields = (
12+
"uuid",
13+
"name",
14+
"restaurant",
15+
"price",
16+
"quantity",
17+
"max_quantity_per_order",
18+
"type",
19+
"is_available",
20+
)
21+
read_only_fields = ("uuid", "is_available")
22+
23+
def create(self, validated_data) -> Product:
24+
validated_data.setdefault("restaurant", self.context["restaurant"])
25+
return super().create(validated_data)

apps/products/tests/__init__.py

Whitespace-only changes.

apps/products/tests/fixtures.py

+129
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import pytest
2+
from products.models import Product
3+
from products.enums import ProductType
4+
5+
6+
@pytest.fixture(scope="session")
7+
def available_food_product(django_db_setup, django_db_blocker, approved_restaurant) -> Product:
8+
with django_db_blocker.unblock():
9+
yield Product.objects.create(
10+
restaurant=approved_restaurant,
11+
name="Available Food Product",
12+
description="Available Food Product Description",
13+
price=10.00,
14+
quantity=10,
15+
max_quantity_per_order=5,
16+
type=ProductType.FOOD,
17+
)
18+
19+
20+
@pytest.fixture(scope="session")
21+
def unavailable_food_product(django_db_setup, django_db_blocker, approved_restaurant) -> Product:
22+
with django_db_blocker.unblock():
23+
yield Product.objects.create(
24+
restaurant=approved_restaurant,
25+
name="Unavailable Food Product",
26+
description="Unavailable Food Product Description",
27+
price=10.00,
28+
quantity=0,
29+
max_quantity_per_order=5,
30+
type=ProductType.FOOD,
31+
)
32+
33+
34+
@pytest.fixture(scope="session")
35+
def available_drink_product(django_db_setup, django_db_blocker, approved_restaurant) -> Product:
36+
with django_db_blocker.unblock():
37+
yield Product.objects.create(
38+
restaurant=approved_restaurant,
39+
name="Available Drink Product",
40+
description="Available Drink Product Description",
41+
price=5.00,
42+
quantity=10,
43+
max_quantity_per_order=5,
44+
type=ProductType.DRINK,
45+
)
46+
47+
48+
@pytest.fixture(scope="session")
49+
def unavailable_drink_product(django_db_setup, django_db_blocker, approved_restaurant) -> Product:
50+
with django_db_blocker.unblock():
51+
yield Product.objects.create(
52+
restaurant=approved_restaurant,
53+
name="Unavailable Drink Product",
54+
description="Unavailable Drink Product Description",
55+
price=5.00,
56+
quantity=0,
57+
max_quantity_per_order=5,
58+
type=ProductType.DRINK,
59+
)
60+
61+
62+
@pytest.fixture(scope="session")
63+
def available_salad_product(django_db_setup, django_db_blocker, approved_restaurant) -> Product:
64+
with django_db_blocker.unblock():
65+
yield Product.objects.create(
66+
restaurant=approved_restaurant,
67+
name="Available Salad Product",
68+
description="Available Salad Product Description",
69+
price=7.50,
70+
quantity=10,
71+
max_quantity_per_order=5,
72+
type=ProductType.SALAD,
73+
)
74+
75+
76+
@pytest.fixture(scope="session")
77+
def unavailable_salad_product(django_db_setup, django_db_blocker, approved_restaurant) -> Product:
78+
with django_db_blocker.unblock():
79+
yield Product.objects.create(
80+
restaurant=approved_restaurant,
81+
name="Unavailable Salad Product",
82+
description="Unavailable Salad Product Description",
83+
price=7.50,
84+
quantity=0,
85+
max_quantity_per_order=5,
86+
type=ProductType.SALAD,
87+
)
88+
89+
90+
@pytest.fixture(scope="session")
91+
def available_other_product(django_db_setup, django_db_blocker, approved_restaurant) -> Product:
92+
with django_db_blocker.unblock():
93+
yield Product.objects.create(
94+
restaurant=approved_restaurant,
95+
name="Available Coffee Product",
96+
description="Available Coffee Product Description",
97+
price=3.50,
98+
quantity=10,
99+
max_quantity_per_order=5,
100+
type=ProductType.OTHER,
101+
)
102+
103+
104+
@pytest.fixture(scope="session")
105+
def unavailable_other_product(django_db_setup, django_db_blocker, approved_restaurant) -> Product:
106+
with django_db_blocker.unblock():
107+
yield Product.objects.create(
108+
restaurant=approved_restaurant,
109+
name="Unavailable Coffee Product",
110+
description="Unavailable Coffee Product Description",
111+
price=3.50,
112+
quantity=0,
113+
max_quantity_per_order=5,
114+
type=ProductType.OTHER,
115+
)
116+
117+
118+
@pytest.fixture(scope="session")
119+
def available_food_to_delete(django_db_setup, django_db_blocker, approved_restaurant) -> Product:
120+
with django_db_blocker.unblock():
121+
yield Product.objects.create(
122+
restaurant=approved_restaurant,
123+
name="Available Food Product",
124+
description="Available Food Product Description",
125+
price=10.00,
126+
quantity=10,
127+
max_quantity_per_order=5,
128+
type=ProductType.FOOD,
129+
)

apps/products/tests/test_models.py

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import pytest
2+
from django.core.exceptions import ValidationError
3+
from products.models import Product
4+
from products.enums import ProductType
5+
6+
7+
@pytest.mark.django_db
8+
class TestProductModel:
9+
def test_product_is_available(self, available_food_product, unavailable_food_product):
10+
assert available_food_product.is_available
11+
assert not unavailable_food_product.is_available
12+
13+
def test_product_clean_method(self, denied_restaurant):
14+
product = Product(
15+
restaurant=denied_restaurant,
16+
name="Test Product",
17+
description="Test Description",
18+
price=10.0,
19+
quantity=10,
20+
max_quantity_per_order=5,
21+
type=ProductType.FOOD,
22+
)
23+
with pytest.raises(ValidationError):
24+
product.save()

apps/products/tests/test_views/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)