chore: resolve conflicts

This commit is contained in:
salman2013
2023-10-04 18:45:02 +05:00
41 changed files with 867 additions and 213 deletions

View File

@@ -15,7 +15,7 @@ class CreditConfig(AppConfig):
name = 'openedx.core.djangoapps.credit'
def ready(self):
from . import signals # lint-amnesty, pylint: disable=unused-import
from .signals import handlers # lint-amnesty, pylint: disable=unused-import
if settings.FEATURES.get('ENABLE_SPECIAL_EXAMS'):
from .services import CreditService
set_runtime_service('credit', CreditService())

View File

@@ -5,15 +5,78 @@ This file contains receivers of course publication signals.
import logging
from django.contrib.auth import get_user_model
from django.core.exceptions import ObjectDoesNotExist
from django.dispatch import receiver
from django.utils import timezone
from opaque_keys.edx.keys import CourseKey
from openedx_events.learning.signals import (
EXAM_ATTEMPT_ERRORED,
EXAM_ATTEMPT_REJECTED,
EXAM_ATTEMPT_RESET,
EXAM_ATTEMPT_SUBMITTED,
EXAM_ATTEMPT_VERIFIED
)
from openedx.core.djangoapps.credit.api.eligibility import (
is_credit_course,
remove_credit_requirement_status,
set_credit_requirement_status
)
from openedx.core.djangoapps.signals.signals import COURSE_GRADE_CHANGED
User = get_user_model()
log = logging.getLogger(__name__)
def handle_exam_event(signal, event_data, credit_status=None):
"""
update credit requirements based on exam event
"""
user_data = event_data.student_user
course_key = event_data.course_key
usage_key = event_data.usage_key
request_namespace = 'exam'
# quick exit, if course is not credit enabled
if not is_credit_course(course_key):
return
# log any activity to the credit requirements table
log.info(
f'Recieved {signal} signal, changing credit requirement for '
f'user_id={user_data.id}, course_key_or_id={course_key} '
f'content_id={usage_key}'
)
try:
user = User.objects.get(id=user_data.id)
except ObjectDoesNotExist:
log.error(
'Error occurred while handling exam event for '
f'{user_data.id} and content_id {usage_key}. '
'User does not exist!'
)
return
if signal == EXAM_ATTEMPT_RESET:
remove_credit_requirement_status(
user.username,
course_key,
request_namespace,
str(usage_key),
)
else:
set_credit_requirement_status(
user.username,
course_key,
request_namespace,
str(usage_key),
credit_status
)
def on_course_publish(course_key):
"""
Will receive a delegated 'course_published' signal from cms/djangoapps/contentstore/signals.py
@@ -94,3 +157,48 @@ def listen_for_grade_calculation(sender, user, course_grade, course_key, deadlin
api.set_credit_requirement_status(
user, course_id, 'grade', 'grade', status=status, reason=reason
)
@receiver(EXAM_ATTEMPT_RESET)
def listen_for_exam_reset(sender, signal, **kwargs):
"""
exam reset event from the event bus
"""
event_data = kwargs.get('exam_attempt')
handle_exam_event(signal, event_data)
@receiver(EXAM_ATTEMPT_SUBMITTED)
def listen_for_exam_submitted(sender, signal, **kwargs):
"""
exam submission event from the event bus
"""
event_data = kwargs.get('exam_attempt')
handle_exam_event(signal, event_data, credit_status='submitted')
@receiver(EXAM_ATTEMPT_VERIFIED)
def listen_for_exam_verified(sender, signal, **kwargs):
"""
exam verification event from the event bus
"""
event_data = kwargs.get('exam_attempt')
handle_exam_event(signal, event_data, credit_status='satisfied')
@receiver(EXAM_ATTEMPT_REJECTED)
def listen_for_exam_rejected(sender, signal, **kwargs):
"""
exam rejection event from the event bus
"""
event_data = kwargs.get('exam_attempt')
handle_exam_event(signal, event_data, credit_status='failed')
@receiver(EXAM_ATTEMPT_ERRORED)
def listen_for_exam_errored(sender, signal, **kwargs):
"""
exam error event from the event bus
"""
event_data = kwargs.get('exam_attempt')
handle_exam_event(signal, event_data, credit_status='failed')

View File

@@ -1,23 +1,41 @@
"""
Tests for minimum grade requirement status
Tests for minimum grade and credit requirement status
"""
from datetime import datetime, timedelta, timezone
from unittest import mock
from uuid import uuid4
from datetime import datetime, timedelta
from unittest.mock import MagicMock
import ddt
import pytz
from django.test.client import RequestFactory
from opaque_keys.edx.keys import UsageKey
from openedx_events.data import EventsMetadata
from openedx_events.learning.data import ExamAttemptData, UserData, UserPersonalData
from openedx_events.learning.signals import (
EXAM_ATTEMPT_ERRORED,
EXAM_ATTEMPT_REJECTED,
EXAM_ATTEMPT_RESET,
EXAM_ATTEMPT_SUBMITTED,
EXAM_ATTEMPT_VERIFIED
)
from common.djangoapps.course_modes.models import CourseMode
from openedx.core.djangoapps.credit.api import get_credit_requirement_status, set_credit_requirements
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
from openedx.core.djangoapps.credit.signals import listen_for_grade_calculation
from openedx.core.djangolib.testing.utils import skip_unless_lms
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.tests.factories import UserFactory
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from openedx.core.djangoapps.credit.api import get_credit_requirement_status, set_credit_requirements
from openedx.core.djangoapps.credit.models import CreditCourse, CreditProvider
from openedx.core.djangoapps.credit.signals.handlers import (
listen_for_exam_errored,
listen_for_exam_rejected,
listen_for_exam_reset,
listen_for_exam_submitted,
listen_for_exam_verified,
listen_for_grade_calculation
)
from openedx.core.djangolib.testing.utils import skip_unless_lms
from xmodule.modulestore.tests.django_utils import \
ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
@@ -76,7 +94,7 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
def assert_requirement_status(self, grade, due_date, expected_status):
""" Verify the user's credit requirement status is as expected after simulating a grading calculation. """
course_grade = MagicMock()
course_grade = mock.MagicMock()
course_grade.percent = grade
listen_for_grade_calculation(None, self.user, course_grade, self.course.id, due_date)
req_status = get_credit_requirement_status(self.course.id, self.request.user.username, 'grade', 'grade')
@@ -129,3 +147,146 @@ class TestMinGradedRequirementStatus(ModuleStoreTestCase):
"""Test with valid grades submitted before deadline with non-verified enrollment."""
self.enrollment.update_enrollment(mode, True)
self.assert_requirement_status(0.8, self.VALID_DUE_DATE, None)
@skip_unless_lms
@ddt.ddt
class TestExamEvents(ModuleStoreTestCase):
"""
Test exam events
"""
HANDLERS = {
EXAM_ATTEMPT_ERRORED: listen_for_exam_errored,
EXAM_ATTEMPT_REJECTED: listen_for_exam_rejected,
EXAM_ATTEMPT_RESET: listen_for_exam_reset,
EXAM_ATTEMPT_SUBMITTED: listen_for_exam_submitted,
EXAM_ATTEMPT_VERIFIED: listen_for_exam_verified,
}
def setUp(self):
super().setUp()
self.course = CourseFactory.create(
org='TestX', number='999', display_name='Test Course'
)
self.subsection_key = UsageKey.from_string('block-v1:edX+TestX+Test_Course+type@sequential+block@subsection')
self.user = UserFactory()
# Enable the course for credit
CreditCourse.objects.create(
course_key=self.course.id,
enabled=True,
)
@staticmethod
def _get_exam_event_data(user, course, usage_key):
""" create ExamAttemptData object for user """
return ExamAttemptData(
student_user=UserData(
id=user.id,
is_active=True,
pii=UserPersonalData(
username=user.username,
email=user.email,
),
),
course_key=course.id,
usage_key=usage_key,
exam_type='timed',
requesting_user=None,
)
@staticmethod
def _get_exam_event_metadata(event_signal):
""" create metadata object for event """
return EventsMetadata(
event_type=event_signal.event_type,
id=uuid4(),
minorversion=0,
source='openedx/lms/web',
sourcehost='lms.test',
time=datetime.now(timezone.utc)
)
@mock.patch('openedx.core.djangoapps.credit.signals.handlers.remove_credit_requirement_status', autospec=True)
def test_exam_reset(self, mock_remove_credit_status):
"""
Test exam reset event
"""
event_data = self._get_exam_event_data(self.user, self.course, self.subsection_key)
event_metadata = self._get_exam_event_metadata(EXAM_ATTEMPT_RESET)
listen_for_exam_reset(None, EXAM_ATTEMPT_RESET, event_metadata=event_metadata, exam_attempt=event_data)
mock_remove_credit_status.assert_called_once_with(
self.user.username, self.course.id, 'exam', str(self.subsection_key)
)
@ddt.data(
(EXAM_ATTEMPT_ERRORED, 'failed'),
(EXAM_ATTEMPT_REJECTED, 'failed'),
(EXAM_ATTEMPT_VERIFIED, 'satisfied'),
(EXAM_ATTEMPT_SUBMITTED, 'submitted'),
)
@ddt.unpack
@mock.patch('openedx.core.djangoapps.credit.signals.handlers.set_credit_requirement_status', autospec=True)
def test_exam_update_event(self, event_signal, expected_status, mock_set_credit_status):
"""
Test exam events that update credit status
"""
event_data = self._get_exam_event_data(self.user, self.course, self.subsection_key)
event_metadata = self._get_exam_event_metadata(event_signal)
handler = self.HANDLERS.get(event_signal)
handler(None, event_signal, event_metadata=event_metadata, exam_attempt=event_data)
mock_set_credit_status.assert_called_once_with(
self.user.username, self.course.id, 'exam', str(self.subsection_key), status=expected_status
)
@ddt.data(
EXAM_ATTEMPT_RESET,
EXAM_ATTEMPT_REJECTED,
EXAM_ATTEMPT_ERRORED,
EXAM_ATTEMPT_VERIFIED,
EXAM_ATTEMPT_SUBMITTED,
)
def test_exam_event_bad_user(self, event_signal):
"""
Test exam event with a user that does not exist in the LMS
"""
self.user.id = 999 # don't save to db so user doesn't exist
event_data = self._get_exam_event_data(self.user, self.course, self.subsection_key)
event_metadata = self._get_exam_event_metadata(event_signal)
handler = self.HANDLERS.get(event_signal)
with mock.patch('openedx.core.djangoapps.credit.signals.handlers.log.error') as mock_log:
handler(None, event_signal, event_metadata=event_metadata, exam_attempt=event_data)
mock_log.assert_called_once_with(
'Error occurred while handling exam event for '
f'{self.user.id} and content_id {self.subsection_key}. '
'User does not exist!'
)
@ddt.data(
EXAM_ATTEMPT_RESET,
EXAM_ATTEMPT_REJECTED,
EXAM_ATTEMPT_ERRORED,
EXAM_ATTEMPT_VERIFIED,
EXAM_ATTEMPT_SUBMITTED,
)
@mock.patch('openedx.core.djangoapps.credit.signals.handlers.remove_credit_requirement_status', autospec=True)
@mock.patch('openedx.core.djangoapps.credit.signals.handlers.set_credit_requirement_status', autospec=True)
def test_exam_event_non_credit_course(self, event_signal, mock_remove_credit_status, mock_set_credit_status):
"""
Credit credit logic should not run on non-credit courses
"""
non_credit_course = CourseFactory.create()
event_data = self._get_exam_event_data(self.user, non_credit_course, self.subsection_key)
event_metadata = self._get_exam_event_metadata(event_signal)
handler = self.HANDLERS.get(event_signal)
handler(None, event_signal, event_metadata=event_metadata, exam_attempt=event_data)
mock_remove_credit_status.assert_not_called()
mock_set_credit_status.assert_not_called()

View File

@@ -11,7 +11,7 @@ from edx_proctoring.api import create_exam
from openedx.core.djangoapps.credit.api import get_credit_requirements
from openedx.core.djangoapps.credit.exceptions import InvalidCreditRequirements
from openedx.core.djangoapps.credit.models import CreditCourse
from openedx.core.djangoapps.credit.signals import on_course_publish
from openedx.core.djangoapps.credit.signals.handlers import on_course_publish
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order

View File

@@ -1066,7 +1066,9 @@ class EnrollmentAllowedView(APIView):
**Example Request**
POST /api/enrollment/v1/enrollment_allowed
POST /api/enrollment/v1/enrollment_allowed/
Note: The URL for this request must finish with /
Example request data:
```
@@ -1086,7 +1088,6 @@ class EnrollmentAllowedView(APIView):
- `auto_enroll` (optional, bool: default=false, _body_)
**Responses**
- 200: Success, enrollment allowed found.
- 400: Bad request, missing data.
- 403: Forbidden, you need to be staff.
- 409: Conflict, enrollment allowed already exists.
@@ -1122,7 +1123,9 @@ class EnrollmentAllowedView(APIView):
**Example Request**
DELETE /api/enrollment/v1/enrollment_allowed
DELETE /api/enrollment/v1/enrollment_allowed/
Note: The URL for this request must finish with /
Example request data:
```

View File

@@ -344,7 +344,7 @@ def _track_user_login(user, request):
segment.identify(
user.id,
{
'email': request.POST.get('email'),
'email': user.email,
'username': user.username
},
{