Skip to content

Commit b35780e

Browse files
✨(backend) add agreement batch order viewset and serializer
We want authenticated organization user who have access to be able to list, retrieve information on the contracts related to batch orders. This will allow certain user who have the rights to retrieve the list of contracts and if they are eligible to sign them. We needed to create a new viewset and serializer because the existing viewset for organization nested contract viewset was built around the relation `contract.order`, it did not correspond to the relation `batch_order.contract`. This is the reason why we created a new viewset for that matter.
1 parent 99d0ddd commit b35780e

File tree

7 files changed

+1270
-0
lines changed

7 files changed

+1270
-0
lines changed

src/backend/joanie/client_urls.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@
103103
api_client.NestedOrganizationContractViewSet,
104104
basename="organization_contracts",
105105
)
106+
organization_related_router.register(
107+
"agreements",
108+
api_client.NestedOrganizationAgreementViewSet,
109+
basename="organization_agreements",
110+
)
106111
organization_related_router.register(
107112
"offerings",
108113
api_client.OfferingViewSet,

src/backend/joanie/core/api/client/__init__.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1644,6 +1644,96 @@ def get_me(self, request):
16441644
return Response(self.serializer_class(request.user, context=context).data)
16451645

16461646

1647+
class GenericAgreementViewSet(
1648+
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
1649+
):
1650+
"""
1651+
Agreement ViewSet that returns information on Contracts related to Batch Orders.
1652+
Only authenticated users that have access to organization can get information.
1653+
1654+
GET /.*/agreements/
1655+
GET /.*/agreements/<uuid:contract_id>
1656+
"""
1657+
1658+
lookup_field = "pk"
1659+
permission_classes = [permissions.IsAuthenticated]
1660+
serializer_class = serializers.AgreementBatchOrderSerializer
1661+
filterset_class = filters.AgreementFilter
1662+
ordering = ["-organization_signed_on", "-created_on"]
1663+
queryset = (
1664+
models.Contract.objects.exclude(
1665+
batch_order__state=enums.BATCH_ORDER_STATE_CANCELED
1666+
)
1667+
.select_related(
1668+
"definition",
1669+
"organization_signatory",
1670+
)
1671+
.prefetch_related(
1672+
"batch_order__organization",
1673+
"batch_order__offering__course",
1674+
"batch_order__owner",
1675+
"batch_order__offering__product",
1676+
)
1677+
)
1678+
1679+
1680+
class NestedOrganizationAgreementViewSet(NestedGenericViewSet, GenericAgreementViewSet):
1681+
"""
1682+
Nested Organization and Agreements (contracts) related to batch orders inside organization
1683+
route.
1684+
1685+
It allows to list & retrieve organization's agreements (contracts) if the user is
1686+
an administrator or an owner of the organization.
1687+
1688+
GET /api/organizations/<organization_id|organization_code>/agreements/
1689+
Return list of all organization's contracts
1690+
1691+
GET /api/organizations/<organization_id|organization_code>/agreements/<contract_id>/
1692+
Return an organization's contract if one matches the provided id
1693+
1694+
You can use query params to filter by signature state when retrieving the list, or by
1695+
offering id.
1696+
"""
1697+
1698+
lookup_fields = ["batch_order__organization__pk", "pk"]
1699+
lookup_url_kwargs = ["organization_id", "pk"]
1700+
1701+
def _lookup_by_organization_code_or_pk(self):
1702+
"""
1703+
Override `lookup_fields` to lookup by organization code or pk according to
1704+
the `organization_id` kwarg is a valid UUID or not.
1705+
"""
1706+
try:
1707+
uuid.UUID(self.kwargs["organization_id"])
1708+
except ValueError:
1709+
self.lookup_fields[0] = "batch_order__organization__code__iexact"
1710+
1711+
def initial(self, request, *args, **kwargs):
1712+
"""
1713+
Runs anything that needs to occur prior to calling method handler.
1714+
"""
1715+
super().initial(request, *args, **kwargs)
1716+
self._lookup_by_organization_code_or_pk()
1717+
1718+
def get_queryset(self):
1719+
"""
1720+
Customize the queryset to get only user's agreements.
1721+
"""
1722+
queryset = super().get_queryset()
1723+
1724+
username = (
1725+
self.request.auth["username"]
1726+
if self.request.auth
1727+
else self.request.user.username
1728+
)
1729+
1730+
additional_filter = {
1731+
"batch_order__organization__accesses__user__username": username,
1732+
}
1733+
1734+
return queryset.filter(**additional_filter)
1735+
1736+
16471737
class GenericContractViewSet(
16481738
mixins.ListModelMixin, mixins.RetrieveModelMixin, viewsets.GenericViewSet
16491739
):

src/backend/joanie/core/filters/client/__init__.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,60 @@ def filter_offering_id(self, queryset, _name, value):
173173
)
174174

175175

176+
class AgreementFilter(filters.FilterSet):
177+
"""
178+
AgreementFilter allows to filter this resource with signature state, organization id,
179+
or offering id.
180+
"""
181+
182+
id = filters.AllValuesMultipleFilter()
183+
signature_state = filters.ChoiceFilter(
184+
method="filter_signature_state",
185+
choices=enums.CONTRACT_SIGNATURE_STATE_FILTER_CHOICES,
186+
)
187+
organization_id = filters.UUIDFilter(field_name="batch_order__organization__id")
188+
offering_id = filters.UUIDFilter(method="filter_offering_id")
189+
190+
class Meta:
191+
model = models.Contract
192+
fields: List[str] = ["id", "signature_state", "offering_id"]
193+
194+
def filter_signature_state(self, queryset, _name, value):
195+
"""
196+
Filter Contracts by signature state
197+
"""
198+
is_unsigned = value == enums.CONTRACT_SIGNATURE_STATE_UNSIGNED
199+
is_half_signed = value == enums.CONTRACT_SIGNATURE_STATE_HALF_SIGNED
200+
201+
return queryset.filter(
202+
student_signed_on__isnull=is_unsigned,
203+
organization_signed_on__isnull=is_unsigned | is_half_signed,
204+
)
205+
206+
def filter_offering_id(self, queryset, _name, value):
207+
"""
208+
Try to retrieve an offering from the given id and filter contracts accordingly.
209+
"""
210+
url_kwargs = self.request.parser_context.get("kwargs", {})
211+
queryset_filters = {"id": value}
212+
213+
if organization_id := url_kwargs.get("organization_id"):
214+
queryset_filters["organizations__in"] = [organization_id]
215+
216+
try:
217+
offering = models.CourseProductRelation.objects.get(**queryset_filters)
218+
except models.CourseProductRelation.DoesNotExist:
219+
return queryset.none()
220+
221+
return queryset.filter(
222+
batch_order__relation__course_id=offering.course_id,
223+
batch_order__relation__product_id=offering.product_id,
224+
batch_order__organization__in=offering.organizations.only("pk").values_list(
225+
"pk", flat=True
226+
),
227+
)
228+
229+
176230
class NestedOrderCourseViewSetFilter(filters.FilterSet):
177231
"""
178232
OrderCourseFilter that allows to filter this resource with a product's 'id', an

src/backend/joanie/core/serializers/client.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,20 @@ def get_total_currency(self, *args, **kwargs) -> str:
546546
return settings.DEFAULT_CURRENCY
547547

548548

549+
class AgreementBatchOrderSerializer(AbilitiesModelSerializer):
550+
"""Small serializer for Contracts models related to Batch Orders (agreements)"""
551+
552+
batch_order = BatchOrderLightSerializer(read_only=True)
553+
554+
class Meta:
555+
model = models.Contract
556+
fields = [
557+
"id",
558+
"batch_order",
559+
]
560+
read_only_fields = fields
561+
562+
549563
class QuoteDefinitionSerializer(serializers.ModelSerializer):
550564
"""Read only serializer for QuoteDefinition model."""
551565

0 commit comments

Comments
 (0)