Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion futurex_openedx_extensions/dashboard/details/courses.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Courses details collectors"""
from __future__ import annotations

from datetime import date
from typing import List

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


def get_courses_orders_queryset( # pylint: disable=too-many-arguments
def get_courses_orders_queryset( # pylint: disable=too-many-arguments, too-many-locals
fx_permission_info: dict,
user_ids: list = None,
course_ids: list = None,
Expand All @@ -311,6 +312,8 @@ def get_courses_orders_queryset( # pylint: disable=too-many-arguments
include_user_details: bool = False,
status: str | None = None,
item_type: str | None = None,
date_from: date | None = None,
date_to: date | None = None,
) -> QuerySet:
"""
Returns a filtered queryset of Cart Orders based on provided criteria.
Expand Down Expand Up @@ -356,4 +359,6 @@ def get_courses_orders_queryset( # pylint: disable=too-many-arguments
item_type=item_type,
include_invoice=include_invoice,
include_user_details=include_user_details,
date_from=date_from,
date_to=date_to,
)
16 changes: 16 additions & 0 deletions futurex_openedx_extensions/dashboard/docs_src.py
Original file line number Diff line number Diff line change
Expand Up @@ -1694,6 +1694,22 @@ def get_optional_parameter(path: str) -> Any:
'Note: right now only paid_course is implemented.'
)
),
query_parameter(
'date_from',
str,
description=(
'The start date of the range for filtering results. Must be provided in `YYYY-MM-DD` format. '
'Can be used together with `date_to` to limit results within a specific date range.'
),
),
query_parameter(
'date_to',
str,
description=(
'The end date of the range for filtering results. Must be provided in `YYYY-MM-DD` format. '
'Can be used together with `date_from` to limit results within a specific date range.'
),
),
common_parameters['include_staff'],
common_parameters['download'],
],
Expand Down
12 changes: 12 additions & 0 deletions futurex_openedx_extensions/dashboard/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -1601,3 +1601,15 @@ def create(self, validated_data: Any) -> Any:
def update(self, instance: Any, validated_data: Any) -> Any:
"""Not implemented: Update an existing object."""
raise ValueError('This serializer does not support update.')


class ReportDateFilterSerializer(ReadOnlySerializer):
"""Serializer for report date filters."""
date_from = serializers.DateField(
required=False,
input_formats=['%Y-%m-%d'],
)
date_to = serializers.DateField(
required=False,
input_formats=['%Y-%m-%d'],
)
24 changes: 15 additions & 9 deletions futurex_openedx_extensions/dashboard/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -305,16 +305,13 @@ def _load_query_params(self, request: Any) -> None:

self.fill_missing_periods = request.query_params.get('fill_missing_periods', '1') == '1'

date_from = request.query_params.get('date_from')
date_to = request.query_params.get('date_to')

try:
self.date_from = datetime.strptime(date_from, '%Y-%m-%d').date() if date_from else None
self.date_to = datetime.strptime(date_to, '%Y-%m-%d').date() if date_to else None
except (ValueError, TypeError) as exc:
serializer = serializers.ReportDateFilterSerializer(data=request.query_params)
if not serializer.is_valid(raise_exception=False):
raise ParseError(
'Invalid dates. You must provide a valid date_from and date_to formated as YYYY-MM-DD'
) from exc
'Invalid dates. date_from and date_to must be formated as YYYY-MM-DD when provided.',
)
self.date_from = serializer.validated_data.get('date_from')
self.date_to = serializer.validated_data.get('date_to')

def _get_certificates_count_data(self, one_tenant_permission_info: dict) -> int:
"""Get the count of certificates for the given tenant"""
Expand Down Expand Up @@ -1915,6 +1912,13 @@ def get_queryset(self) -> QuerySet:
course_ids = self.request.query_params.get('course_ids', '')
user_ids = self.request.query_params.get('user_ids', '')
usernames = self.request.query_params.get('usernames', '')

date_serializer = serializers.ReportDateFilterSerializer(data=self.request.query_params)
if not date_serializer.is_valid(raise_exception=False):
raise ParseError(
'Invalid dates. date_from and date_to must be formated as YYYY-MM-DD when provided.',
)

course_ids_list = [
course.strip() for course in course_ids.split(',')
] if course_ids else None
Expand Down Expand Up @@ -1952,6 +1956,8 @@ def get_queryset(self) -> QuerySet:
include_user_details=self.request.query_params.get('include_user_details', '0') == '1',
status=status,
item_type=item_type,
date_from=date_serializer.validated_data.get('date_from'),
date_to=date_serializer.validated_data.get('date_to'),
)
self._cached_course_map = getattr(qs, 'courses_map', {})
return qs
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Mock"""
from datetime import date

from django.db.models.query import QuerySet


Expand All @@ -10,6 +12,8 @@ def get_orders_queryset( # pylint: disable=too-many-arguments,unused-argument
item_type: str | None = None,
include_invoice: bool = False,
include_user_details: bool = False,
date_from: date | None = None,
date_to: date | None = None,
):
"""
Mock.
Expand Down
6 changes: 6 additions & 0 deletions tests/test_dashboard/test_details/test_details_courses.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for courses details collectors"""
from datetime import datetime
from unittest.mock import Mock, patch

import pytest
Expand Down Expand Up @@ -247,6 +248,7 @@ def test_get_courses_orders_queryset( # pylint: disable=too-many-locals
include_user_details = True
status = 'paid'
item_type = 'paid_course'
date_from = datetime.strptime('2025-01-01', '%Y-%m-%d').date()
mock_accessible_users = Mock(name='UsersQS')
mock_accessible_courses = Mock(name='CoursesQS')
mock_get_users_and_courses.return_value = (
Expand All @@ -269,6 +271,8 @@ def test_get_courses_orders_queryset( # pylint: disable=too-many-locals
user_ids=user_ids,
item_type=item_type,
learner_search=learner_search,
date_from=date_from,
date_to=None,
)

mock_get_users_and_courses.assert_called_once_with(
Expand All @@ -290,6 +294,8 @@ def test_get_courses_orders_queryset( # pylint: disable=too-many-locals
item_type=item_type,
include_invoice=include_invoice,
include_user_details=include_user_details,
date_from=date_from,
date_to=None,
)

assert result == mock_orders_qs
17 changes: 14 additions & 3 deletions tests/test_dashboard/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,13 +229,13 @@ def test_invalid_stats(self):
(
'day',
'invalid', '2024-01-02',
'Invalid dates. You must provide a valid date_from and date_to formated as YYYY-MM-DD'
'Invalid dates. date_from and date_to must be formated as YYYY-MM-DD when provided.',
),
(
'day',
'2024-01-01',
'invalid',
'Invalid dates. You must provide a valid date_from and date_to formated as YYYY-MM-DD'
'Invalid dates. date_from and date_to must be formated as YYYY-MM-DD when provided.',
),
('day', '2024-01-03', '2024-01-02', None),
)
Expand Down Expand Up @@ -268,6 +268,7 @@ def test_load_query_params(
response = self.client.get(url)
if error_message:
self.assertEqual(response.status_code, http_status.HTTP_400_BAD_REQUEST)
print(response.data)
self.assertEqual(str(response.data['detail']), error_message)
else:
self.assertEqual(response.status_code, http_status.HTTP_200_OK)
Expand Down Expand Up @@ -3303,7 +3304,7 @@ def test_invalid_item_type(self, item_valid_types):
self.assertEqual(response.status_code, http_status.HTTP_400_BAD_REQUEST)

@patch('futurex_openedx_extensions.dashboard.views.get_courses_orders_queryset')
def test_success_witchout_cached_course_map(self, mock_qs):
def test_success_without_cached_course_map(self, mock_qs):
"""Verify that the view returns the correct response"""
mock_qs.return_value = []
self.login_user(self.staff_user)
Expand Down Expand Up @@ -3331,6 +3332,16 @@ def test_success_with_cached_course_map(self, mock_get_qs):
response = self.client.get(self.url)
self.assertEqual(response.status_code, http_status.HTTP_200_OK)

def test_invalid_date(self):
"""Verify that the view returns the correct response"""
self.login_user(self.staff_user)
response = self.client.get(f'{self.url}?date_from=invalid-date')
self.assertEqual(response.status_code, http_status.HTTP_400_BAD_REQUEST)
self.assertEqual(
response.data['detail'],
'Invalid dates. date_from and date_to must be formated as YYYY-MM-DD when provided.',
)


@ddt.ddt
class TestCategoriesView(MockPatcherMixin, BaseTestViewMixin):
Expand Down