Skip to content

Commit

Permalink
feat: added ORA grade assigned notification (#2232)
Browse files Browse the repository at this point in the history
* feat: added ORA grade assigned notification

* test: updated python tests

* chore: updated version

* refactor: refactored notification implementation
  • Loading branch information
eemaanamir authored Sep 19, 2024
1 parent 3aef63f commit a33db7f
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 5 deletions.
2 changes: 1 addition & 1 deletion openassessment/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
Initialization Information for Open Assessment Module
"""

__version__ = '6.11.3'
__version__ = '6.12.0'
4 changes: 4 additions & 0 deletions openassessment/workflow/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,7 @@ 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"""
11 changes: 11 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,13 @@ 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)
if submission_dict['student_item']['student_id']:
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 +451,9 @@ 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)
if submission_dict['student_item']['student_id']:
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
128 changes: 127 additions & 1 deletion openassessment/xblock/test/test_notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@
import unittest
from unittest.mock import patch, MagicMock

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

from django.contrib.auth import get_user_model
from django.core.exceptions import FieldError
from openassessment.xblock.utils.notifications import send_staff_notification, send_grade_assigned_notification
from openassessment.workflow.errors import ItemNotFoundError

User = get_user_model()


class TestSendStaffNotification(unittest.TestCase):
Expand Down Expand Up @@ -64,3 +71,122 @@ 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")

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")

@patch('openassessment.xblock.utils.notifications.User.objects.get')
@patch('openassessment.xblock.utils.notifications.UsageKey.from_string')
@patch('openassessment.xblock.utils.notifications.logger.error')
@patch('openassessment.xblock.utils.notifications.USER_NOTIFICATION_REQUESTED.send_event')
@patch('openassessment.data.map_anonymized_ids_to_usernames')
def test_invalid_key_error_logging(self, mock_map_to_username, mock_send_event, mock_logger_error,
mock_from_string, mock_get_user):
"""
Test error logging when InvalidKeyError is raised.
"""
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_exception = InvalidKeyError('Invalid key error', 'some_serialized_data')

# Force the exception
with patch('openassessment.xblock.utils.notifications.UsageKey.from_string', side_effect=mock_exception):
send_grade_assigned_notification(self.usage_id, self.ora_user_anonymized_id, self.score)

# Assertions
mock_logger_error.assert_called_once_with(f"Bad ORA location provided: {self.usage_id}")
mock_send_event.assert_not_called()

@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.logger.error')
@patch('openassessment.xblock.utils.notifications.USER_NOTIFICATION_REQUESTED.send_event')
@patch('openassessment.data.map_anonymized_ids_to_usernames')
def test_item_not_found_error_logging(self, mock_map_to_username, mock_send_event, mock_logger_error,
mock_modulestore, mock_from_string, mock_get_user):
"""
Test error logging when ItemNotFoundError is raised.
"""
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_exception = ItemNotFoundError('Item not found')
mock_modulestore.return_value.get_item.side_effect = mock_exception

send_grade_assigned_notification(self.usage_id, self.ora_user_anonymized_id, self.score)

# Assertions
mock_logger_error.assert_called_once_with(f"Bad ORA location provided: {self.usage_id}")
mock_send_event.assert_not_called()

@patch('openassessment.xblock.utils.notifications.logger.error')
@patch('openassessment.xblock.utils.notifications.USER_NOTIFICATION_REQUESTED.send_event')
@patch('openassessment.data.map_anonymized_ids_to_usernames')
@patch('openassessment.xblock.utils.notifications.User.objects.get')
def test_user_does_not_exist_error_logging(self, mock_get_user, mock_map_to_username, mock_send_event,
mock_logger_error):
"""
Test error logging when User.DoesNotExist is raised.
"""
mock_map_to_username.return_value = {self.ora_user_anonymized_id: 'non_existent_user'}
mock_get_user.side_effect = User.DoesNotExist('User does not exist')

send_grade_assigned_notification(self.usage_id, self.ora_user_anonymized_id, self.score)

# Assertions
mock_logger_error.assert_called_once_with('Unknown User Error: User does not exist')
mock_send_event.assert_not_called()

@patch('openassessment.xblock.utils.notifications.logger.error')
@patch('openassessment.xblock.utils.notifications.USER_NOTIFICATION_REQUESTED.send_event')
@patch('openassessment.data.map_anonymized_ids_to_usernames')
def test_getting_user_name_error_logging(self, mock_map_to_username, mock_send_event, mock_logger_error):
"""
Test error logging when FieldError is raised.
"""
mock_map_to_username.side_effect = FieldError('FieldError: Cannot resolve keyword \'anonymoususerid\'')

send_grade_assigned_notification(self.usage_id, self.ora_user_anonymized_id, self.score)

# Assertions
mock_logger_error.assert_called_once_with('Error while getting user name for the user id anon_user_1: '
'FieldError: Cannot resolve keyword \'anonymoususerid\'')
mock_send_event.assert_not_called()
60 changes: 58 additions & 2 deletions openassessment/xblock/utils/notifications.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,19 @@
"""
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 django.core.exceptions import FieldError
from openedx_events.learning.signals import COURSE_NOTIFICATION_REQUESTED, USER_NOTIFICATION_REQUESTED
from openedx_events.learning.data import CourseNotificationData, UserNotificationData
from django.contrib.auth import get_user_model
from openassessment.runtime_imports.functions import modulestore
from openassessment.workflow.errors import ItemNotFoundError

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


def send_staff_notification(course_id, problem_id, ora_name):
Expand All @@ -34,3 +41,52 @@ 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

user_name_list = []
try:
# Get ORA user name
user_name_list = map_to_username([ora_user_anonymized_id])
except FieldError as exc:
logger.error(f'Error while getting user name for the user id {ora_user_anonymized_id}: {exc}')

try:
if (not user_name_list) or (not user_name_list[ora_user_anonymized_id]):
return
# Get ORA user
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)}"
f"/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}")

# Error with getting User
except User.DoesNotExist as exc:
logger.error(f'Unknown User Error: {exc}')
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "edx-ora2",
"version": "6.11.3",
"version": "6.12.0",
"repository": "https://github.com/openedx/edx-ora2.git",
"dependencies": {
"@edx/frontend-build": "8.0.6",
Expand Down

0 comments on commit a33db7f

Please sign in to comment.