Merge branch 'master' into final-dj52
This commit is contained in:
@@ -34,6 +34,7 @@ FEATURES["ENABLE_MKTG_SITE"] = False
|
||||
INSTALLED_APPS.extend(
|
||||
[
|
||||
"cms.djangoapps.contentstore.apps.ContentstoreConfig",
|
||||
'cms.djangoapps.modulestore_migrator',
|
||||
"cms.djangoapps.course_creators",
|
||||
"cms.djangoapps.xblock_config.apps.XBlockConfig",
|
||||
"lms.djangoapps.lti_provider",
|
||||
|
||||
@@ -3523,11 +3523,77 @@ paths:
|
||||
in: path
|
||||
required: true
|
||||
type: string
|
||||
/dashboard/v0/programs/:
|
||||
get:
|
||||
operationId: dashboard_v0_programs_list
|
||||
description: |-
|
||||
For a learner, get list of enrolled programs with progress.
|
||||
If an enterprise UUID ias provided, filter out all non-enterprise enrollments for the learner.
|
||||
|
||||
**Example Request**
|
||||
|
||||
GET /api/dashboard/v1/programs/{enterprise_uuid}/
|
||||
|
||||
**Parameters**
|
||||
|
||||
* `enterprise_uuid`: UUID of an enterprise customer.
|
||||
|
||||
**Example Response**
|
||||
|
||||
[
|
||||
{
|
||||
"uuid": "ff41a5eb-2a73-4933-8e80-a1c66068ed2c",
|
||||
"title": "Demonstration Program",
|
||||
"type": "MicroMasters",
|
||||
"banner_image": {
|
||||
"large": {
|
||||
"url": "http://example.com/images/foo.large.jpg",
|
||||
"width": 1440,
|
||||
"height": 480
|
||||
},
|
||||
"medium": {
|
||||
"url": "http://example.com/images/foo.medium.jpg",
|
||||
"width": 726,
|
||||
"height": 242
|
||||
},
|
||||
"small": {
|
||||
"url": "http://example.com/images/foo.small.jpg",
|
||||
"width": 435,
|
||||
"height": 145
|
||||
},
|
||||
"x-small": {
|
||||
"url": "http://example.com/images/foo.x-small.jpg",
|
||||
"width": 348,
|
||||
"height": 116
|
||||
}
|
||||
},
|
||||
"authoring_organizations": [
|
||||
{
|
||||
"key": "example"
|
||||
}
|
||||
],
|
||||
"progress": {
|
||||
"uuid": "ff41a5eb-2a73-4933-8e80-a1c66068ed2c",
|
||||
"completed": 0,
|
||||
"in_progress": 0,
|
||||
"not_started": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- dashboard
|
||||
parameters: []
|
||||
/dashboard/v0/programs/{enterprise_uuid}/:
|
||||
get:
|
||||
operationId: dashboard_v0_programs_read
|
||||
summary: For an enterprise learner, get list of enrolled programs with progress.
|
||||
description: |-
|
||||
For a learner, get list of enrolled programs with progress.
|
||||
If an enterprise UUID ias provided, filter out all non-enterprise enrollments for the learner.
|
||||
|
||||
**Example Request**
|
||||
|
||||
GET /api/dashboard/v1/programs/{enterprise_uuid}/
|
||||
@@ -6446,11 +6512,77 @@ paths:
|
||||
tags:
|
||||
- learner_home
|
||||
parameters: []
|
||||
/learner_home/v1/programs/:
|
||||
get:
|
||||
operationId: learner_home_v1_programs_list
|
||||
description: |-
|
||||
For a learner, get list of enrolled programs with progress.
|
||||
If an enterprise UUID ias provided, filter out all non-enterprise enrollments for the learner.
|
||||
|
||||
**Example Request**
|
||||
|
||||
GET /api/dashboard/v1/programs/{enterprise_uuid}/
|
||||
|
||||
**Parameters**
|
||||
|
||||
* `enterprise_uuid`: UUID of an enterprise customer.
|
||||
|
||||
**Example Response**
|
||||
|
||||
[
|
||||
{
|
||||
"uuid": "ff41a5eb-2a73-4933-8e80-a1c66068ed2c",
|
||||
"title": "Demonstration Program",
|
||||
"type": "MicroMasters",
|
||||
"banner_image": {
|
||||
"large": {
|
||||
"url": "http://example.com/images/foo.large.jpg",
|
||||
"width": 1440,
|
||||
"height": 480
|
||||
},
|
||||
"medium": {
|
||||
"url": "http://example.com/images/foo.medium.jpg",
|
||||
"width": 726,
|
||||
"height": 242
|
||||
},
|
||||
"small": {
|
||||
"url": "http://example.com/images/foo.small.jpg",
|
||||
"width": 435,
|
||||
"height": 145
|
||||
},
|
||||
"x-small": {
|
||||
"url": "http://example.com/images/foo.x-small.jpg",
|
||||
"width": 348,
|
||||
"height": 116
|
||||
}
|
||||
},
|
||||
"authoring_organizations": [
|
||||
{
|
||||
"key": "example"
|
||||
}
|
||||
],
|
||||
"progress": {
|
||||
"uuid": "ff41a5eb-2a73-4933-8e80-a1c66068ed2c",
|
||||
"completed": 0,
|
||||
"in_progress": 0,
|
||||
"not_started": 2
|
||||
}
|
||||
}
|
||||
]
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
tags:
|
||||
- learner_home
|
||||
parameters: []
|
||||
/learner_home/v1/programs/{enterprise_uuid}/:
|
||||
get:
|
||||
operationId: learner_home_v1_programs_read
|
||||
summary: For an enterprise learner, get list of enrolled programs with progress.
|
||||
description: |-
|
||||
For a learner, get list of enrolled programs with progress.
|
||||
If an enterprise UUID ias provided, filter out all non-enterprise enrollments for the learner.
|
||||
|
||||
**Example Request**
|
||||
|
||||
GET /api/dashboard/v1/programs/{enterprise_uuid}/
|
||||
@@ -6912,7 +7044,7 @@ paths:
|
||||
django settings. This is a temporary change as a part of the migration of some legacy
|
||||
pages to MFEs. This is a temporary compatibility layer which will eventually be deprecated.
|
||||
|
||||
See [Link to DEPR ticket] for more details. todo: add link
|
||||
See [DEPR ticket](https://github.com/openedx/edx-platform/issues/37210) for more details.
|
||||
|
||||
The compatability means that settings from the legacy locations will continue to work but
|
||||
the settings listed below in the `_get_legacy_config` function should be added to the MFE
|
||||
|
||||
@@ -35,6 +35,128 @@ from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase #
|
||||
from xmodule.modulestore.tests.factories import CourseFactory # lint-amnesty, pylint: disable=wrong-import-order
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CertificateTaskViewTests(SharedModuleStoreTestCase):
|
||||
"""Tests for the certificate panel of the instructor dash. """
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
"""
|
||||
Set up the test class with a test course and instructor dashboard URL.
|
||||
"""
|
||||
super().setUpClass()
|
||||
cls.course = CourseFactory.create()
|
||||
cls.url = reverse(
|
||||
'instructor_dashboard',
|
||||
kwargs={'course_id': str(cls.course.id)}
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Set up test users and enable certificate generation configuration.
|
||||
"""
|
||||
super().setUp()
|
||||
self.user = UserFactory.create()
|
||||
self.global_staff = GlobalStaffFactory()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
|
||||
# Need to clear the cache for model-based configuration
|
||||
cache.clear()
|
||||
|
||||
# Enable the certificate generation feature
|
||||
CertificateGenerationConfiguration.objects.create(enabled=True)
|
||||
|
||||
def _login_as(self, role):
|
||||
"""
|
||||
Log in the test client as the specified user role.
|
||||
"""
|
||||
user_map = {
|
||||
"user": self.user.username,
|
||||
"instructor": self.instructor.username,
|
||||
"global_staff": self.global_staff.username
|
||||
}
|
||||
self.client.login(username=user_map.get(role, "user"), password=self.TEST_PASSWORD)
|
||||
|
||||
def _get_url(self, action):
|
||||
"""
|
||||
Build the unified certificate task URL for the given action.
|
||||
"""
|
||||
return reverse("certificate_task", kwargs={"course_id": self.course.id, "action": action})
|
||||
|
||||
def _assert_redirects_to_instructor_dash(self, response):
|
||||
"""Check that the response redirects to the certificates section. """
|
||||
expected_redirect = reverse(
|
||||
'instructor_dashboard',
|
||||
kwargs={'course_id': str(self.course.id)}
|
||||
)
|
||||
expected_redirect += '#view-certificates'
|
||||
self.assertRedirects(response, expected_redirect)
|
||||
|
||||
@ddt.data(True, False)
|
||||
def test_certificate_generation_enable(self, is_enabled):
|
||||
"""
|
||||
Test enabling or disabling self-generated certificates as global staff.
|
||||
"""
|
||||
self._login_as("global_staff")
|
||||
|
||||
params = {"certificates-enabled": "true" if is_enabled else "false"}
|
||||
response = self.client.post(
|
||||
self._get_url("toggle"),
|
||||
data=params
|
||||
)
|
||||
|
||||
# Expect a redirect back to the instructor dashboard
|
||||
self._assert_redirects_to_instructor_dash(response)
|
||||
|
||||
# Expect that certificate generation is now enabled for the course
|
||||
actual_enabled = certs_api.has_self_generated_certificates_enabled(str(self.course.id))
|
||||
assert is_enabled == actual_enabled
|
||||
|
||||
@ddt.data("user", "instructor", "global_staff")
|
||||
def test_certificate_generation(self, role):
|
||||
"""
|
||||
Test permission-based access to certificate generation by role.
|
||||
"""
|
||||
self._login_as(role)
|
||||
response = self.client.post(self._get_url("generate"))
|
||||
actual_status_code = {
|
||||
"user": 403,
|
||||
"instructor": 200,
|
||||
"global_staff": 200
|
||||
}
|
||||
assert response.status_code == actual_status_code[role]
|
||||
|
||||
@ddt.data(
|
||||
("downloadable", 200, True, 'Certificate regeneration task has been started. You can view '
|
||||
'the status of the generation task in the "Pending Tasks" section.'),
|
||||
("generating", 400, False, 'Please select certificate statuses that lie with '
|
||||
'in "certificate_statuses" entry in POST data.')
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_certificate_regeneration_status_handling(self, cert_status, expected_status, success, expected_message):
|
||||
"""
|
||||
Test certificate regeneration with valid and invalid certificate statuses.
|
||||
"""
|
||||
# Create a certificate with the given status
|
||||
GeneratedCertificateFactory.create(
|
||||
user=self.user,
|
||||
course_id=self.course.id,
|
||||
status=cert_status,
|
||||
mode='honor'
|
||||
)
|
||||
|
||||
self._login_as("global_staff")
|
||||
response = self.client.post(
|
||||
self._get_url("regenerate"),
|
||||
data={'certificate_statuses': [cert_status]},
|
||||
)
|
||||
|
||||
assert response.status_code == expected_status
|
||||
res_json = response.json()
|
||||
assert res_json.get('success', False) is success
|
||||
assert res_json.get('message') == expected_message
|
||||
|
||||
|
||||
@ddt.ddt
|
||||
class CertificatesInstructorDashTest(SharedModuleStoreTestCase):
|
||||
"""Tests for the certificate panel of the instructor dash. """
|
||||
@@ -138,7 +260,11 @@ class CertificatesInstructorDashTest(SharedModuleStoreTestCase):
|
||||
self.assertContains(response, 'enable-certificates-submit')
|
||||
self.assertNotContains(response, 'Generate Example Certificates')
|
||||
|
||||
@mock.patch.dict(settings.FEATURES, {'CERTIFICATES_HTML_VIEW': True})
|
||||
@mock.patch.dict(settings.FEATURES, {
|
||||
'CERTIFICATES_HTML_VIEW': True,
|
||||
'CERTIFICATES_INSTRUCTOR_GENERATION': False
|
||||
}
|
||||
)
|
||||
def test_buttons_for_html_certs_in_self_paced_course(self):
|
||||
"""
|
||||
Tests `Enable Student-Generated Certificates` button is enabled
|
||||
|
||||
@@ -39,7 +39,7 @@ from opaque_keys.edx.keys import CourseKey, UsageKey
|
||||
from openedx.core.djangoapps.course_groups.cohorts import get_cohort_by_name
|
||||
from rest_framework.exceptions import MethodNotAllowed
|
||||
from rest_framework import serializers, status # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from rest_framework.permissions import IsAdminUser, IsAuthenticated # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from rest_framework.permissions import IsAdminUser, IsAuthenticated, BasePermission # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from rest_framework.response import Response # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from rest_framework.views import APIView # lint-amnesty, pylint: disable=wrong-import-order
|
||||
from submissions import api as sub_api # installed from the edx-submissions repository # lint-amnesty, pylint: disable=wrong-import-order
|
||||
@@ -3402,17 +3402,57 @@ def _instructor_dash_url(course_key, section=None):
|
||||
return url
|
||||
|
||||
|
||||
@require_course_permission(permissions.ENABLE_CERTIFICATE_GENERATION)
|
||||
@require_POST
|
||||
def enable_certificate_generation(request, course_id=None):
|
||||
"""Enable/disable self-generated certificates for a course.
|
||||
class HasCertificateActionPermission(BasePermission):
|
||||
"""
|
||||
DRF permission class to validate course-level certificate task permissions
|
||||
based on the `action` URL parameter.
|
||||
"""
|
||||
|
||||
Once self-generated certificates have been enabled, students
|
||||
who have passed the course will be able to generate certificates.
|
||||
permission_map = {
|
||||
'toggle': permissions.ENABLE_CERTIFICATE_GENERATION,
|
||||
'generate': permissions.START_CERTIFICATE_GENERATION,
|
||||
'regenerate': permissions.START_CERTIFICATE_REGENERATION,
|
||||
}
|
||||
|
||||
Redirects back to the instructor dashboard once the
|
||||
setting has been updated.
|
||||
def has_permission(self, request, view):
|
||||
"""
|
||||
Check whether the user has permission to perform the requested certificate action
|
||||
on the specified course.
|
||||
"""
|
||||
course_id = view.kwargs.get('course_id')
|
||||
action = view.kwargs.get('action')
|
||||
|
||||
if not course_id or not action:
|
||||
return False
|
||||
|
||||
required_perm = self.permission_map.get(action)
|
||||
if required_perm is None:
|
||||
return False
|
||||
|
||||
try:
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
except (ValueError, TypeError):
|
||||
return False
|
||||
|
||||
return request.user.has_perm(required_perm, course_key)
|
||||
|
||||
|
||||
def toggle_certificate_generation(request, course_id):
|
||||
"""
|
||||
Enable or disable student-generated certificates for a course.
|
||||
|
||||
Based on the value of the POST field `certificates-enabled`, this function
|
||||
updates the course setting to allow or prevent students from generating their
|
||||
own certificates. This function assumes that permission checks
|
||||
have already been performed.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming POST request.
|
||||
course_id (str): The course identifier in string format.
|
||||
|
||||
Returns:
|
||||
HttpResponseRedirect: Redirects back to the instructor dashboard
|
||||
(certificates section) after updating the course setting.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
is_enabled = (request.POST.get('certificates-enabled', 'false') == 'true')
|
||||
@@ -3420,6 +3460,167 @@ def enable_certificate_generation(request, course_id=None):
|
||||
return redirect(_instructor_dash_url(course_key, section='certificates'))
|
||||
|
||||
|
||||
def start_certificate_generation(request, course_id):
|
||||
"""
|
||||
Initiates the generation of certificates for all enrolled students in the course.
|
||||
|
||||
This function triggers an asynchronous background task that generates certificates
|
||||
for every student enrolled in the specified course. It returns a response payload
|
||||
containing a confirmation message and the task ID for tracking the task's progress.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The HTTP request object.
|
||||
course_key (CourseKey): The course identifier for which to generate certificates.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with a success message and the task ID.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
task = task_api.generate_certificates_for_students(request, course_key)
|
||||
|
||||
return {
|
||||
"message": _(
|
||||
"Certificate generation task for all students of this course has been started. "
|
||||
"You can view the status of the generation task in the \"Pending Tasks\" section."
|
||||
),
|
||||
"task_id": task.task_id
|
||||
}
|
||||
|
||||
|
||||
def start_certificate_regeneration(request, course_id, certificates_statuses):
|
||||
"""
|
||||
Initiates regeneration of certificates for students based on given certificate statuses.
|
||||
|
||||
This function triggers a background task that regenerates certificates for students
|
||||
whose certificates match the provided list of statuses.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The HTTP request object.
|
||||
course_key (CourseKey): The identifier of the course for which certificates are being regenerated.
|
||||
certificates_statuses (list[str]): A list of certificate statuses to filter the affected certificates.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary with a success message and success status.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
task_api.regenerate_certificates(request, course_key, certificates_statuses)
|
||||
|
||||
return {
|
||||
'message': _(
|
||||
'Certificate regeneration task has been started. '
|
||||
'You can view the status of the generation task in the "Pending Tasks" section.'
|
||||
),
|
||||
'success': True
|
||||
}
|
||||
|
||||
|
||||
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
|
||||
@method_decorator(transaction.non_atomic_requests, name='dispatch')
|
||||
class CertificateTask(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
API endpoint for handling certificate-related administrative tasks for a given course.
|
||||
|
||||
Supported actions:
|
||||
- "toggle": Enable or disable self-generated certificates.
|
||||
- "generate": Initiate certificate generation for all enrolled students.
|
||||
- "regenerate": Regenerate certificates based on selected certificate statuses.
|
||||
|
||||
URL pattern:
|
||||
POST /courses/{course_id}/instructor/api/certificates/{action}/
|
||||
|
||||
The `action` path parameter determines the task to perform.
|
||||
The request must be authenticated and the user must have the appropriate permission for the action.
|
||||
"""
|
||||
permission_classes = [IsAuthenticated, HasCertificateActionPermission]
|
||||
|
||||
@method_decorator(ensure_csrf_cookie)
|
||||
def post(self, request, course_id, action=None):
|
||||
"""
|
||||
Handles POST requests for certificate actions.
|
||||
|
||||
Depending on the `action` parameter, different tasks are performed:
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The HTTP request object.
|
||||
course_id (str): The ID of the course on which to perform the action.
|
||||
action (str, optional): The certificate task to perform. Must be one of:
|
||||
- "toggle": Enable or disable certificates for the course. No additional
|
||||
parameters are required.
|
||||
- "generate": Generate certificates for eligible learners. No additional
|
||||
parameters are required.
|
||||
- "regenerate": Regenerate certificates for learners. Requires an additional
|
||||
parameter in the request body:
|
||||
- `statuses` (list of str): List of certificate statuses to regenerate
|
||||
(e.g., ["downloaded", "issued"]).
|
||||
|
||||
Returns:
|
||||
Response: A DRF Response object containing a success message or error details.
|
||||
If the `action` is invalid, returns HTTP 400 with an error message.
|
||||
|
||||
Example request body for `regenerate` action:
|
||||
{
|
||||
"statuses": ["downloaded", "issued"]
|
||||
}
|
||||
"""
|
||||
if action == "toggle":
|
||||
return self._handle_toggle(request, course_id)
|
||||
elif action == "generate":
|
||||
return self._handle_generate(request, course_id)
|
||||
elif action == "regenerate":
|
||||
return self._handle_regenerate(request, course_id)
|
||||
else:
|
||||
return Response(
|
||||
{"error": f"Invalid action: {action}"},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
def _handle_toggle(self, request, course_id):
|
||||
"""Handle certificate generation toggle."""
|
||||
# TODO: Update this to return a proper API response (e.g., {"enabled": true})
|
||||
return toggle_certificate_generation(request, course_id)
|
||||
|
||||
def _handle_generate(self, request, course_id):
|
||||
"""Handle certificate generation for all students."""
|
||||
payload = start_certificate_generation(request, course_id)
|
||||
return Response(payload, status=status.HTTP_200_OK)
|
||||
|
||||
def _handle_regenerate(self, request, course_id):
|
||||
"""Handle certificate regeneration based on status."""
|
||||
# Validate and extract certificate statuses from the request
|
||||
serializer = CertificateStatusesSerializer(data=request.data)
|
||||
if not serializer.is_valid():
|
||||
return Response(
|
||||
{'message': _(
|
||||
'Please select certificate statuses that '
|
||||
'lie with in "certificate_statuses" entry in POST data.'
|
||||
)},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
statuses = serializer.validated_data['certificate_statuses']
|
||||
payload = start_certificate_regeneration(request, course_id, statuses)
|
||||
return Response(payload, status=status.HTTP_200_OK)
|
||||
|
||||
|
||||
@require_course_permission(permissions.ENABLE_CERTIFICATE_GENERATION)
|
||||
@require_POST
|
||||
def enable_certificate_generation(request, course_id=None):
|
||||
"""
|
||||
View to toggle self-generated certificate availability for a course.
|
||||
|
||||
This endpoint is protected by course-level permission checks and allows
|
||||
enabling or disabling student-generated certificates. The logic is handled
|
||||
by `toggle_certificate_generation`.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming POST request.
|
||||
course_id (str): The course identifier in string format.
|
||||
|
||||
Returns:
|
||||
HttpResponseRedirect: Redirects to the instructor dashboard after update.
|
||||
"""
|
||||
return toggle_certificate_generation(request, course_id)
|
||||
|
||||
|
||||
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
|
||||
class MarkStudentCanSkipEntranceExam(APIView):
|
||||
"""
|
||||
@@ -3471,16 +3672,8 @@ class StartCertificateGeneration(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
Generating certificates for all students enrolled in given course.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
task = task_api.generate_certificates_for_students(request, course_key)
|
||||
message = _('Certificate generation task for all students of this course has been started. '
|
||||
'You can view the status of the generation task in the "Pending Tasks" section.')
|
||||
response_payload = {
|
||||
'message': message,
|
||||
'task_id': task.task_id
|
||||
}
|
||||
|
||||
return JsonResponse(response_payload)
|
||||
payload = start_certificate_generation(request, course_id=course_id)
|
||||
return JsonResponse(payload)
|
||||
|
||||
|
||||
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
|
||||
@@ -3501,7 +3694,6 @@ class StartCertificateRegeneration(DeveloperErrorViewMixin, APIView):
|
||||
"""
|
||||
certificate_statuses 'certificate_statuses' in POST data.
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
serializer = self.serializer_class(data=request.data)
|
||||
|
||||
if not serializer.is_valid():
|
||||
@@ -3511,13 +3703,8 @@ class StartCertificateRegeneration(DeveloperErrorViewMixin, APIView):
|
||||
)
|
||||
|
||||
certificates_statuses = serializer.validated_data['certificate_statuses']
|
||||
task_api.regenerate_certificates(request, course_key, certificates_statuses)
|
||||
response_payload = {
|
||||
'message': _('Certificate regeneration task has been started. '
|
||||
'You can view the status of the generation task in the "Pending Tasks" section.'),
|
||||
'success': True
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
payload = start_certificate_regeneration(request, course_id, certificates_statuses)
|
||||
return JsonResponse(payload)
|
||||
|
||||
|
||||
@method_decorator(cache_control(no_cache=True, no_store=True, must_revalidate=True), name='dispatch')
|
||||
|
||||
@@ -82,6 +82,9 @@ urlpatterns = [
|
||||
# Cohort management
|
||||
path('add_users_to_cohorts', api.AddUsersToCohorts.as_view(), name='add_users_to_cohorts'),
|
||||
|
||||
# Unified endpoint for Certificate tasks
|
||||
path('certificate_task/<action>', api.CertificateTask.as_view(), name='certificate_task'),
|
||||
|
||||
# Certificates
|
||||
path('enable_certificate_generation', api.enable_certificate_generation, name='enable_certificate_generation'),
|
||||
path('start_certificate_generation', api.StartCertificateGeneration.as_view(), name='start_certificate_generation'),
|
||||
|
||||
@@ -385,16 +385,16 @@ def _section_certificates(course):
|
||||
CertificateGenerationHistory.objects.filter(course_id=course.id).order_by("-created"),
|
||||
'urls': {
|
||||
'enable_certificate_generation': reverse(
|
||||
'enable_certificate_generation',
|
||||
kwargs={'course_id': course.id}
|
||||
'certificate_task',
|
||||
kwargs={'course_id': course.id, "action": "toggle"}
|
||||
),
|
||||
'start_certificate_generation': reverse(
|
||||
'start_certificate_generation',
|
||||
kwargs={'course_id': course.id}
|
||||
'certificate_task',
|
||||
kwargs={'course_id': course.id, "action": "generate"}
|
||||
),
|
||||
'start_certificate_regeneration': reverse(
|
||||
'start_certificate_regeneration',
|
||||
kwargs={'course_id': course.id}
|
||||
'certificate_task',
|
||||
kwargs={'course_id': course.id, "action": "regenerate"}
|
||||
),
|
||||
'list_instructor_tasks_url': reverse(
|
||||
'list_instructor_tasks',
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|
||||
@media only screen and (max-width: 767px) {
|
||||
.survey-table .survey-option .visible-mobile-only {
|
||||
width: calc(100% - 21px) !important;
|
||||
width: calc(100% - 54px) !important;
|
||||
}
|
||||
|
||||
.survey-percentage .percentage {
|
||||
|
||||
Reference in New Issue
Block a user