diff --git a/CHANGELOG.rst b/CHANGELOG.rst index b1a632f..500fe05 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -13,19 +13,23 @@ Change Log Unreleased ********** + +4.6.3 - 2025-01-06 +****************** +* Uses CourseEnrollment instead of CourseMode to get the upgrade deadline required to calculate if a learner's audit trial is expired. * Updated setup docs -4.6.2 - 2025-12-18 +4.6.2 - 2024-12-18 ****************** * Fixed the params for expiration_date in the admin table for audit trial. * Add ENABLE_XPERT_AUDIT instructions. -4.6.1 - 2025-12-17 +4.6.1 - 2024-12-17 ****************** * Added an admin table for the LearningAssistantAuditTrial model. This table includes an expiration_date valued that is calculated based on the start_date. -4.6.0 - 2025-12-10 +4.6.0 - 2024-12-10 ****************** * Add an audit_trial_length_days attribute to the response returned by the ChatSummaryView, representing the number of days in an audit trial as currently configured. It does not necessarily represent the number of days in the diff --git a/learning_assistant/__init__.py b/learning_assistant/__init__.py index b52cec0..dabae04 100644 --- a/learning_assistant/__init__.py +++ b/learning_assistant/__init__.py @@ -2,6 +2,6 @@ Plugin for a learning assistant backend, intended for use within edx-platform. """ -__version__ = '4.6.2' +__version__ = '4.6.3' default_app_config = 'learning_assistant.apps.LearningAssistantConfig' # pylint: disable=invalid-name diff --git a/learning_assistant/api.py b/learning_assistant/api.py index 3f1ff5c..5c488db 100644 --- a/learning_assistant/api.py +++ b/learning_assistant/api.py @@ -3,7 +3,7 @@ """ import datetime import logging -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from django.conf import settings from django.contrib.auth import get_user_model @@ -12,11 +12,6 @@ from jinja2 import BaseLoader, Environment from opaque_keys import InvalidKeyError -try: - from common.djangoapps.course_modes.models import CourseMode -except ImportError: - CourseMode = None - from learning_assistant.constants import ACCEPTED_CATEGORY_TYPES, CATEGORY_TYPE_MAP from learning_assistant.data import LearningAssistantAuditTrialData, LearningAssistantCourseEnabledData from learning_assistant.models import ( @@ -307,19 +302,25 @@ def get_or_create_audit_trial(user): ) -def audit_trial_is_expired(audit_trial_data, courserun_key): - """ - Given a user (User), get or create the corresponding LearningAssistantAuditTrial trial object. +def audit_trial_is_expired(enrollment, audit_trial_data): """ - course_mode = CourseMode.objects.get(course=courserun_key) + Given an enrollment and audit_trial_data, return whether the audit trial is expired as a boolean. + + Arguments: + * enrollment (CourseEnrollment): the user course enrollment + * audit_trial_data (LearningAssistantAuditTrialData): the data related to the audit trial - upgrade_deadline = course_mode.expiration_datetime() + Returns: + * audit_trial_is_expired (boolean): whether the audit trial is expired + """ + upgrade_deadline = enrollment.upgrade_deadline + today = datetime.now(tz=timezone.utc) # If the upgrade deadline has passed, return True for expired. Upgrade deadline is an optional attribute of a - # CourseMode, so if it's None, then do not return True. - days_until_upgrade_deadline = datetime.now() - upgrade_deadline if upgrade_deadline else None + # CourseEnrollment, so if it's None, then do not return True. + days_until_upgrade_deadline = today - upgrade_deadline if upgrade_deadline else None if days_until_upgrade_deadline is not None and days_until_upgrade_deadline >= timedelta(days=0): return True # If the user's trial is past its expiry date, return True for expired. Else, return False. - return audit_trial_data is None or audit_trial_data.expiration_date <= datetime.now() + return audit_trial_data is None or audit_trial_data.expiration_date <= today diff --git a/learning_assistant/views.py b/learning_assistant/views.py index 87d5713..77eda79 100644 --- a/learning_assistant/views.py +++ b/learning_assistant/views.py @@ -156,7 +156,7 @@ def post(self, request, course_run_id): # next message. Otherwise, return 403 elif enrollment_mode in CourseMode.UPSELL_TO_VERIFIED_MODES: # AUDIT, HONOR audit_trial = get_or_create_audit_trial(request.user) - is_user_audit_trial_expired = audit_trial_is_expired(audit_trial, courserun_key) + is_user_audit_trial_expired = audit_trial_is_expired(enrollment_object, audit_trial) if is_user_audit_trial_expired: return Response( status=http_status.HTTP_403_FORBIDDEN, @@ -383,7 +383,7 @@ def get(self, request, course_run_id): has_trial_access = ( enrollment_mode in valid_trial_access_modes and audit_trial - and not audit_trial_is_expired(audit_trial, courserun_key) + and not audit_trial_is_expired(enrollment_object, audit_trial) ) if ( diff --git a/tests/test_api.py b/tests/test_api.py index 3937856..91599d5 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -2,7 +2,7 @@ Test cases for the learning-assistant api module. """ import itertools -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from unittest.mock import MagicMock, patch import ddt @@ -491,6 +491,7 @@ class GetAuditTrialExpirationDateTests(TestCase): """ Test suite for get_audit_trial_expiration_date. """ + @ddt.data( (datetime(2024, 1, 1, 0, 0, 0), datetime(2024, 1, 2, 0, 0, 0), 1), (datetime(2024, 1, 18, 0, 0, 0), datetime(2024, 1, 19, 0, 0, 0), 1), @@ -516,6 +517,7 @@ class GetAuditTrialTests(TestCase): """ Test suite for get_audit_trial. """ + @freeze_time('2024-01-01') def setUp(self): super().setUp() @@ -548,6 +550,7 @@ class GetOrCreateAuditTrialTests(TestCase): """ Test suite for get_or_create_audit_trial. """ + def setUp(self): super().setUp() self.user = User(username='tester', email='tester@test.com') @@ -596,65 +599,60 @@ def setUp(self): self.user = User(username='tester', email='tester@test.com') self.user.save() - self.upgrade_deadline = datetime.now() + timedelta(days=1) # 1 day from now - - @freeze_time('2024-01-01') - @patch('learning_assistant.api.CourseMode') - def test_upgrade_deadline_expired(self, mock_course_mode): - - mock_mode = MagicMock() - mock_mode.expiration_datetime.return_value = datetime.now() - timedelta(days=1) # yesterday - mock_course_mode.objects.get.return_value = mock_mode + @freeze_time('2024-01-01 00:00:01 UTC') + def test_upgrade_deadline_expired(self): + today = datetime.now(tz=timezone.utc) + mock_enrollment = MagicMock() + mock_enrollment.upgrade_deadline = today - timedelta(days=1) # yesterday - start_date = datetime.now() + start_date = today audit_trial_data = LearningAssistantAuditTrialData( user_id=self.user.id, start_date=start_date, expiration_date=start_date + timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS), ) - self.assertEqual(audit_trial_is_expired(audit_trial_data, self.course_key), True) - - @freeze_time('2024-01-01') - @patch('learning_assistant.api.CourseMode') - def test_upgrade_deadline_none(self, mock_course_mode): + self.assertEqual(audit_trial_is_expired(mock_enrollment, audit_trial_data), True) - mock_mode = MagicMock() - mock_mode.expiration_datetime.return_value = None - mock_course_mode.objects.get.return_value = mock_mode + @freeze_time('2024-01-01 00:00:01 UTC') + def test_upgrade_deadline_none(self): + today = datetime.now(tz=timezone.utc) + mock_enrollment = MagicMock() + mock_enrollment.upgrade_deadline = None - # Verify that the audit trial data is considered when determing whether an audit trial is expired and not the + # Verify that the audit trial data is considered when determining whether an audit trial is expired and not the # upgrade deadline. - start_date = datetime.now() + start_date = today audit_trial_data = LearningAssistantAuditTrialData( user_id=self.user.id, start_date=start_date, expiration_date=start_date + timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS), ) - self.assertEqual(audit_trial_is_expired(audit_trial_data, self.course_key), False) + self.assertEqual(audit_trial_is_expired(mock_enrollment, audit_trial_data), False) - start_date = datetime.now() - timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS + 1) + start_date = today - timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS + 1) audit_trial_data = LearningAssistantAuditTrialData( user_id=self.user.id, start_date=start_date, expiration_date=start_date + timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS), ) - self.assertEqual(audit_trial_is_expired(audit_trial_data, self.course_key), True) + self.assertEqual(audit_trial_is_expired(mock_enrollment, audit_trial_data), True) @ddt.data( # exactly the trial deadline - datetime(year=2024, month=1, day=1) - timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS), + datetime(year=2024, month=1, day=1, tzinfo=timezone.utc) - + timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS), # 1 day more than trial deadline - datetime(year=2024, month=1, day=1) - timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS + 1), + datetime(year=2024, month=1, day=1, tzinfo=timezone.utc) - + timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS + 1), ) - @freeze_time('2024-01-01') - @patch('learning_assistant.api.CourseMode') - def test_audit_trial_expired(self, start_date, mock_course_mode): - mock_mode = MagicMock() - mock_mode.expiration_datetime.return_value = datetime.now() + timedelta(days=1) # tomorrow - mock_course_mode.objects.get.return_value = mock_mode + @freeze_time('2024-01-01 00:00:01 UTC') + def test_audit_trial_expired(self, start_date): + today = datetime.now(tz=timezone.utc) + mock_enrollment = MagicMock() + mock_enrollment.upgrade_deadline = today + timedelta(days=1) # tomorrow audit_trial_data = LearningAssistantAuditTrialData( user_id=self.user.id, @@ -662,20 +660,19 @@ def test_audit_trial_expired(self, start_date, mock_course_mode): expiration_date=get_audit_trial_expiration_date(start_date), ) - self.assertEqual(audit_trial_is_expired(audit_trial_data, self.upgrade_deadline), True) + self.assertEqual(audit_trial_is_expired(mock_enrollment, audit_trial_data), True) - @freeze_time('2024-01-01') - @patch('learning_assistant.api.CourseMode') - def test_audit_trial_unexpired(self, mock_course_mode): - mock_mode = MagicMock() - mock_mode.expiration_datetime.return_value = datetime.now() + timedelta(days=1) # tomorrow - mock_course_mode.objects.get.return_value = mock_mode + @freeze_time('2024-01-01 00:00:01 UTC') + def test_audit_trial_unexpired(self): + today = datetime.now(tz=timezone.utc) + mock_enrollment = MagicMock() + mock_enrollment.upgrade_deadline = today + timedelta(days=1) # tomorrow - start_date = datetime.now() - timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS - 1) + start_date = today - timedelta(days=settings.LEARNING_ASSISTANT_AUDIT_TRIAL_LENGTH_DAYS - 1) audit_trial_data = LearningAssistantAuditTrialData( user_id=self.user.id, start_date=start_date, expiration_date=get_audit_trial_expiration_date(start_date), ) - self.assertEqual(audit_trial_is_expired(audit_trial_data, self.upgrade_deadline), False) + self.assertEqual(audit_trial_is_expired(mock_enrollment, audit_trial_data), False)