Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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.11.4'
Comment thread
eemaanamir marked this conversation as resolved.
Outdated
13 changes: 12 additions & 1 deletion openassessment/assessment/test/test_peer.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import copy
import datetime
from unittest.mock import Mock, patch
from unittest.mock import Mock, patch, MagicMock

from ddt import ddt, file_data, data, unpack
import pytz
Expand All @@ -29,6 +29,17 @@
from openassessment.test_utils import CacheResetTest
from openassessment.workflow import api as workflow_api

import openassessment.workflow.models as workflow_models


def setUpModule():
"""
This method is run once for the entire test module.
We use it to globally replace send_grade_assigned_notification with a mock.
"""
workflow_models.send_grade_assigned_notification = MagicMock()


STUDENT_ITEM = {
"student_id": "Tim",
"course_id": "Demo_Course",
Expand Down
9 changes: 9 additions & 0 deletions openassessment/assessment/test/test_staff.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,19 @@
from openassessment.test_utils import CacheResetTest
from openassessment.tests.factories import StaffWorkflowFactory, TeamStaffWorkflowFactory, AssessmentFactory
from openassessment.workflow import api as workflow_api
import openassessment.workflow.models as workflow_models

from .constants import OPTIONS_SELECTED_DICT, RUBRIC, RUBRIC_OPTIONS, RUBRIC_POSSIBLE_POINTS, STUDENT_ITEM


def setUpModule():
"""
This method is run once for the entire test module.
We use it to globally replace send_grade_assigned_notification with a mock.
"""
workflow_models.send_grade_assigned_notification = mock.MagicMock()


@ddt
class TestStaffAssessment(CacheResetTest):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from contextlib import contextmanager
import tempfile
import json
from mock import Mock, patch
from mock import Mock, patch, MagicMock

from django.core.management import call_command
from django.core.management.base import CommandError
Expand All @@ -16,6 +16,17 @@
from openassessment.management.commands.create_oa_submissions_from_file import Command, SUPERUSER_USERNAME
from openassessment.tests.factories import UserFactory

import openassessment.workflow.models as workflow_models


def setUpModule():
"""
This method is run once for the entire test module.
We use it to globally replace send_grade_assigned_notification with a mock.
"""
workflow_models.send_grade_assigned_notification = MagicMock()


USERNAME_1 = 'user1'
USERNAME_2 = 'user2'
STAFF_USER_1 = 'staffuser1'
Expand Down
11 changes: 11 additions & 0 deletions openassessment/staffgrader/tests/test_get_submission_info.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
""" Tests for the get_submission_and_assessment_info endpoint """
from contextlib import contextmanager
from uuid import uuid4
from unittest.mock import MagicMock

from mock import patch, Mock
from submissions import api as sub_api
Expand All @@ -9,6 +10,16 @@
from openassessment.data import VersionNotFoundException
from openassessment.staffgrader.tests.test_base import StaffGraderMixinTestBase

import openassessment.workflow.models as workflow_models


def setUpModule():
"""
This method is run once for the entire test module.
We use it to globally replace send_grade_assigned_notification with a mock.
"""
workflow_models.send_grade_assigned_notification = MagicMock()


class GetSubmissionInfoTests(StaffGraderMixinTestBase):
""" Tests for the get_submission_info handler endpoint """
Expand Down
11 changes: 11 additions & 0 deletions openassessment/staffgrader/tests/test_list_staff_workflows.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from datetime import datetime, timedelta, timezone
import json
import random
from unittest.mock import MagicMock

import ddt
from freezegun import freeze_time
Expand All @@ -24,6 +25,16 @@
import openassessment.workflow.api as workflow_api
from openassessment.xblock.test.base import XBlockHandlerTestCase, scenario

import openassessment.workflow.models as workflow_models


def setUpModule():
"""
This method is run once for the entire test module.
We use it to globally replace send_grade_assigned_notification with a mock.
"""
workflow_models.send_grade_assigned_notification = MagicMock()


EXPECTED_ANNOTATED_WORKFLOW_FIELDS = [
'submission_uuid',
Expand Down
10 changes: 10 additions & 0 deletions openassessment/tests/test_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,16 @@
from openassessment.tests.factories import * # pylint: disable=wildcard-import
from openassessment.workflow import api as workflow_api, team_api as team_workflow_api

import openassessment.workflow.models as workflow_models


def setUpModule():
"""
This method is run once for the entire test module.
We use it to globally replace send_grade_assigned_notification with a mock.
"""
workflow_models.send_grade_assigned_notification = MagicMock()


COURSE_ID = "Test_Course"

Expand Down
19 changes: 19 additions & 0 deletions openassessment/workflow/errors.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,22 @@ 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"""


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:
Comment thread
eemaanamir marked this conversation as resolved.
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)
Comment thread
eemaanamir marked this conversation as resolved.
Outdated
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)
Comment thread
eemaanamir marked this conversation as resolved.
Outdated

# Finally save our changes if the status has changed
if self.status != new_status:
Expand Down
11 changes: 11 additions & 0 deletions openassessment/xblock/test/test_grade.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@
import copy
import json

from unittest.mock import MagicMock

import ddt

from openassessment.assessment.api import peer as peer_api
import openassessment.workflow.models as workflow_models

from .base import (
PEER_ASSESSMENTS,
Expand All @@ -22,6 +25,14 @@
)


def setUpModule():
"""
This method is run once for the entire test module.
We use it to globally replace send_grade_assigned_notification with a mock.
"""
workflow_models.send_grade_assigned_notification = MagicMock()


@ddt.ddt
class TestGrade(XBlockHandlerTestCase, SubmitAssessmentsMixin, SubmissionTestMixin):
"""
Expand Down
13 changes: 12 additions & 1 deletion openassessment/xblock/test/test_grade_explanation.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@

import json

from unittest.mock import patch
from unittest.mock import patch, MagicMock

from django.conf import settings
from django.test.utils import override_settings

from ddt import ddt, data

import openassessment.workflow.models as workflow_models

from .base import (
PEER_ASSESSMENTS,
SELF_ASSESSMENT,
Expand All @@ -21,6 +23,15 @@
scenario
)


def setUpModule():
"""
This method is run once for the entire test module.
We use it to globally replace send_grade_assigned_notification with a mock.
"""
workflow_models.send_grade_assigned_notification = MagicMock()


FEATURES_WITH_GRADING_STRATEGY_ON = settings.FEATURES.copy()
FEATURES_WITH_GRADING_STRATEGY_ON['ENABLE_ORA_PEER_CONFIGURABLE_GRADING'] = True

Expand Down
91 changes: 90 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


class TestSendStaffNotification(unittest.TestCase):
Expand Down Expand Up @@ -64,3 +67,89 @@ 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):
Comment thread
eemaanamir marked this conversation as 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.modulestore')
@patch('openassessment.xblock.utils.notifications.logger.error')
@patch('openassessment.data.map_anonymized_ids_to_usernames')
def test_invalid_key_error_logging(self, mock_map_to_username, mock_logger_error, mock_modulestore,
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_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")
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}")

@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.data.map_anonymized_ids_to_usernames')
def test_item_not_found_error_logging(self, mock_map_to_username, 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
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)

# Assertions
mock_logger_error.assert_called_once_with(f"Bad ORA location provided: {self.usage_id}")
9 changes: 9 additions & 0 deletions openassessment/xblock/test/test_self.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,19 @@
from openassessment.assessment.api import self as self_api
from openassessment.workflow import api as workflow_api
from openassessment.xblock.utils.data_conversion import create_rubric_dict
import openassessment.workflow.models as workflow_models

from .base import SubmissionTestMixin, XBlockHandlerTestCase, scenario


def setUpModule():
"""
This method is run once for the entire test module.
We use it to globally replace send_grade_assigned_notification with a mock.
"""
workflow_models.send_grade_assigned_notification = mock.MagicMock()


class TestSelfAssessment(XBlockHandlerTestCase, SubmissionTestMixin):
"""
Tests for the self-assessment XBlock handler.
Expand Down
Loading