Skip to content

Commit

Permalink
✨(backend) allow to filter contracts by their signature state
Browse files Browse the repository at this point in the history
We want to able to retrieve only signed or unsigned contracts. So we add a drf
filter which returned contract according its property `signed_on` value.

Fix #393

Co-authored-by: Samy Ait-Ouakli <samy.aitouakli@gmail.com>
  • Loading branch information
jbpenrath and samy-aitouakli committed Oct 19, 2023
1 parent be61262 commit 6985805
Show file tree
Hide file tree
Showing 6 changed files with 235 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to

## Added

- Allow to filter contracts through their signature state
- Add contract and contract definition models with related API endpoints
- Add `instructions` markdown field to `Product` model
- Add filter course by product type
Expand Down
1 change: 1 addition & 0 deletions src/backend/joanie/core/api/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -943,6 +943,7 @@ class GenericContractViewSet(
pagination_class = Pagination
permission_classes = [permissions.IsAuthenticated]
serializer_class = serializers.ContractSerializer
filterset_class = filters.ContractViewSetFilter
ordering = ["-signed_on", "-created_on"]
queryset = models.Contract.objects.all().select_related(
"definition",
Expand Down
16 changes: 16 additions & 0 deletions src/backend/joanie/core/filters/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,19 @@ def filter_product_type(self, queryset, _name, value):
Filter courses by looking for related products with matching type
"""
return queryset.filter(products__type=value).distinct()


class ContractViewSetFilter(filters.FilterSet):
"""ContractFilter allows to filter this resource with a signature state."""

is_signed = filters.BooleanFilter(method="get_is_signed")

class Meta:
model = models.Contract
fields: List[str] = ["is_signed"]

def get_is_signed(self, queryset, _name, value):
"""
Filter Contracts by signature status
"""
return queryset.filter(signed_on__isnull=not value)
62 changes: 62 additions & 0 deletions src/backend/joanie/tests/core/test_api_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,68 @@ def test_api_contracts_list_with_owner(self, _):
},
)

def test_api_contracts_list_filter_is_signed(self):
"""
Authenticated user can query owned contracts and filter them by signature state.
"""
user = factories.UserFactory()
token = self.generate_token_from_user(user)

unsigned_contracts = factories.ContractFactory.create_batch(
5, order__owner=user
)

signed_contract = factories.ContractFactory.create(
order__owner=user,
signed_on=timezone.now(),
definition_checksum="test",
context={"title": "test"},
)

# Create random contracts that should not be returned
factories.ContractFactory.create_batch(5)

# - List without filter should return 6 contracts
with self.assertNumQueries(2):
response = self.client.get(
"/api/v1.0/contracts/",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, 200)
content = response.json()
self.assertEqual(content["count"], 6)

# - Filter by is_signed=false should return 5 contracts
with self.assertNumQueries(2):
response = self.client.get(
"/api/v1.0/contracts/?is_signed=false",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, 200)
content = response.json()
count = content["count"]
result_ids = [result["id"] for result in content["results"]]
self.assertEqual(count, 5)
self.assertCountEqual(
result_ids, [str(contract.id) for contract in unsigned_contracts]
)

# - Filter by is_signed=true should return 1 contract
with self.assertNumQueries(2):
response = self.client.get(
"/api/v1.0/contracts/?is_signed=true",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, 200)
content = response.json()
count = content["count"]
result_ids = [result["id"] for result in content["results"]]
self.assertEqual(count, 1)
self.assertEqual(result_ids, [str(signed_contract.id)])

def test_api_contracts_retrieve_anonymous(self):
"""
Anonymous user cannot query a contract.
Expand Down
79 changes: 79 additions & 0 deletions src/backend/joanie/tests/core/test_api_courses_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,85 @@ def test_api_courses_contracts_list_with_accesses(self, _):
},
)

def test_api_courses_contracts_list_filter_is_signed(self):
"""
Authenticated user with admin or owner access to the organization
can query organization's course contracts and filter them by signature state.
"""
organization = factories.OrganizationFactory()
course = factories.CourseFactory()
user = factories.UserFactory()
token = self.generate_token_from_user(user)
factories.UserOrganizationAccessFactory(
user=user,
organization=organization,
role=random.choice([enums.ADMIN, enums.OWNER]),
)

relation = factories.CourseProductRelationFactory(
organizations=[organization], course=course
)
unsigned_contracts = factories.ContractFactory.create_batch(
5,
order__product=relation.product,
order__course=course,
order__organization=organization,
)

signed_contract = factories.ContractFactory.create(
order__product=relation.product,
order__course=course,
order__organization=organization,
signed_on=timezone.now(),
definition_checksum="test",
context={"title": "test"},
)

# Create random contracts that should not be returned
factories.ContractFactory.create_batch(5)
factories.ContractFactory(order__owner=user)

# - List without filter should return 6 contracts
with self.assertNumQueries(2):
response = self.client.get(
f"/api/v1.0/courses/{str(course.id)}/contracts/",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, 200)
content = response.json()
self.assertEqual(content["count"], 6)

# - Filter by is_signed=false should return 5 contracts
with self.assertNumQueries(2):
response = self.client.get(
f"/api/v1.0/courses/{str(course.id)}/contracts/?is_signed=false",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, 200)
content = response.json()
count = content["count"]
result_ids = [result["id"] for result in content["results"]]
self.assertEqual(count, 5)
self.assertCountEqual(
result_ids, [str(contract.id) for contract in unsigned_contracts]
)

# - Filter by is_signed=true should return 1 contract
with self.assertNumQueries(2):
response = self.client.get(
f"/api/v1.0/courses/{str(course.id)}/contracts/?is_signed=true",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, 200)
content = response.json()
count = content["count"]
result_ids = [result["id"] for result in content["results"]]
self.assertEqual(count, 1)
self.assertEqual(result_ids, [str(signed_contract.id)])

def test_api_courses_contracts_retrieve_anonymous(self):
"""
Anonymous user cannot query an organization's course contract.
Expand Down
76 changes: 76 additions & 0 deletions src/backend/joanie/tests/core/test_api_organizations_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,82 @@ def test_api_organizations_contracts_list_with_accesses(self, _):
},
)

def test_api_organizations_contracts_list_filter_is_signed(self):
"""
Authenticated user with admin or owner access to the organization
can query organization's contracts and filter them by signature state.
"""
organization = factories.OrganizationFactory()
user = factories.UserFactory()
token = self.generate_token_from_user(user)
factories.UserOrganizationAccessFactory(
user=user,
organization=organization,
role=random.choice([enums.ADMIN, enums.OWNER]),
)

relation = factories.CourseProductRelationFactory(organizations=[organization])
unsigned_contracts = factories.ContractFactory.create_batch(
5,
order__product=relation.product,
order__course=relation.course,
order__organization=organization,
)

signed_contract = factories.ContractFactory.create(
order__product=relation.product,
order__course=relation.course,
order__organization=organization,
signed_on=timezone.now(),
definition_checksum="test",
context={"title": "test"},
)

# Create random contracts that should not be returned
factories.ContractFactory.create_batch(5)
factories.ContractFactory(order__owner=user)

# - List without filter should return 6 contracts
with self.assertNumQueries(2):
response = self.client.get(
f"/api/v1.0/organizations/{str(organization.id)}/contracts/",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, 200)
content = response.json()
self.assertEqual(content["count"], 6)

# - Filter by is_signed=false should return 5 contracts
with self.assertNumQueries(2):
response = self.client.get(
f"/api/v1.0/organizations/{str(organization.id)}/contracts/?is_signed=false",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, 200)
content = response.json()
count = content["count"]
result_ids = [result["id"] for result in content["results"]]
self.assertEqual(count, 5)
self.assertCountEqual(
result_ids, [str(contract.id) for contract in unsigned_contracts]
)

# - Filter by is_signed=true should return 1 contract
with self.assertNumQueries(2):
response = self.client.get(
f"/api/v1.0/organizations/{str(organization.id)}/contracts/?is_signed=true",
HTTP_AUTHORIZATION=f"Bearer {token}",
)

self.assertEqual(response.status_code, 200)
content = response.json()
count = content["count"]
result_ids = [result["id"] for result in content["results"]]
self.assertEqual(count, 1)
self.assertEqual(result_ids, [str(signed_contract.id)])

def test_api_organizations_contracts_retrieve_anonymous(self):
"""
Anonymous user cannot query an organization's contract.
Expand Down

0 comments on commit 6985805

Please sign in to comment.