Skip to content

Commit

Permalink
Merge branch 'master' into rpenido/fal-3621-paste-tags-when-pasting-x…
Browse files Browse the repository at this point in the history
…blocks-with-tag-data
  • Loading branch information
rpenido authored Mar 4, 2024
2 parents e0fb8e9 + 42418fb commit 46556f9
Show file tree
Hide file tree
Showing 21 changed files with 622 additions and 96 deletions.
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ else
atlas pull $(ATLAS_OPTIONS) translations/edx-platform/conf/locale:conf/locale
i18n_tool generate
endif
paver i18n_compilejs
python manage.py lms compilejsi18n
python manage.py cms compilejsi18n


detect_changed_source_translations: ## check if translation files are up-to-date
Expand Down
96 changes: 95 additions & 1 deletion lms/djangoapps/support/tests/test_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
from common.djangoapps.third_party_auth.tests.factories import SAMLProviderConfigFactory
from common.test.utils import disable_signal
from lms.djangoapps.program_enrollments.tests.factories import ProgramCourseEnrollmentFactory, ProgramEnrollmentFactory
from lms.djangoapps.support.models import CourseResetAudit
from lms.djangoapps.support.models import CourseResetAudit, CourseResetCourseOptIn
from lms.djangoapps.support.serializers import ProgramEnrollmentSerializer
from lms.djangoapps.support.tests.factories import CourseResetCourseOptInFactory, CourseResetAuditFactory
from lms.djangoapps.verify_student.models import VerificationDeadline
Expand Down Expand Up @@ -2331,3 +2331,97 @@ def test_multiple_failed_audits(self):
'can_reset': True,
'status': most_recent_audit.status_message()
}])


class TestResetCourseViewPost(SupportViewTestCase):
"""
Tests for creating course request
"""

def setUp(self):
super().setUp()
SupportStaffRole().add_users(self.user)

self.course_id = 'course-v1:a+b+c'

self.other_user = User.objects.create(username='otheruser', password='test')

self.course = CourseFactory.create(
org='a',
course='b',
run='c',
enable_proctored_exams=True,
proctoring_provider=settings.PROCTORING_BACKENDS['DEFAULT'],
)
self.enrollment = CourseEnrollmentFactory(
is_active=True,
mode='verified',
course_id=self.course.id,
user=self.user
)
self.opt_in = CourseResetCourseOptInFactory.create(course_id=self.course.id)

self.other_course = CourseFactory.create(
org='x',
course='y',
run='z',
)

def _url(self, username):
return reverse("support:course_reset", kwargs={'username_or_email': username})

def test_wrong_username(self):
"""
Test that a request with a username which does not exits returns 404
"""
response = self.client.post(self._url(username='does_not_exist'), data={'course_id': 'course-v1:aa+bb+c'})
self.assertEqual(response.status_code, 404)

def test_learner_course_reset(self):
response = self.client.post(self._url(username=self.user.username), data={'course_id': self.course_id})
self.assertEqual(response.status_code, 201)
self.assertEqual(response.data, {
'course_id': self.course_id,
'status': response.data['status'],
'can_reset': False,
'display_name': self.course.display_name
})

def test_course_not_opt_in(self):
response = self.client.post(self._url(username=self.user.username), data={'course_id': 'course-v1:aa+bb+c'})
self.assertEqual(response.status_code, 404)

def test_course_reset_failed(self):
course = CourseFactory.create(
org='xx',
course='yy',
run='zz',
)
enrollment = CourseEnrollmentFactory(
is_active=True,
mode='verified',
course_id=course.id,
user=self.user
)

opt_in_course = CourseResetCourseOptIn.objects.create(
course_id=course.id,
active=True
)

CourseResetAudit.objects.create(
course=opt_in_course,
course_enrollment=enrollment,
reset_by=self.other_user,
status=CourseResetAudit.CourseResetStatus.FAILED
)
response = self.client.post(self._url(username=self.user.username), data={'course_id': course.id})
self.assertEqual(response.status_code, 200)

def test_course_reset_dupe(self):
CourseResetAuditFactory.create(
course=self.opt_in,
course_enrollment=self.enrollment,
)
response2 = self.client.post(self._url(username=self.user.username), data={'course_id': self.course_id})
self.assertEqual(response2.status_code, 204)
67 changes: 66 additions & 1 deletion lms/djangoapps/support/views/course_reset.py
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,69 @@ def get(self, request, username_or_email):

@method_decorator(require_support_permission)
def post(self, request, username_or_email):
""" Other Ticket """
"""
Resets a course for the given learner
returns a dicts with the format {
'course_id': <course id>
'display_name': <course display name>
'status': <status of the enrollment wrt/reset, to be displayed to user>
'can_reset': (boolean) <can the course be reset for this learner>
}
"""
course_id = request.data['course_id']
try:
user = get_user_by_username_or_email(username_or_email)
except User.DoesNotExist:
return Response({'error': 'User does not exist'}, status=404)
try:
opt_in_course = CourseResetCourseOptIn.objects.get(course_id=course_id)
except CourseResetCourseOptIn.DoesNotExist:
return Response({'error': 'Course is not eligible'}, status=404)
enrollment = CourseEnrollment.objects.get(
course=course_id,
user=user,
is_active=True
)
user_passed = user_has_passing_grade_in_course(enrollment=enrollment)
course_overview = enrollment.course_overview
course_reset_audit = CourseResetAudit.objects.filter(course_enrollment=enrollment).first()

if course_reset_audit and (
course_reset_audit.status == CourseResetAudit.CourseResetStatus.FAILED
and not user_passed
):
course_reset_audit.status = CourseResetAudit.CourseResetStatus.ENQUEUED
course_reset_audit.save()
# Call celery task
resp = {
'course_id': course_id,
'status': course_reset_audit.status_message(),
'can_reset': False,
'display_name': course_overview.display_name
}
return Response(resp, status=200)

elif course_reset_audit and course_reset_audit.status in (
CourseResetAudit.CourseResetStatus.IN_PROGRESS,
CourseResetAudit.CourseResetStatus.ENQUEUED
):
return Response(None, status=204)

if enrollment and opt_in_course and not user_passed:
course_reset_audit = CourseResetAudit.objects.create(
course=opt_in_course,
course_enrollment=enrollment,
reset_by=request.user,
)
resp = {
'course_id': course_id,
'status': course_reset_audit.status_message(),
'can_reset': False,
'display_name': course_overview.display_name
}

# Call celery task
return Response(resp, status=201)
else:
return Response(None, status=400)
21 changes: 13 additions & 8 deletions openedx/core/djangoapps/content_tagging/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@

import openedx_tagging.core.tagging.api as oel_tagging
from django.db.models import Exists, OuterRef, Q, QuerySet
from opaque_keys.edx.keys import CourseKey, LearningContextKey
from opaque_keys.edx.keys import CourseKey
from opaque_keys.edx.locator import LibraryLocatorV2
from openedx_tagging.core.tagging.models import ObjectTag, Taxonomy
from organizations.models import Organization

Expand Down Expand Up @@ -131,24 +132,28 @@ def get_unassigned_taxonomies(enabled=True) -> QuerySet:


def get_all_object_tags(
content_key: LearningContextKey,
content_key: LibraryLocatorV2 | CourseKey,
) -> tuple[ObjectTagByObjectIdDict, TaxonomyDict]:
"""
Get all the object tags applied to components in the given course/library.
Includes any tags applied to the course/library as a whole.
Returns a tuple with a dictionary of grouped object tags for all blocks and a dictionary of taxonomies.
"""
# ToDo: Add support for other content types (like LibraryContent and LibraryBlock)
context_key_str = str(content_key)
# We use a block_id_prefix (i.e. the modified course id) to get the tags for the children of the Content
# (course/library) in a single db query.
if isinstance(content_key, CourseKey):
course_key_str = str(content_key)
# We use a block_id_prefix (i.e. the modified course id) to get the tags for the children of the Content
# (course) in a single db query.
block_id_prefix = course_key_str.replace("course-v1:", "block-v1:", 1)
block_id_prefix = context_key_str.replace("course-v1:", "block-v1:", 1)
elif isinstance(content_key, LibraryLocatorV2):
block_id_prefix = context_key_str.replace("lib:", "lb:", 1)
else:
raise NotImplementedError(f"Invalid content_key: {type(content_key)} -> {content_key}")

# There is no API method in oel_tagging.api that does this yet,
# so for now we have to build the ORM query directly.
all_object_tags = list(ObjectTag.objects.filter(
Q(object_id__startswith=block_id_prefix) | Q(object_id=course_key_str),
Q(object_id__startswith=block_id_prefix) | Q(object_id=content_key),
Q(tag__isnull=False, tag__taxonomy__isnull=False),
).select_related("tag__taxonomy"))

Expand Down
Loading

0 comments on commit 46556f9

Please sign in to comment.