Skip to content

Commit

Permalink
feat: added ORA grade assigned notification
Browse files Browse the repository at this point in the history
  • Loading branch information
eemaanamir committed Sep 10, 2024
1 parent 8dbf14c commit 937f6d1
Show file tree
Hide file tree
Showing 4 changed files with 154 additions and 3 deletions.
20 changes: 20 additions & 0 deletions openassessment/workflow/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,23 @@ def __init__(self, assessment_name, api_path):
assessment_name, api_path
)
super().__init__(msg)


class ItemNotFoundError(Exception):
"""An item was not found in the modulestore"""
pass


class ExceptionWithContext(Exception):
"""An exception with optional context dict to be supplied in serialized result"""

def __init__(self, context=None):
super().__init__(self)
self.context = context


class XBlockInternalError(ExceptionWithContext):
"""Errors from XBlock handlers"""

def __str__(self):
return str(self.context)
8 changes: 8 additions & 0 deletions openassessment/workflow/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from submissions import api as sub_api, team_api as sub_team_api
from openassessment.assessment.errors.base import AssessmentError
from openassessment.assessment.signals import assessment_complete_signal
from openassessment.xblock.utils.notifications import send_grade_assigned_notification

from .errors import AssessmentApiLoadError, AssessmentWorkflowError, AssessmentWorkflowInternalError

Expand Down Expand Up @@ -376,6 +377,11 @@ def update_from_assessments(
if override_submitter_requirements:
step.submitter_completed_at = common_now
step.save()
if self.status == self.STATUS.done:
score = self.get_score(assessment_requirements, course_settings, step_for_name)
submission_dict = sub_api.get_submission_and_student(self.submission_uuid)
send_grade_assigned_notification(self.item_id, submission_dict['student_item']['student_id'], score)
return

if self.status == self.STATUS.done:
return
Expand Down Expand Up @@ -443,6 +449,8 @@ def update_from_assessments(
if score.get("staff_id") is None:
self.set_score(score)
new_status = self.STATUS.done
submission_dict = sub_api.get_submission_and_student(self.submission_uuid)
send_grade_assigned_notification(self.item_id, submission_dict['student_item']['student_id'] ,score)

# Finally save our changes if the status has changed
if self.status != new_status:
Expand Down
79 changes: 78 additions & 1 deletion openassessment/xblock/test/test_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@
import unittest
from unittest.mock import patch, MagicMock

from openassessment.xblock.utils.notifications import send_staff_notification
from opaque_keys import InvalidKeyError

from openassessment.xblock.utils.notifications import send_staff_notification, send_grade_assigned_notification
from openassessment.workflow.errors import ItemNotFoundError, XBlockInternalError


class TestSendStaffNotification(unittest.TestCase):
Expand Down Expand Up @@ -64,3 +67,77 @@ def test_send_staff_notification_error_logging(self, mock_send_event, mock_logge

# Assertions
mock_logger_error.assert_called_once_with(f"Error while sending ora staff notification: {mock_exception}")


class TestSendGradeAssignedNotification(unittest.TestCase):

def setUp(self):
self.usage_id = 'block-v1:TestX+TST+TST+type@problem+block@ora'
self.ora_user_anonymized_id = 'anon_user_1'
self.score = {
'points_earned': 10,
'points_possible': 20,
}

@patch('openassessment.xblock.utils.notifications.User.objects.get')
@patch('openassessment.xblock.utils.notifications.UsageKey.from_string')
@patch('openassessment.xblock.utils.notifications.modulestore')
@patch('openassessment.xblock.utils.notifications.USER_NOTIFICATION_REQUESTED.send_event')
@patch('openassessment.data.map_anonymized_ids_to_usernames')
def test_send_notification_success(self, mock_map_to_username, mock_send_event, mock_modulestore, mock_from_string,
mock_get_user):
"""
Test that the notification is sent when all data is valid.
"""
mock_map_to_username.return_value = {self.ora_user_anonymized_id: 'student1'}
mock_get_user.return_value = MagicMock(id=2)
mock_from_string.return_value = MagicMock(course_key='course-v1:TestX+TST+TST')
mock_modulestore.return_value.get_item.return_value = MagicMock(display_name="ORA Assignment")
mock_modulestore.return_value.get_course.return_value = MagicMock(display_name="Test Course")

with patch('django.conf.settings.LMS_ROOT_URL', 'http://localhost'):
send_grade_assigned_notification(self.usage_id, self.ora_user_anonymized_id, self.score)

mock_send_event.assert_called_once()
args, kwargs = mock_send_event.call_args
notification_data = kwargs['notification_data']
self.assertEqual(notification_data.user_ids, [2])
self.assertEqual(notification_data.context['ora_name'], 'ORA Assignment')
self.assertEqual(notification_data.context['course_name'], 'Test Course')
self.assertEqual(notification_data.context['points_earned'], 10)
self.assertEqual(notification_data.context['points_possible'], 20)
self.assertEqual(notification_data.notification_type, "ora_grade_assigned")
self.assertEqual(notification_data.content_url,
'http://localhost/courses/course-v1:TestX+TST+TST/jump_to/block-v1:TestX+TST+TST+type@problem+block@ora')

@patch('openassessment.xblock.utils.notifications.logger.error')
@patch('openassessment.xblock.utils.notifications.UsageKey.from_string', side_effect=InvalidKeyError)
def test_invalid_key_error(self, mock_from_string, mock_logger_error):
"""
Test that InvalidKeyError is logged correctly.
"""
send_grade_assigned_notification(self.usage_id, self.ora_user_anonymized_id, self.score)
mock_logger_error.assert_called_once_with(f"Bad ORA location provided: {self.usage_id}")

@patch('openassessment.xblock.utils.notifications.logger.error')
@patch('openassessment.xblock.utils.notifications.modulestore.get_item', side_effect=ItemNotFoundError)
def test_item_not_found_error(self, mock_get_item, mock_logger_error):
"""
Test that ItemNotFoundError is logged correctly.
"""
with patch('openassessment.xblock.utils.notifications.UsageKey.from_string',
return_value=MagicMock(course_key='course-v1:TestX+TST+TST')):
send_grade_assigned_notification(self.usage_id, self.ora_user_anonymized_id, self.score)
mock_logger_error.assert_called_once_with(f"Bad ORA location provided: {self.usage_id}")

@patch('openassessment.xblock.utils.notifications.logger.error')
@patch('openassessment.xblock.utils.notifications.modulestore.get_item',
side_effect=XBlockInternalError("XBlock error"))
def test_xblock_internal_error(self, mock_get_item, mock_logger_error):
"""
Test that XBlockInternalError is logged correctly.
"""
with patch('openassessment.xblock.utils.notifications.UsageKey.from_string',
return_value=MagicMock(course_key='course-v1:TestX+TST+TST')):
send_grade_assigned_notification(self.usage_id, self.ora_user_anonymized_id, self.score)
mock_logger_error.assert_called_once_with("XBlock error")
50 changes: 48 additions & 2 deletions openassessment/xblock/utils/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
"""
import logging

from opaque_keys.edx.keys import UsageKey, CourseKey
from opaque_keys import InvalidKeyError

from django.conf import settings
from openedx_events.learning.signals import COURSE_NOTIFICATION_REQUESTED
from openedx_events.learning.data import CourseNotificationData
from openedx_events.learning.signals import COURSE_NOTIFICATION_REQUESTED, USER_NOTIFICATION_REQUESTED
from openedx_events.learning.data import CourseNotificationData, UserNotificationData
from openassessment.runtime_imports.functions import modulestore
from django.contrib.auth import get_user_model
from openassessment.workflow.errors import ItemNotFoundError, XBlockInternalError

logger = logging.getLogger(__name__)
User = get_user_model()


def send_staff_notification(course_id, problem_id, ora_name):
Expand All @@ -34,3 +40,43 @@ def send_staff_notification(course_id, problem_id, ora_name):
COURSE_NOTIFICATION_REQUESTED.send_event(course_notification_data=notification_data)
except Exception as e:
logger.error(f"Error while sending ora staff notification: {e}")


def send_grade_assigned_notification(usage_id, ora_user_anonymized_id, score):
"""
Send a user notification for a course for a new grade being assigned
"""
from openassessment.data import map_anonymized_ids_to_usernames as map_to_username

try:
# Get ORA user
user_name_list = map_to_username([ora_user_anonymized_id])
ora_user = User.objects.get(username=user_name_list[ora_user_anonymized_id])
# Get ORA block
ora_usage_key = UsageKey.from_string(usage_id)
ora_metadata = modulestore().get_item(ora_usage_key)
# Get course metadata
course_id = CourseKey.from_string(str(ora_usage_key.course_key))
course_metadata = modulestore().get_course(course_id)
notification_data = UserNotificationData(
user_ids=[ora_user.id],
context={
'ora_name': ora_metadata.display_name,
'course_name': course_metadata.display_name,
'points_earned': score['points_earned'],
'points_possible': score['points_possible'],
},
notification_type="ora_grade_assigned",
content_url=f"{getattr(settings, 'LMS_ROOT_URL', '')}/courses/{str(course_id)}/jump_to/{str(ora_usage_key)}",
app_name="grading",
course_key=course_id,
)
USER_NOTIFICATION_REQUESTED.send_event(notification_data=notification_data)

# Catch bad ORA location
except (InvalidKeyError, ItemNotFoundError):
logger.error(f"Bad ORA location provided: {usage_id}")

# Issues with the XBlock handlers
except XBlockInternalError as ex:
logger.error(ex)

0 comments on commit 937f6d1

Please sign in to comment.