Skip to content
Draft
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
13 changes: 13 additions & 0 deletions openedx/core/djangoapps/course_date_signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from datetime import timedelta
import logging

from django.db import transaction
from django.dispatch import receiver
from edx_when.api import FIELDS_TO_EXTRACT, set_dates_for_course
from xblock.fields import Scope
Expand Down Expand Up @@ -181,3 +182,15 @@ def extract_dates(sender, course_key, **kwargs): # pylint: disable=unused-argum
set_dates_for_course(course_key, date_items)
except Exception: # pylint: disable=broad-except
log.exception('Unable to set dates for %s on course publish', course_key)


@receiver(SignalHandler.course_published)
def update_assignment_dates(sender, course_key, **kwargs): # pylint: disable=unused-argument
"""
Receive the course_published signal and update assignment dates for the course.
"""
# import here, because signal is registered at startup, but items in tasks are not available yet
from .tasks import update_assignment_dates_for_course

course_key_str = str(course_key)
transaction.on_commit(lambda: update_assignment_dates_for_course.delay(course_key_str))
35 changes: 35 additions & 0 deletions openedx/core/djangoapps/course_date_signals/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from celery import shared_task
from celery.utils.log import get_task_logger
from django.contrib.auth import get_user_model
from edx_django_utils.monitoring import set_code_owner_attribute
from edx_when.api import update_or_create_assignments_due_dates
from opaque_keys.edx.keys import CourseKey

from lms.djangoapps.courseware.courses import get_course_assignments


User = get_user_model()


LOGGER = get_task_logger(__name__)


@shared_task
@set_code_owner_attribute
def update_assignment_dates_for_course(course_key_str):
"""
Celery task to update assignment dates for a course.
"""
try:
LOGGER.info("Starting to update assignment dates for course %s", course_key_str)
course_key = CourseKey.from_string(course_key_str)
staff_user = User.objects.filter(is_staff=True).first()
if not staff_user:
LOGGER.error("No staff user found to update assignment dates for course %s", course_key_str)
return
assignments = get_course_assignments(course_key, staff_user)
update_or_create_assignments_due_dates(course_key, assignments)
LOGGER.info("Successfully updated assignment dates for course %s", course_key_str)
except Exception: # pylint: disable=broad-except
LOGGER.exception("Could not update assignment dates for course %s", course_key_str)
raise
Empty file.
205 changes: 205 additions & 0 deletions openedx/core/djangoapps/course_date_signals/tests/test_tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
from unittest.mock import Mock, patch
from django.test import TestCase
from opaque_keys.edx.keys import CourseKey, UsageKey
from edx_when.api import models as when_models
from django.contrib.auth import get_user_model
from datetime import datetime, timezone

from openedx.core.djangoapps.course_date_signals.tasks import update_assignment_dates_for_course

User = get_user_model()


class TestUpdateAssignmentDatesForCourse(TestCase):

def setUp(self):
self.course_key = CourseKey.from_string('course-v1:edX+DemoX+Demo_Course')
self.course_key_str = str(self.course_key)
self.staff_user = User.objects.create_user(
username='staff_user',
email='staff@example.com',
is_staff=True
)
self.block_key = UsageKey.from_string(
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@test1'
)
self.due_date = datetime(2024, 12, 31, 23, 59, 59, tzinfo=timezone.utc)

@patch('openedx.core.djangoapps.course_date_signals.tasks.get_course_assignments')
def test_update_assignment_dates_new_records(self, mock_get_assignments):
"""
Test inserting new records when missing.
"""
assignment = Mock()
assignment.title = 'Test Assignment'
assignment.date = self.due_date
assignment.block_key = self.block_key
assignment.assignment_type = 'Homework'
mock_get_assignments.return_value = [assignment]

update_assignment_dates_for_course(self.course_key_str)

content_date = when_models.ContentDate.objects.get(
course_id=self.course_key,
location=self.block_key
)
self.assertEqual(content_date.assignment_title, 'Test Assignment')
self.assertEqual(content_date.block_type, 'Homework')
self.assertEqual(content_date.policy.abs_date, self.due_date)

@patch('openedx.core.djangoapps.course_date_signals.tasks.get_course_assignments')
def test_update_assignment_dates_existing_records(self, mock_get_assignments):
"""
Test updating existing records when values differ.
"""
existing_policy = when_models.DatePolicy.objects.create(
abs_date=datetime(2024, 6, 1, tzinfo=timezone.utc)
)
when_models.ContentDate.objects.create(
course_id=self.course_key,
location=self.block_key,
field='due',
block_type='Homework',
policy=existing_policy,
assignment_title='Old Title',
course_name=self.course_key.course,
subsection_name='Old Title'
)

assignment = Mock()
assignment.title = 'Updated Assignment'
assignment.date = self.due_date
assignment.block_key = self.block_key
assignment.assignment_type = 'Homework'
mock_get_assignments.return_value = [assignment]

update_assignment_dates_for_course(self.course_key_str)

content_date = when_models.ContentDate.objects.get(
course_id=self.course_key,
location=self.block_key
)
self.assertEqual(content_date.assignment_title, 'Updated Assignment')
self.assertEqual(content_date.policy.abs_date, self.due_date)

@patch('openedx.core.djangoapps.course_date_signals.tasks.get_course_assignments')
def test_missing_staff_user(self, mock_get_assignments):
"""
Test graceful handling when no staff user exists.
"""
User.objects.filter(is_staff=True).delete()

update_assignment_dates_for_course(self.course_key_str)

mock_get_assignments.assert_not_called()

@patch('openedx.core.djangoapps.course_date_signals.tasks.get_course_assignments')
def test_assignment_with_null_date(self, mock_get_assignments):
"""
Test handling assignments with null dates.
"""
assignment = Mock()
assignment.title = 'No Due Date Assignment'
assignment.date = None
assignment.block_key = self.block_key
assignment.assignment_type = 'Homework'
mock_get_assignments.return_value = [assignment]

update_assignment_dates_for_course(self.course_key_str)

content_date_exists = when_models.ContentDate.objects.filter(
course_id=self.course_key,
location=self.block_key
).exists()
self.assertFalse(content_date_exists)

@patch('openedx.core.djangoapps.course_date_signals.tasks.get_course_assignments')
def test_assignment_with_missing_metadata(self, mock_get_assignments):
"""
Test handling assignments with missing metadata.
"""
assignment = Mock()
assignment.title = None
assignment.date = self.due_date
assignment.block_key = self.block_key
assignment.assignment_type = None
mock_get_assignments.return_value = [assignment]

update_assignment_dates_for_course(self.course_key_str)

content_date_exists = when_models.ContentDate.objects.filter(
course_id=self.course_key,
location=self.block_key
).exists()
self.assertFalse(content_date_exists)

@patch('openedx.core.djangoapps.course_date_signals.tasks.get_course_assignments')
def test_multiple_assignments(self, mock_get_assignments):
"""
Test processing multiple assignments.
"""
assignment1 = Mock()
assignment1.title = 'Assignment 1'
assignment1.date = self.due_date
assignment1.block_key = self.block_key
assignment1.assignment_type = 'Gradeable'

assignment2 = Mock()
assignment2.title = 'Assignment 2'
assignment2.date = datetime(2025, 1, 15, tzinfo=timezone.utc)
assignment2.block_key = UsageKey.from_string(
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@test2'
)
assignment2.assignment_type = 'Homework'

mock_get_assignments.return_value = [assignment1, assignment2]

update_assignment_dates_for_course(self.course_key_str)

self.assertEqual(when_models.ContentDate.objects.count(), 2)

@patch('openedx.core.djangoapps.course_date_signals.tasks.get_course_assignments')
def test_invalid_course_key(self, mock_get_assignments):
"""
Test handling invalid course key.
"""
with self.assertRaises(Exception):
update_assignment_dates_for_course('invalid-course-key')

@patch('openedx.core.djangoapps.course_date_signals.tasks.get_course_assignments')
def test_get_course_assignments_exception(self, mock_get_assignments):
"""
Test handling exception from get_course_assignments.
"""
mock_get_assignments.side_effect = Exception('API Error')

with self.assertRaises(Exception):
update_assignment_dates_for_course(self.course_key_str)

@patch('openedx.core.djangoapps.course_date_signals.tasks.get_course_assignments')
def test_empty_assignments_list(self, mock_get_assignments):
"""
Test handling empty assignments list.
"""
mock_get_assignments.return_value = []

update_assignment_dates_for_course(self.course_key_str)

self.assertEqual(when_models.ContentDate.objects.count(), 0)

@patch('openedx.core.djangoapps.course_date_signals.tasks.get_course_assignments')
@patch('edx_when.models.DatePolicy.objects.get_or_create')
def test_date_policy_creation_exception(self, mock_policy_create, mock_get_assignments):
"""
Test handling exception during DatePolicy creation.
"""
assignment = Mock()
assignment.title = 'Test Assignment'
assignment.date = self.due_date
assignment.block_key = self.block_key
assignment.assignment_type = 'problem'
mock_get_assignments.return_value = [assignment]
mock_policy_create.side_effect = Exception('Database Error')

with self.assertRaises(Exception):
update_assignment_dates_for_course(self.course_key_str)
Loading