Skip to content

Commit

Permalink
fixup! ✨(backend) add start and end date on order groups model
Browse files Browse the repository at this point in the history
  • Loading branch information
jonathanreveille committed Feb 5, 2025
1 parent 621949b commit 356a85c
Show file tree
Hide file tree
Showing 10 changed files with 257 additions and 21 deletions.
5 changes: 4 additions & 1 deletion src/backend/joanie/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@ class OrderGroupAdmin(admin.ModelAdmin):
fields = (
"course_product_relation",
"is_enabled",
"is_active",
"nb_seats",
"start",
"end",
Expand All @@ -229,7 +230,9 @@ def get_readonly_fields(self, request, obj=None):

def nb_available_seats(self, obj): # pylint: disable=no-self-use
"""Return the number of available seats for this order group."""
return obj.nb_seats - obj.get_nb_binding_orders()
if obj.nb_seats is not None:
return obj.nb_seats - obj.get_nb_binding_orders()
return obj.nb_seats


class CourseProductRelationInline(admin.StackedInline):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ class Migration(migrations.Migration):
]

operations = [
migrations.AlterField(
model_name='ordergroup',
name='nb_seats',
field=models.PositiveSmallIntegerField(default=0, help_text='The maximum number of orders that can be validated for a given order group', null=True, verbose_name='Number of seats'),
),
migrations.AddField(
model_name='ordergroup',
name='end',
Expand Down
32 changes: 26 additions & 6 deletions src/backend/joanie/core/models/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,12 +365,15 @@ def delete(self, using=None, keep_parents=False):
class OrderGroup(BaseModel):
"""Order group to enforce a maximum number of seats for a product."""

nb_seats = models.PositiveSmallIntegerField(
default=0,
nb_seats = models.IntegerField(
default=None,
verbose_name=_("Number of seats"),
help_text=_(
"The maximum number of orders that can be validated for a given order group"
),
null=True,
blank=True,
validators=[MinValueValidator(0)],
)
course_product_relation = models.ForeignKey(
to=CourseProductRelation,
Expand Down Expand Up @@ -432,6 +435,10 @@ def is_enabled(self):
if not self.is_active:
return False

# Unlimited spots
if self.is_active and self.nb_seats is None:
return True

available_seats = self.nb_seats - self.get_nb_binding_orders()
if not available_seats:
return False
Expand Down Expand Up @@ -867,10 +874,23 @@ def clean(self):
)
else:
nb_seats = self.order_group.nb_seats
if 0 < nb_seats <= self.order_group.get_nb_binding_orders():
error_dict["order_group"].append(
f"Maximum number of orders reached for product {product_title:s}"
)
# When nb_seats is None, the order group has unlimited available seats
if nb_seats is not None:
if 0 < nb_seats <= self.order_group.get_nb_binding_orders():
error_dict["order_group"].append(
f"Maximum number of orders reached for product {product_title:s}"
)
# For order group that does not have a fix nb of seats and is not active
# We should block the order creation for that group
if (
course_product_relation
and self.order_group
and not self.order_group.is_active
and self.order_group.nb_seats is None
):
error_dict["order_group"].append(
f"This order group does not accept anymore orders for {product_title:s}"
)

if error_dict:
raise ValidationError(error_dict)
Expand Down
5 changes: 4 additions & 1 deletion src/backend/joanie/core/serializers/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ class AdminOrderGroupSerializer(serializers.ModelSerializer):

nb_seats = serializers.IntegerField(
required=False,
allow_null=True,
label=models.OrderGroup._meta.get_field("nb_seats").verbose_name,
help_text=models.OrderGroup._meta.get_field("nb_seats").help_text,
default=models.OrderGroup._meta.get_field("nb_seats").default,
Expand Down Expand Up @@ -474,7 +475,9 @@ class Meta:

def get_nb_available_seats(self, order_group) -> int:
"""Return the number of available seats for this order group."""
return order_group.nb_seats - order_group.get_nb_binding_orders()
if order_group.nb_seats is not None:
return order_group.nb_seats - order_group.get_nb_binding_orders()
return order_group.nb_seats


@extend_schema_serializer(exclude_fields=("course_product_relation",))
Expand Down
4 changes: 3 additions & 1 deletion src/backend/joanie/core/serializers/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -776,7 +776,9 @@ class Meta:

def get_nb_available_seats(self, order_group) -> int:
"""Return the number of available seats for this order group."""
return order_group.nb_seats - order_group.get_nb_binding_orders()
if order_group.nb_seats is not None:
return order_group.nb_seats - order_group.get_nb_binding_orders()
return order_group.nb_seats


class DefinitionResourcesProductSerializer(serializers.ModelSerializer):
Expand Down
84 changes: 84 additions & 0 deletions src/backend/joanie/tests/core/api/order/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -1488,6 +1488,90 @@ def test_api_order_create_authenticated_no_seats(self):
)
self.assertEqual(response.status_code, HTTPStatus.CREATED)

def test_api_order_create_authenticated_nb_seat_is_none_on_active_order_group(self):
"""
If nb_seats is set to None on an active order group, there should be no limit
to the number of orders
"""
user = factories.UserFactory()
token = self.generate_token_from_user(user)
relation = factories.CourseProductRelationFactory(
organizations=factories.OrganizationFactory.create_batch(2),
)
order_group = factories.OrderGroupFactory(
course_product_relation=relation, nb_seats=None, is_active=True
)
factories.OrderFactory.create_batch(
10,
product=relation.product,
course=relation.course,
order_group=order_group,
)
data = {
"course_code": relation.course.code,
"organization_id": str(relation.organizations.first().id),
"order_group_id": str(order_group.id),
"product_id": str(relation.product.id),
"billing_address": BillingAddressDictFactory(),
"has_waived_withdrawal_right": True,
}

response = self.client.post(
"/api/v1.0/orders/",
data=data,
content_type="application/json",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, HTTPStatus.CREATED)
self.assertEqual(
models.Order.objects.filter(
product=relation.product, course=relation.course
).count(),
11,
)

def test_api_order_create_authenticated_nb_seat_is_none_on_not_active_order_group(
self,
):
"""
If nb_seats is set to None and the order group is not active, it should not be possible
to create an order.
"""
user = factories.UserFactory()
token = self.generate_token_from_user(user)
relation = factories.CourseProductRelationFactory(
organizations=factories.OrganizationFactory.create_batch(2),
)
order_group = factories.OrderGroupFactory(
course_product_relation=relation, nb_seats=None, is_active=False
)
data = {
"course_code": relation.course.code,
"organization_id": str(relation.organizations.first().id),
"order_group_id": str(order_group.id),
"product_id": str(relation.product.id),
"billing_address": BillingAddressDictFactory(),
"has_waived_withdrawal_right": True,
}

response = self.client.post(
"/api/v1.0/orders/",
data=data,
content_type="application/json",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, HTTPStatus.BAD_REQUEST)
self.assertEqual(
response.json(),
{
"order_group": [
f"This order group does not accept anymore orders for {relation.product.title}"
]
},
)

def test_api_order_create_authenticated_free_product_no_billing_address(self):
"""
Create an order on a free product without billing address
Expand Down
60 changes: 59 additions & 1 deletion src/backend/joanie/tests/core/test_api_admin_order_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,10 +137,41 @@ def test_admin_api_order_group_retrieve_authenticated(self):
}
self.assertEqual(content, expected_return)

def test_admin_api_order_group_retrieve_authenticated_with_nb_seats_is_none(self):
"""
When the order was setted with None for the number of seats, the
authenticated users should be able to request order groups detail and
find `None` for the value `nb_available_seats` and `nb_seats` fields.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
relation = factories.CourseProductRelationFactory()
order_group = factories.OrderGroupFactory(
course_product_relation=relation,
nb_seats=None,
is_active=False,
)
expected_return = {
"nb_seats": None,
"nb_available_seats": None,
}

response = self.client.get(
f"{self.base_url}/{relation.id}/order-groups/{order_group.id}/"
)

content = response.json()

self.assertEqual(response.status_code, HTTPStatus.OK)
self.assertEqual(content["nb_seats"], expected_return["nb_seats"])
self.assertEqual(
content["nb_available_seats"], expected_return["nb_available_seats"]
)

# create
def test_admin_api_order_group_create_anonymous(self):
"""
Anonymous users should not be able to create an order groups.
Anonymous users should not be able to create an order group.
"""
relation = factories.CourseProductRelationFactory()

Expand All @@ -156,6 +187,33 @@ def test_admin_api_order_group_create_anonymous(self):
content["detail"], "Authentication credentials were not provided."
)

def test_admin_api_order_group_create_authenticated_with_nb_seats_is_none(self):
"""
Authenticated users should be able to create an order group and set None for
`nb_seats`.
"""
admin = factories.UserFactory(is_staff=True, is_superuser=True)
self.client.login(username=admin.username, password="password")
relation = factories.CourseProductRelationFactory()
data = {
"nb_seats": None,
"is_active": True,
}

response = self.client.post(
f"{self.base_url}/{relation.id}/order-groups/",
content_type="application/json",
data=data,
)

content = response.json()

self.assertEqual(response.status_code, HTTPStatus.CREATED)
self.assertEqual(content["nb_seats"], None)
self.assertEqual(content["is_active"], data["is_active"])
self.assertEqual(content["is_enabled"], True)
self.assertEqual(models.OrderGroup.objects.filter(**data).count(), 1)

def test_admin_api_order_group_create_authenticated(self):
"""
Authenticated users should be able to request order groups list.
Expand Down
62 changes: 61 additions & 1 deletion src/backend/joanie/tests/core/test_models_order_group.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@

from datetime import timedelta

from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.test import TestCase
from django.utils import timezone

from joanie.core import enums, factories
from joanie.core import enums, factories, models


class OrderGroupModelTestCase(TestCase):
Expand Down Expand Up @@ -276,3 +277,62 @@ def test_models_order_group_is_enabled_certificate_product_last_minute(
order_group.save()

self.assertTrue(order_group.is_enabled)

def test_models_order_group_nb_of_seats_negative_number(self):
"""
It should not be possible to insert a negative value for the number of seats
on the order group.
"""
with self.assertRaises(IntegrityError):
factories.OrderGroupFactory(nb_seats=-1)

def test_models_order_group_nb_seats_is_null_and_is_not_active(self):
"""
When the order group is active and nb seats is null, orders can be created
on that group. As soon as the order group is not active, it should not let
any new order get into that group.
"""
relation = factories.CourseProductRelationFactory()
order_group = factories.OrderGroupFactory(
course_product_relation=relation, nb_seats=None, is_active=True
)
factories.OrderFactory.create_batch(
2,
course=relation.course,
product=relation.product,
order_group=order_group,
state=enums.ORDER_STATE_PENDING,
)

order_group.is_active = False
order_group.save()
order_group.refresh_from_db()

with self.assertRaises(ValidationError) as context:
factories.OrderFactory(
course=relation.course,
product=relation.product,
order_group=order_group,
state=enums.ORDER_STATE_PENDING,
)

self.assertTrue(
"This order group does not accept anymore orders" in str(context.exception)
)
self.assertFalse(order_group.is_enabled)
self.assertEqual(models.Order.objects.count(), 2)

order_group.is_active = True
order_group.save()
order_group.refresh_from_db()

# This order should be created now
factories.OrderFactory(
course=relation.course,
product=relation.product,
order_group=order_group,
state=enums.ORDER_STATE_PENDING,
)

self.assertEqual(models.Order.objects.count(), 3)
self.assertTrue(order_group.is_enabled)
Loading

0 comments on commit 356a85c

Please sign in to comment.