Skip to content

Commit

Permalink
✨(backend) add start and end date on order groups model
Browse files Browse the repository at this point in the history
We want to be able to define a start or an end date
on the order group. Both dates can be setted as well.
The enabled property defines if the order group can
accept new orders when it is in relation with a
course product relation. With this work, the field
`is_active` has become a switch for admin user to
enabled or not the group. Then, the property
`is_enabled` computes the value depending on
the activation of the group and the time constraints.

Fix #1029
  • Loading branch information
jonathanreveille committed Feb 13, 2025
1 parent 1dcfb65 commit c867b40
Show file tree
Hide file tree
Showing 13 changed files with 929 additions and 38 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to

## [Unreleased]

### Added

- Add `start` and `end` datetime fields on order group model

## [2.16.0] - 2025-02-13

### Added
Expand Down
17 changes: 12 additions & 5 deletions src/backend/joanie/core/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,22 @@ class OrderGroupAdmin(admin.ModelAdmin):
list_display = (
"course_product_relation",
"is_active",
"is_enabled",
"nb_available_seats",
"start",
"end",
)
search_fields = ("course_product_relation",)
fields = ("course_product_relation", "is_active", "nb_seats", "nb_available_seats")
readonly_fields = ("nb_available_seats",)
readonly_update_fields = (
search_fields = ("course_product_relation", "start", "end")
fields = (
"course_product_relation",
"is_enabled",
"is_active",
"nb_seats",
"start",
"end",
)
readonly_fields = ("nb_available_seats", "is_enabled")
readonly_update_fields = ("course_product_relation", "nb_seats")

def get_readonly_fields(self, request, obj=None):
"""
Expand All @@ -223,7 +230,7 @@ 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()
return obj.available_seats


class CourseProductRelationInline(admin.StackedInline):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Generated by Django 4.2.18 on 2025-02-06 10:16

import django.core.validators
from django.db import migrations, models


def migrate_order_group_nb_seats_zero_to_none(apps, schema_editor):
"""
Update order groups where the seat number is 0 to None.
"""
OrderGroup = apps.get_model("core", "OrderGroup")
OrderGroup.objects.filter(nb_seats=0).update(nb_seats=None)


def rollback_order_group_nb_seats_none_to_zero(apps, schema_editor):
"""
Restore order group where number of seats is None to 0.
"""
OrderGroup = apps.get_model("core", "OrderGroup")
OrderGroup.objects.filter(nb_seats=None).update(nb_seats=0)


class Migration(migrations.Migration):

dependencies = [
('core', '0055_alter_documentimage_options_alter_ordergroup_options'),
]

operations = [
migrations.AddField(
model_name='ordergroup',
name='end',
field=models.DateTimeField(blank=True, help_text='Date at which the order group activation ends', null=True, verbose_name='order group end datetime'),
),
migrations.AddField(
model_name='ordergroup',
name='start',
field=models.DateTimeField(blank=True, help_text='Date at which the order group activation begins', null=True, verbose_name='order group start datetime'),
),
migrations.AlterField(
model_name='ordergroup',
name='nb_seats',
field=models.PositiveSmallIntegerField(blank=True, default=None, help_text='The maximum number of orders that can be validated for a given order group', null=True, verbose_name='Number of seats'),
),
migrations.AddConstraint(
model_name='ordergroup',
constraint=models.CheckConstraint(check=models.Q(('start__lte', models.F('end'))), name='check_start_before_end', violation_error_message='Start date cannot be greater than end date'),
),
migrations.RunPython(
migrate_order_group_nb_seats_zero_to_none,
rollback_order_group_nb_seats_none_to_zero,
),
]
74 changes: 65 additions & 9 deletions src/backend/joanie/core/models/products.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,11 +366,13 @@ class OrderGroup(BaseModel):
"""Order group to enforce a maximum number of seats for a product."""

nb_seats = models.PositiveSmallIntegerField(
default=0,
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,
)
course_product_relation = models.ForeignKey(
to=CourseProductRelation,
Expand All @@ -379,6 +381,27 @@ class OrderGroup(BaseModel):
on_delete=models.CASCADE,
)
is_active = models.BooleanField(_("is active"), default=True)
start = models.DateTimeField(
help_text=_("Date at which the order group activation begins"),
verbose_name=_("order group start datetime"),
blank=True,
null=True,
)
end = models.DateTimeField(
help_text=_("Date at which the order group activation ends"),
verbose_name=_("order group end datetime"),
blank=True,
null=True,
)

class Meta:
constraints = [
models.CheckConstraint(
check=models.Q(start__lte=models.F("end")),
name="check_start_before_end",
violation_error_message=_("Start date cannot be greater than end date"),
),
]

def get_nb_binding_orders(self):
"""Query the number of binding orders related to this order group."""
Expand All @@ -397,6 +420,28 @@ def can_edit(self):
"""Return True if the order group can be edited."""
return not self.orders.exists()

@property
def available_seats(self) -> int | None:
"""Return the number of available seats on the order group."""
if self.nb_seats is None:
return None
return self.nb_seats - self.get_nb_binding_orders()

@property
def is_enabled(self):
"""
Returns boolean whether the order group is enabled based on its activation status
and time constraints.
"""
if not self.is_active:
return False

now = timezone.now()
start = self.start or now
end = self.end or now

return start <= now <= end


class OrderManager(models.Manager):
"""Custom manager for the Order model."""
Expand Down Expand Up @@ -796,20 +841,31 @@ def clean(self):
f"and the course {course_title}."
)

if (
course_product_relation
and course_product_relation.order_groups.filter(is_active=True).exists()
if course_product_relation and any(
order_group.is_enabled
for order_group in course_product_relation.order_groups.all()
):
if not self.order_group_id or not self.order_group.is_active:
if not self.order_group_id or not self.order_group.is_enabled:
error_dict["order_group"].append(
f"An active order group is required for product {product_title:s}."
f"An enabled order group is required for product {product_title:s}."
)
else:
nb_seats = self.order_group.nb_seats
if 0 < nb_seats <= self.order_group.get_nb_binding_orders():
elif (
self.order_group.is_enabled
and self.order_group.nb_seats is not None
):
if self.order_group.available_seats == 0:
error_dict["order_group"].append(
f"Maximum number of orders reached for product {product_title:s}"
)
elif (
course_product_relation
and self.order_group
and not self.order_group.is_enabled
):
error_dict["order_group"].append(
f"This order group is not enabled for product {product_title:s} "
"and does not accept any orders at the moment."
)

if error_dict:
raise ValidationError(error_dict)
Expand Down
10 changes: 7 additions & 3 deletions 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 All @@ -466,12 +467,15 @@ class Meta:
"nb_available_seats",
"created_on",
"can_edit",
"is_enabled",
"start",
"end",
]
read_only_fields = ["id", "can_edit", "created_on"]
read_only_fields = ["id", "can_edit", "created_on", "is_enabled"]

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


@extend_schema_serializer(exclude_fields=("course_product_relation",))
Expand Down
14 changes: 11 additions & 3 deletions src/backend/joanie/core/serializers/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -763,12 +763,20 @@ class OrderGroupSerializer(serializers.ModelSerializer):

class Meta:
model = models.OrderGroup
fields = ["id", "is_active", "nb_seats", "nb_available_seats"]
fields = [
"id",
"is_active",
"nb_seats",
"nb_available_seats",
"is_enabled",
"start",
"end",
]
read_only_fields = fields

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


class DefinitionResourcesProductSerializer(serializers.ModelSerializer):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,13 @@ def test_api_admin_orders_course_retrieve(self):
"id": str(order_group.id),
"nb_seats": order_group.nb_seats,
"is_active": order_group.is_active,
"is_enabled": order_group.is_enabled,
"nb_available_seats": order_group.nb_seats
- order_group.get_nb_binding_orders(),
"created_on": format_date(order_group.created_on),
"can_edit": order_group.can_edit,
"start": None,
"end": None,
},
"total": float(order.total),
"total_currency": settings.DEFAULT_CURRENCY,
Expand Down
Loading

0 comments on commit c867b40

Please sign in to comment.