SOL-1288
This commit is contained in:
@@ -65,6 +65,8 @@ from instructor.views.api import require_finance_admin
|
||||
from instructor.tests.utils import FakeContentTask, FakeEmail, FakeEmailInfo
|
||||
from instructor.views.api import _split_input_list, common_exceptions_400, generate_unique_password
|
||||
from instructor_task.api_helper import AlreadyRunningError
|
||||
from certificates.tests.factories import GeneratedCertificateFactory
|
||||
from certificates.models import CertificateStatuses
|
||||
|
||||
from openedx.core.djangoapps.course_groups.cohorts import set_course_cohort_settings
|
||||
|
||||
@@ -3855,6 +3857,118 @@ class TestDueDateExtensions(SharedModuleStoreTestCase, LoginEnrollmentTestCase):
|
||||
self.user1.profile.name, self.user1.username)})
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
class TestCourseIssuedCertificatesData(SharedModuleStoreTestCase):
|
||||
"""
|
||||
Test data dumps for issued certificates.
|
||||
"""
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(TestCourseIssuedCertificatesData, cls).setUpClass()
|
||||
cls.course = CourseFactory.create()
|
||||
|
||||
def setUp(self):
|
||||
super(TestCourseIssuedCertificatesData, self).setUp()
|
||||
self.instructor = InstructorFactory(course_key=self.course.id)
|
||||
self.client.login(username=self.instructor.username, password='test')
|
||||
|
||||
def generate_certificate(self, course_id, mode, status):
|
||||
"""
|
||||
Generate test certificate
|
||||
"""
|
||||
test_user = UserFactory()
|
||||
GeneratedCertificateFactory.create(
|
||||
user=test_user,
|
||||
course_id=course_id,
|
||||
mode=mode,
|
||||
status=status
|
||||
)
|
||||
|
||||
def test_certificates_features_against_status(self):
|
||||
"""
|
||||
Test certificates with status 'downloadable' should be in the response.
|
||||
"""
|
||||
url = reverse('get_issued_certificates', kwargs={'course_id': unicode(self.course.id)})
|
||||
# firstly generating downloadable certificates with 'honor' mode
|
||||
certificate_count = 3
|
||||
for __ in xrange(certificate_count):
|
||||
self.generate_certificate(course_id=self.course.id, mode='honor', status=CertificateStatuses.generating)
|
||||
|
||||
response = self.client.get(url)
|
||||
res_json = json.loads(response.content)
|
||||
self.assertIn('certificates', res_json)
|
||||
self.assertEqual(len(res_json['certificates']), 0)
|
||||
|
||||
# Certificates with status 'downloadable' should be in response.
|
||||
self.generate_certificate(course_id=self.course.id, mode='honor', status=CertificateStatuses.downloadable)
|
||||
response = self.client.get(url)
|
||||
res_json = json.loads(response.content)
|
||||
self.assertIn('certificates', res_json)
|
||||
self.assertEqual(len(res_json['certificates']), 1)
|
||||
|
||||
def test_certificates_features_group_by_mode(self):
|
||||
"""
|
||||
Test for certificate csv features against mode. Certificates should be group by 'mode' in reponse.
|
||||
"""
|
||||
url = reverse('get_issued_certificates', kwargs={'course_id': unicode(self.course.id)})
|
||||
# firstly generating downloadable certificates with 'honor' mode
|
||||
certificate_count = 3
|
||||
for __ in xrange(certificate_count):
|
||||
self.generate_certificate(course_id=self.course.id, mode='honor', status=CertificateStatuses.downloadable)
|
||||
|
||||
response = self.client.get(url)
|
||||
res_json = json.loads(response.content)
|
||||
self.assertIn('certificates', res_json)
|
||||
self.assertEqual(len(res_json['certificates']), 1)
|
||||
|
||||
# retrieve the first certificate from the list, there should be 3 certificates for 'honor' mode.
|
||||
certificate = res_json['certificates'][0]
|
||||
self.assertEqual(certificate.get('total_issued_certificate'), 3)
|
||||
self.assertEqual(certificate.get('mode'), 'honor')
|
||||
self.assertEqual(certificate.get('course_id'), str(self.course.id))
|
||||
|
||||
# Now generating downloadable certificates with 'verified' mode
|
||||
for __ in xrange(certificate_count):
|
||||
self.generate_certificate(
|
||||
course_id=self.course.id,
|
||||
mode='verified',
|
||||
status=CertificateStatuses.downloadable
|
||||
)
|
||||
|
||||
response = self.client.get(url)
|
||||
res_json = json.loads(response.content)
|
||||
self.assertIn('certificates', res_json)
|
||||
|
||||
# total certificate count should be 2 for 'verified' mode.
|
||||
self.assertEqual(len(res_json['certificates']), 2)
|
||||
|
||||
# retrieve the second certificate from the list
|
||||
certificate = res_json['certificates'][1]
|
||||
self.assertEqual(certificate.get('total_issued_certificate'), 3)
|
||||
self.assertEqual(certificate.get('mode'), 'verified')
|
||||
|
||||
def test_certificates_features_csv(self):
|
||||
"""
|
||||
Test for certificate csv features.
|
||||
"""
|
||||
url = reverse('get_issued_certificates', kwargs={'course_id': unicode(self.course.id)})
|
||||
url += '?csv=true'
|
||||
# firstly generating downloadable certificates with 'honor' mode
|
||||
certificate_count = 3
|
||||
for __ in xrange(certificate_count):
|
||||
self.generate_certificate(course_id=self.course.id, mode='honor', status=CertificateStatuses.downloadable)
|
||||
|
||||
current_date = datetime.date.today().strftime("%B %d, %Y")
|
||||
response = self.client.get(url)
|
||||
self.assertEqual(response['Content-Type'], 'text/csv')
|
||||
self.assertEqual(response['Content-Disposition'], 'attachment; filename={0}'.format('issued_certificates.csv'))
|
||||
self.assertEqual(
|
||||
response.content.strip(),
|
||||
'"CourseID","Certificate Type","Total Certificates Issued","Date Report Run"\r\n"'
|
||||
+ str(self.course.id) + '","honor","3","' + current_date + '"'
|
||||
)
|
||||
|
||||
|
||||
@attr('shard_1')
|
||||
@override_settings(REGISTRATION_CODE_LENGTH=8)
|
||||
class TestCourseRegistrationCodes(SharedModuleStoreTestCase):
|
||||
|
||||
@@ -136,9 +136,9 @@ class CertificatesInstructorDashTest(SharedModuleStoreTestCase):
|
||||
"""Check that the certificates section is visible on the instructor dash. """
|
||||
response = self.client.get(self.url)
|
||||
if is_visible:
|
||||
self.assertContains(response, "Certificates")
|
||||
self.assertContains(response, "Student-Generated Certificates")
|
||||
else:
|
||||
self.assertNotContains(response, "Certificates")
|
||||
self.assertNotContains(response, "Student-Generated Certificates")
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _certificate_status(self, description, status):
|
||||
|
||||
@@ -1089,6 +1089,45 @@ def re_validate_invoice(obj_invoice):
|
||||
return JsonResponse({'message': message})
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
def get_issued_certificates(request, course_id): # pylint: disable=invalid-name
|
||||
"""
|
||||
Responds with JSON if CSV is not required. contains a list of issued certificates.
|
||||
Arguments:
|
||||
course_id
|
||||
Returns:
|
||||
{"certificates": [{course_id: xyz, mode: 'honor'}, ...]}
|
||||
|
||||
"""
|
||||
course_key = CourseKey.from_string(course_id)
|
||||
csv_required = request.GET.get('csv', 'false')
|
||||
|
||||
query_features = ['course_id', 'mode', 'total_issued_certificate', 'report_run_date']
|
||||
query_features_names = [
|
||||
('course_id', _('CourseID')),
|
||||
('mode', _('Certificate Type')),
|
||||
('total_issued_certificate', _('Total Certificates Issued')),
|
||||
('report_run_date', _('Date Report Run'))
|
||||
]
|
||||
certificates_data = instructor_analytics.basic.issued_certificates(course_key, query_features)
|
||||
if csv_required.lower() == 'true':
|
||||
__, data_rows = instructor_analytics.csvs.format_dictlist(certificates_data, query_features)
|
||||
return instructor_analytics.csvs.create_csv_response(
|
||||
'issued_certificates.csv',
|
||||
[col_header for __, col_header in query_features_names],
|
||||
data_rows
|
||||
)
|
||||
else:
|
||||
response_payload = {
|
||||
'certificates': certificates_data,
|
||||
'queried_features': query_features,
|
||||
'feature_names': dict(query_features_names)
|
||||
}
|
||||
return JsonResponse(response_payload)
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
@require_level('staff')
|
||||
|
||||
@@ -23,6 +23,8 @@ urlpatterns = patterns(
|
||||
'instructor.views.api.get_grading_config', name="get_grading_config"),
|
||||
url(r'^get_students_features(?P<csv>/csv)?$',
|
||||
'instructor.views.api.get_students_features', name="get_students_features"),
|
||||
url(r'^get_issued_certificates/$',
|
||||
'instructor.views.api.get_issued_certificates', name="get_issued_certificates"),
|
||||
url(r'^get_students_who_may_enroll$',
|
||||
'instructor.views.api.get_students_who_may_enroll', name="get_students_who_may_enroll"),
|
||||
url(r'^get_user_invoice_preference$',
|
||||
|
||||
@@ -504,6 +504,9 @@ def _section_data_download(course, access):
|
||||
'get_problem_responses_url': reverse('get_problem_responses', kwargs={'course_id': unicode(course_key)}),
|
||||
'get_grading_config_url': reverse('get_grading_config', kwargs={'course_id': unicode(course_key)}),
|
||||
'get_students_features_url': reverse('get_students_features', kwargs={'course_id': unicode(course_key)}),
|
||||
'get_issued_certificates_url': reverse(
|
||||
'get_issued_certificates', kwargs={'course_id': unicode(course_key)}
|
||||
),
|
||||
'get_students_who_may_enroll_url': reverse(
|
||||
'get_students_who_may_enroll', kwargs={'course_id': unicode(course_key)}
|
||||
),
|
||||
|
||||
@@ -4,6 +4,7 @@ Student and course analytics.
|
||||
Serve miscellaneous course and student data
|
||||
"""
|
||||
import json
|
||||
import datetime
|
||||
from shoppingcart.models import (
|
||||
PaidCourseRegistration, CouponRedemption, CourseRegCodeItem,
|
||||
RegistrationCodeRedemption, CourseRegistrationCodeInvoiceItem
|
||||
@@ -19,6 +20,9 @@ from microsite_configuration import microsite
|
||||
from student.models import CourseEnrollmentAllowed
|
||||
from edx_proctoring.api import get_all_exam_attempts
|
||||
from courseware.models import StudentModule
|
||||
from certificates.models import GeneratedCertificate
|
||||
from django.db.models import Count
|
||||
from certificates.models import CertificateStatuses
|
||||
|
||||
|
||||
STUDENT_FEATURES = ('id', 'username', 'first_name', 'last_name', 'is_staff', 'email')
|
||||
@@ -38,6 +42,7 @@ SALE_ORDER_FEATURES = ('id', 'company_name', 'company_contact_name', 'company_co
|
||||
AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES
|
||||
COURSE_REGISTRATION_FEATURES = ('code', 'course_id', 'created_by', 'created_at', 'is_valid')
|
||||
COUPON_FEATURES = ('code', 'course_id', 'percentage_discount', 'description', 'expiration_date', 'is_active')
|
||||
CERTIFICATE_FEATURES = ('course_id', 'mode', 'status', 'grade', 'created_date', 'is_active', 'error_reason')
|
||||
|
||||
UNAVAILABLE = "[unavailable]"
|
||||
|
||||
@@ -162,6 +167,32 @@ def sale_record_features(course_id, features):
|
||||
return [sale_records_info(sale, features) for sale in sales]
|
||||
|
||||
|
||||
def issued_certificates(course_key, features):
|
||||
"""
|
||||
Return list of issued certificates as dictionaries against the given course key.
|
||||
|
||||
issued_certificates(course_key, features)
|
||||
would return [
|
||||
{course_id: 'abc', 'total_issued_certificate': '5', 'mode': 'honor'}
|
||||
{course_id: 'abc', 'total_issued_certificate': '10', 'mode': 'verified'}
|
||||
{course_id: 'abc', 'total_issued_certificate': '15', 'mode': 'Professional Education'}
|
||||
]
|
||||
"""
|
||||
|
||||
report_run_date = datetime.date.today().strftime("%B %d, %Y")
|
||||
certificate_features = [x for x in CERTIFICATE_FEATURES if x in features]
|
||||
generated_certificates = list(GeneratedCertificate.objects.filter(
|
||||
course_id=course_key,
|
||||
status=CertificateStatuses.downloadable
|
||||
).values(*certificate_features).annotate(total_issued_certificate=Count('mode')))
|
||||
|
||||
# Report run date
|
||||
for data in generated_certificates:
|
||||
data['report_run_date'] = report_run_date
|
||||
|
||||
return generated_certificates
|
||||
|
||||
|
||||
def enrolled_students_features(course_key, features):
|
||||
"""
|
||||
Return list of student features as dictionaries.
|
||||
|
||||
@@ -11,12 +11,66 @@ std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, argum
|
||||
PendingInstructorTasks = -> window.InstructorDashboard.util.PendingInstructorTasks
|
||||
ReportDownloads = -> window.InstructorDashboard.util.ReportDownloads
|
||||
|
||||
# Data Download Certificate issued
|
||||
class @DataDownload_Certificate
|
||||
constructor: (@$container) ->
|
||||
# gather elements
|
||||
@$list_issued_certificate_table_btn = @$container.find("input[name='issued-certificates-list']'")
|
||||
@$list_issued_certificate_csv_btn = @$container.find("input[name='issued-certificates-csv']'")
|
||||
@$certificate_display_table = @$container.find '.certificate-data-display-table'
|
||||
@$certificates_request_response_error = @$container.find '.issued-certificates-error.request-response-error'
|
||||
|
||||
|
||||
@$list_issued_certificate_table_btn.click (e) =>
|
||||
url = @$list_issued_certificate_table_btn.data 'endpoint'
|
||||
# Dynamically generate slickgrid table for displaying issued certificate information.
|
||||
@clear_ui()
|
||||
@$certificate_display_table.text gettext('Loading data...')
|
||||
# fetch user list
|
||||
$.ajax
|
||||
type: 'POST'
|
||||
url: url
|
||||
error: (std_ajax_err) =>
|
||||
@clear_ui()
|
||||
@$certificates_request_response_error.text gettext("Error getting issued certificates list.")
|
||||
$(".issued_certificates .issued-certificates-error.msg-error").css({"display":"block"})
|
||||
success: (data) =>
|
||||
@clear_ui()
|
||||
# display on a SlickGrid
|
||||
options =
|
||||
enableCellNavigation: true
|
||||
enableColumnReorder: false
|
||||
forceFitColumns: true
|
||||
rowHeight: 35
|
||||
|
||||
columns = ({id: feature, field: feature, name: data.feature_names[feature]} for feature in data.queried_features)
|
||||
grid_data = data.certificates
|
||||
|
||||
$table_placeholder = $ '<div/>', class: 'slickgrid'
|
||||
@$certificate_display_table.append $table_placeholder
|
||||
new Slick.Grid($table_placeholder, grid_data, columns, options)
|
||||
|
||||
@$list_issued_certificate_csv_btn.click (e) =>
|
||||
@clear_ui()
|
||||
url = @$list_issued_certificate_csv_btn.data 'endpoint'
|
||||
location.href = url + '?csv=true'
|
||||
|
||||
clear_ui: ->
|
||||
# Clear any generated tables, warning messages, etc of certificates.
|
||||
@$certificate_display_table.empty()
|
||||
@$certificates_request_response_error.empty()
|
||||
$(".issued-certificates-error.msg-error").css({"display":"none"})
|
||||
|
||||
# Data Download Section
|
||||
class DataDownload
|
||||
constructor: (@$section) ->
|
||||
# attach self to html so that instructor_dashboard.coffee can find
|
||||
# this object to call event handlers like 'onClickTitle'
|
||||
@$section.data 'wrapper', @
|
||||
|
||||
# isolate # initialize DataDownload_Certificate subsection
|
||||
new DataDownload_Certificate @$section.find '.issued_certificates'
|
||||
|
||||
# gather elements
|
||||
@$list_studs_btn = @$section.find("input[name='list-profiles']'")
|
||||
@$list_studs_csv_btn = @$section.find("input[name='list-profiles-csv']'")
|
||||
@@ -34,7 +88,7 @@ class DataDownload
|
||||
@$download_display_text = @$download.find '.data-display-text'
|
||||
@$download_request_response_error = @$download.find '.request-response-error'
|
||||
@$reports = @$section.find '.reports-download-container'
|
||||
@$download_display_table = @$reports.find '.data-display-table'
|
||||
@$download_display_table = @$reports.find '.profile-data-display-table'
|
||||
@$reports_request_response = @$reports.find '.request-response'
|
||||
@$reports_request_response_error = @$reports.find '.request-response-error'
|
||||
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
<div class="issued_certificates">
|
||||
<p>${_("Click to list certificates that are issued for this course:")}</p>
|
||||
<span>
|
||||
<input type="button" name="issued-certificates-list" value="View Certificates Issued" >
|
||||
<input type="button" name="issued-certificates-csv" value="Download CSV of Certificates Issued" >
|
||||
</span>
|
||||
<div class="data-display-table certificate-data-display-table" id="data-issued-certificates-table"></div>
|
||||
<div class="issued-certificates-error request-response-error msg msg-error copy"></div>
|
||||
</div>
|
||||
@@ -0,0 +1,67 @@
|
||||
/*global define */
|
||||
define(['jquery', 'coffee/src/instructor_dashboard/data_download', 'common/js/spec_helpers/ajax_helpers', 'slick.grid'],
|
||||
function ($, DataDownload, AjaxHelpers) {
|
||||
'use strict';
|
||||
describe("edx.instructor_dashboard.data_download.DataDownload_Certificate", function() {
|
||||
var url, data_download_certificate;
|
||||
|
||||
beforeEach(function() {
|
||||
loadFixtures('js/fixtures/instructor_dashboard/data_download.html');
|
||||
data_download_certificate = new window.DataDownload_Certificate($('.issued_certificates'));
|
||||
url = '/courses/PU/FSc/2014_T4/instructor/api/get_issued_certificates';
|
||||
data_download_certificate.$list_issued_certificate_table_btn.data('endpoint', url);
|
||||
});
|
||||
|
||||
it('show data on success callback', function() {
|
||||
// Spy on AJAX requests
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
var data = {
|
||||
'certificates': [{'course_id':'xyz_test', 'mode':'honor'}],
|
||||
'queried_features': ['course_id', 'mode'],
|
||||
'feature_names': { 'course_id': 'Course ID', 'mode': ' Mode'}
|
||||
};
|
||||
|
||||
data_download_certificate.$list_issued_certificate_table_btn.click();
|
||||
AjaxHelpers.expectJsonRequest(requests, 'POST', url);
|
||||
|
||||
// Simulate a success response from the server
|
||||
AjaxHelpers.respondWithJson(requests, data);
|
||||
expect(data_download_certificate.$certificate_display_table.html()
|
||||
.indexOf('Course ID') !== -1).toBe(true);
|
||||
expect(data_download_certificate.$certificate_display_table.html()
|
||||
.indexOf('Mode') !== -1).toBe(true);
|
||||
expect(data_download_certificate.$certificate_display_table.html()
|
||||
.indexOf('xyz_test') !== -1).toBe(true);
|
||||
expect(data_download_certificate.$certificate_display_table.html()
|
||||
.indexOf('honor') !== -1).toBe(true);
|
||||
});
|
||||
|
||||
it('show error on failure callback', function() {
|
||||
// Spy on AJAX requests
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
|
||||
data_download_certificate.$list_issued_certificate_table_btn.click();
|
||||
// Simulate a error response from the server
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
expect(data_download_certificate.$certificates_request_response_error.text())
|
||||
.toEqual('Error getting issued certificates list.');
|
||||
});
|
||||
|
||||
it('error should be clear from UI on success callback', function() {
|
||||
var requests = AjaxHelpers.requests(this);
|
||||
data_download_certificate.$list_issued_certificate_table_btn.click();
|
||||
|
||||
// Simulate a error response from the server
|
||||
AjaxHelpers.respondWithError(requests);
|
||||
expect(data_download_certificate.$certificates_request_response_error.text())
|
||||
.toEqual('Error getting issued certificates list.');
|
||||
|
||||
// Simulate a success response from the server
|
||||
data_download_certificate.$list_issued_certificate_table_btn.click();
|
||||
AjaxHelpers.expectJsonRequest(requests, 'POST', url);
|
||||
|
||||
expect(data_download_certificate.$certificates_request_response_error.text())
|
||||
.not.toEqual('Error getting issued certificates list');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,7 @@
|
||||
'codemirror': 'xmodule_js/common_static/js/vendor/CodeMirror/codemirror',
|
||||
'jquery': 'xmodule_js/common_static/js/vendor/jquery.min',
|
||||
'jquery.ui': 'xmodule_js/common_static/js/vendor/jquery-ui.min',
|
||||
'jquery.eventDrag': 'xmodule_js/common_static/js/vendor/jquery.event.drag-2.2',
|
||||
'jquery.flot': 'xmodule_js/common_static/js/vendor/flot/jquery.flot.min',
|
||||
'jquery.form': 'xmodule_js/common_static/js/vendor/jquery.form',
|
||||
'jquery.markitup': 'xmodule_js/common_static/js/vendor/markitup/jquery.markitup',
|
||||
@@ -89,7 +90,9 @@
|
||||
'annotator_1.2.9': 'xmodule_js/common_static/js/vendor/edxnotes/annotator-full.min',
|
||||
|
||||
// Common edx utils
|
||||
'common/js/utils/edx.utils.validate': 'xmodule_js/common_static/common/js/utils/edx.utils.validate'
|
||||
'common/js/utils/edx.utils.validate': 'xmodule_js/common_static/common/js/utils/edx.utils.validate',
|
||||
'slick.grid': 'xmodule_js/common_static/js/vendor/slick.grid',
|
||||
'slick.core': 'xmodule_js/common_static/js/vendor/slick.core'
|
||||
},
|
||||
shim: {
|
||||
'gettext': {
|
||||
|
||||
@@ -65,6 +65,9 @@ lib_paths:
|
||||
- xmodule_js/common_static/js/vendor/moment.min.js
|
||||
- xmodule_js/common_static/js/vendor/moment-with-locales.min.js
|
||||
- xmodule_js/common_static/common/js/utils/edx.utils.validate.js
|
||||
- xmodule_js/common_static/js/vendor/slick.core.js
|
||||
- xmodule_js/common_static/js/vendor/slick.grid.js
|
||||
- xmodule_js/common_static/js/vendor/jquery.event.drag-2.2.js
|
||||
|
||||
# Paths to source JavaScript files
|
||||
src_paths:
|
||||
|
||||
@@ -52,11 +52,21 @@
|
||||
<input type="button" name="list-problem-responses-csv" value="${_("Download a CSV of problem responses")}" data-endpoint="${ section_data['get_problem_responses_url'] }" data-csv="true">
|
||||
</p>
|
||||
|
||||
<div class="issued_certificates">
|
||||
<p>${_("Click to list certificates that are issued for this course:")}</p>
|
||||
<span>
|
||||
<input type="button" name="issued-certificates-list" value="${_("View Certificates Issued")}" data-csv="false" data-endpoint="${ section_data['get_issued_certificates_url'] }">
|
||||
<input type="button" name="issued-certificates-csv" value="${_("Download CSV of Certificates Issued")}" data-csv="true" data-endpoint="${ section_data['get_issued_certificates_url'] }">
|
||||
</span>
|
||||
<div class="data-display-table certificate-data-display-table" id="data-issued-certificates-table"></div>
|
||||
<div class="issued-certificates-error request-response-error msg msg-error copy"></div>
|
||||
</div>
|
||||
|
||||
% if not disable_buttons:
|
||||
<p>${_("For smaller courses, click to list profile information for enrolled students directly on this page:")}</p>
|
||||
<p><input type="button" name="list-profiles" value="${_("List enrolled students' profile information")}" data-endpoint="${ section_data['get_students_features_url'] }"></p>
|
||||
%endif
|
||||
<div class="data-display-table" id="data-student-profiles-table"></div>
|
||||
<div class="data-display-table profile-data-display-table" id="data-student-profiles-table"></div>
|
||||
|
||||
%if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
|
||||
<p>${_("Click to generate a CSV grade report for all currently enrolled students.")}</p>
|
||||
|
||||
Reference in New Issue
Block a user