Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: added ORA grade assigned notification #2232

Merged
merged 10 commits into from
Sep 19, 2024
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
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:
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved
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):
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

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
eemaanamir marked this conversation as resolved.
Show resolved Hide resolved

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
Loading