Skip to content

Commit d150ac1

Browse files
committed
fix: add date filter in payments api
1 parent d91ea94 commit d150ac1

File tree

8 files changed

+89
-12
lines changed

8 files changed

+89
-12
lines changed

futurex_openedx_extensions/dashboard/details/courses.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Courses details collectors"""
22
from __future__ import annotations
33

4+
from datetime import date
45
from typing import List
56

67
from common.djangoapps.student.models import CourseEnrollment
@@ -298,7 +299,7 @@ def get_courses_feedback_queryset( # pylint: disable=too-many-arguments
298299
return queryset
299300

300301

301-
def get_courses_orders_queryset( # pylint: disable=too-many-arguments
302+
def get_courses_orders_queryset( # pylint: disable=too-many-arguments, too-many-locals
302303
fx_permission_info: dict,
303304
user_ids: list = None,
304305
course_ids: list = None,
@@ -311,6 +312,8 @@ def get_courses_orders_queryset( # pylint: disable=too-many-arguments
311312
include_user_details: bool = False,
312313
status: str | None = None,
313314
item_type: str | None = None,
315+
date_from: date | None = None,
316+
date_to: date | None = None,
314317
) -> QuerySet:
315318
"""
316319
Returns a filtered queryset of Cart Orders based on provided criteria.
@@ -356,4 +359,6 @@ def get_courses_orders_queryset( # pylint: disable=too-many-arguments
356359
item_type=item_type,
357360
include_invoice=include_invoice,
358361
include_user_details=include_user_details,
362+
date_from=date_from,
363+
date_to=date_to,
359364
)

futurex_openedx_extensions/dashboard/docs_src.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1694,6 +1694,22 @@ def get_optional_parameter(path: str) -> Any:
16941694
'Note: right now only paid_course is implemented.'
16951695
)
16961696
),
1697+
query_parameter(
1698+
'date_from',
1699+
str,
1700+
description=(
1701+
'The start date of the range for filtering results. Must be provided in `YYYY-MM-DD` format. '
1702+
'Can be used together with `date_to` to limit results within a specific date range.'
1703+
),
1704+
),
1705+
query_parameter(
1706+
'date_to',
1707+
str,
1708+
description=(
1709+
'The end date of the range for filtering results. Must be provided in `YYYY-MM-DD` format. '
1710+
'Can be used together with `date_from` to limit results within a specific date range.'
1711+
),
1712+
),
16971713
common_parameters['include_staff'],
16981714
common_parameters['download'],
16991715
],

futurex_openedx_extensions/dashboard/views.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,11 @@
7474
FX_VIEW_DEFAULT_AUTH_CLASSES,
7575
RATING_RANGE,
7676
)
77-
from futurex_openedx_extensions.helpers.converters import dict_to_hash, error_details_to_dictionary
77+
from futurex_openedx_extensions.helpers.converters import (
78+
date_str_to_date_obj,
79+
dict_to_hash,
80+
error_details_to_dictionary,
81+
)
7882
from futurex_openedx_extensions.helpers.course_categories import CourseCategories
7983
from futurex_openedx_extensions.helpers.exceptions import FXCodedException, FXExceptionCodes
8084
from futurex_openedx_extensions.helpers.export_mixins import ExportCSVMixin
@@ -308,13 +312,8 @@ def _load_query_params(self, request: Any) -> None:
308312
date_from = request.query_params.get('date_from')
309313
date_to = request.query_params.get('date_to')
310314

311-
try:
312-
self.date_from = datetime.strptime(date_from, '%Y-%m-%d').date() if date_from else None
313-
self.date_to = datetime.strptime(date_to, '%Y-%m-%d').date() if date_to else None
314-
except (ValueError, TypeError) as exc:
315-
raise ParseError(
316-
'Invalid dates. You must provide a valid date_from and date_to formated as YYYY-MM-DD'
317-
) from exc
315+
self.date_from = date_str_to_date_obj(date_from, 'date_from')
316+
self.date_to = date_str_to_date_obj(date_to, 'date_to')
318317

319318
def _get_certificates_count_data(self, one_tenant_permission_info: dict) -> int:
320319
"""Get the count of certificates for the given tenant"""
@@ -1915,6 +1914,8 @@ def get_queryset(self) -> QuerySet:
19151914
course_ids = self.request.query_params.get('course_ids', '')
19161915
user_ids = self.request.query_params.get('user_ids', '')
19171916
usernames = self.request.query_params.get('usernames', '')
1917+
date_from = self.request.query_params.get('date_from', '')
1918+
date_to = self.request.query_params.get('date_to', '')
19181919
course_ids_list = [
19191920
course.strip() for course in course_ids.split(',')
19201921
] if course_ids else None
@@ -1952,6 +1953,8 @@ def get_queryset(self) -> QuerySet:
19521953
include_user_details=self.request.query_params.get('include_user_details', '0') == '1',
19531954
status=status,
19541955
item_type=item_type,
1956+
date_from=date_str_to_date_obj(date_from, 'date_from'),
1957+
date_to=date_str_to_date_obj(date_to, 'date_to'),
19551958
)
19561959
self._cached_course_map = getattr(qs, 'courses_map', {})
19571960
return qs

futurex_openedx_extensions/helpers/converters.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from urllib.parse import urljoin
1010

1111
from dateutil.relativedelta import relativedelta
12+
from rest_framework.exceptions import ParseError
1213

1314
from futurex_openedx_extensions.helpers import constants as cs
1415

@@ -281,3 +282,15 @@ def to_indian_numerals(text: str) -> str:
281282
:return: The text with Arabic numerals converted to Indian numerals.
282283
"""
283284
return _text_translate(text, '0123456789', '٠١٢٣٤٥٦٧٨٩')
285+
286+
287+
def date_str_to_date_obj(date_str: str, field_name: str) -> date | None:
288+
"""Convert date of format (YYYY-MM-DD) to a Python date"""
289+
if not date_str:
290+
return None
291+
try:
292+
return datetime.strptime(date_str, '%Y-%m-%d').date()
293+
except (ValueError, TypeError) as exc:
294+
raise ParseError(
295+
f'Invalid {field_name}. You must provide a valid date formated as YYYY-MM-DD'
296+
) from exc

test_utils/edx_platform_mocks_shared/zeitlabs_payments/querysets.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
"""Mock"""
2+
from datetime import date
3+
24
from django.db.models.query import QuerySet
35

46

@@ -10,6 +12,8 @@ def get_orders_queryset( # pylint: disable=too-many-arguments,unused-argument
1012
item_type: str | None = None,
1113
include_invoice: bool = False,
1214
include_user_details: bool = False,
15+
date_from: date | None = None,
16+
date_to: date | None = None,
1317
):
1418
"""
1519
Mock.

tests/test_dashboard/test_details/test_details_courses.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from unittest.mock import Mock, patch
33

44
import pytest
5+
from datetime import datetime
56
from common.djangoapps.student.models import CourseEnrollment
67
from completion.models import BlockCompletion
78
from django.contrib.auth import get_user_model
@@ -247,6 +248,7 @@ def test_get_courses_orders_queryset( # pylint: disable=too-many-locals
247248
include_user_details = True
248249
status = 'paid'
249250
item_type = 'paid_course'
251+
date_from = datetime.strptime('2025-01-01', "%Y-%m-%d").date()
250252
mock_accessible_users = Mock(name='UsersQS')
251253
mock_accessible_courses = Mock(name='CoursesQS')
252254
mock_get_users_and_courses.return_value = (
@@ -269,6 +271,8 @@ def test_get_courses_orders_queryset( # pylint: disable=too-many-locals
269271
user_ids=user_ids,
270272
item_type=item_type,
271273
learner_search=learner_search,
274+
date_from=date_from,
275+
date_to=None,
272276
)
273277

274278
mock_get_users_and_courses.assert_called_once_with(
@@ -290,6 +294,8 @@ def test_get_courses_orders_queryset( # pylint: disable=too-many-locals
290294
item_type=item_type,
291295
include_invoice=include_invoice,
292296
include_user_details=include_user_details,
297+
date_from=date_from,
298+
date_to=None,
293299
)
294300

295301
assert result == mock_orders_qs

tests/test_dashboard/test_views.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,13 @@ def test_invalid_stats(self):
229229
(
230230
'day',
231231
'invalid', '2024-01-02',
232-
'Invalid dates. You must provide a valid date_from and date_to formated as YYYY-MM-DD'
232+
'Invalid date_from. You must provide a valid date formated as YYYY-MM-DD'
233233
),
234234
(
235235
'day',
236236
'2024-01-01',
237237
'invalid',
238-
'Invalid dates. You must provide a valid date_from and date_to formated as YYYY-MM-DD'
238+
'Invalid date_to. You must provide a valid date formated as YYYY-MM-DD'
239239
),
240240
('day', '2024-01-03', '2024-01-02', None),
241241
)

tests/test_helpers/test_converters.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,10 @@
44
from unittest.mock import Mock, patch
55

66
import pytest
7+
from rest_framework.exceptions import ParseError
78

89
from futurex_openedx_extensions.helpers import converters
9-
from futurex_openedx_extensions.helpers.converters import DateMethods
10+
from futurex_openedx_extensions.helpers.converters import DateMethods, date_str_to_date_obj
1011

1112

1213
@pytest.mark.parametrize('ids_string, expected', [
@@ -200,3 +201,32 @@ def test_to_arabic_numerals(input_text, expected_output, test_case):
200201
def test_to_indian_numerals(input_text, expected_output, test_case):
201202
"""Verify that to_indian_numerals returns correct data"""
202203
assert converters.to_indian_numerals(input_text) == expected_output, f'Failed: {test_case}'
204+
205+
206+
@pytest.mark.parametrize(
207+
'usecase,input_value,expected,should_raise',
208+
[
209+
('Valid date', '2025-01-01', date(2025, 1, 1), False),
210+
('Empty string returns None', '', None, False),
211+
('None input returns None', None, None, False),
212+
('Wrong format', '01-01-2025', None, True),
213+
('Wrong separator', '2025/01/01', None, True),
214+
('Invalid month >12', '2025-13-01', None, True),
215+
('Invalid day 0', '2025-01-00', None, True),
216+
('Random string', 'abcd-ef-gh', None, True),
217+
('Integer input', 12345, None, True),
218+
],
219+
)
220+
def test_date_str_to_date_obj(usecase, input_value, expected, should_raise):
221+
"""Test date_str_to_date_obj with valid and invalid inputs."""
222+
if should_raise:
223+
with pytest.raises(ParseError) as exc_info:
224+
date_str_to_date_obj(input_value, 'date_from')
225+
assert 'Invalid date_from' in str(exc_info.value), (
226+
f"Failed usecase '{usecase}': input={repr(input_value)}, exception={repr(exc_info.value)}"
227+
)
228+
else:
229+
result = date_str_to_date_obj(input_value, 'date_from')
230+
assert result == expected, (
231+
f"Failed usecase '{usecase}': input={repr(input_value)}, expected={repr(expected)}, got={repr(result)}"
232+
)

0 commit comments

Comments
 (0)