Commit 1e96c81d by Matt Drayer

Merge pull request #10901 from edx/saleem-latif/SOL-1418

saleem-latif/SOL-1418: Revised Generate Certificates Section and added Certificate Generation UI
parents 7253e3c8 3134a761
......@@ -673,7 +673,6 @@ class CertificatesTest(BaseInstructorDashboardTest):
self.certificates_section.add_certificate_exception(self.user_name, notes)
self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text)
self.assertIn(notes, self.certificates_section.last_certificate_exception.text)
self.assertIn(str(self.user_id), self.certificates_section.last_certificate_exception.text)
# Verify that added exceptions are also synced with backend
# Revisit Page
......@@ -685,7 +684,6 @@ class CertificatesTest(BaseInstructorDashboardTest):
# validate certificate exception synced with server is visible in certificate exceptions list
self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text)
self.assertIn(notes, self.certificates_section.last_certificate_exception.text)
self.assertIn(str(self.user_id), self.certificates_section.last_certificate_exception.text)
def test_instructor_can_remove_certificate_exception(self):
"""
......@@ -701,13 +699,11 @@ class CertificatesTest(BaseInstructorDashboardTest):
self.certificates_section.add_certificate_exception(self.user_name, notes)
self.assertIn(self.user_name, self.certificates_section.last_certificate_exception.text)
self.assertIn(notes, self.certificates_section.last_certificate_exception.text)
self.assertIn(str(self.user_id), self.certificates_section.last_certificate_exception.text)
# Remove Certificate Exception
self.certificates_section.remove_first_certificate_exception()
self.assertNotIn(self.user_name, self.certificates_section.last_certificate_exception.text)
self.assertNotIn(notes, self.certificates_section.last_certificate_exception.text)
self.assertNotIn(str(self.user_id), self.certificates_section.last_certificate_exception.text)
# Verify that added exceptions are also synced with backend
# Revisit Page
......@@ -719,7 +715,6 @@ class CertificatesTest(BaseInstructorDashboardTest):
# validate certificate exception synced with server is visible in certificate exceptions list
self.assertNotIn(self.user_name, self.certificates_section.last_certificate_exception.text)
self.assertNotIn(notes, self.certificates_section.last_certificate_exception.text)
self.assertNotIn(str(self.user_id), self.certificates_section.last_certificate_exception.text)
def test_error_on_duplicate_certificate_exception(self):
"""
......
......@@ -45,7 +45,6 @@ Eligibility:
then the student will be issued a certificate regardless of his grade,
unless he has allow_certificate set to False.
"""
from datetime import datetime
import json
import logging
import uuid
......@@ -88,6 +87,12 @@ class CertificateStatuses(object):
unavailable = 'unavailable'
auditing = 'auditing'
readable_statuses = {
downloadable: "already received",
notpassing: "didn't receive",
error: "error states"
}
class CertificateSocialNetworks(object):
"""
......@@ -138,15 +143,26 @@ class CertificateWhitelist(models.Model):
if student:
white_list = white_list.filter(user=student)
result = []
generated_certificates = GeneratedCertificate.objects.filter(
course_id=course_id,
user__in=[exception.user for exception in white_list],
status=CertificateStatuses.downloadable
)
generated_certificates = {
certificate['user']: certificate['created_date']
for certificate in generated_certificates.values('user', 'created_date')
}
for item in white_list:
certificate_generated = generated_certificates.get(item.user.id, '')
result.append({
'id': item.id,
'user_id': item.user.id,
'user_name': unicode(item.user.username),
'user_email': unicode(item.user.email),
'course_id': unicode(item.course_id),
'created': item.created.strftime("%A, %B %d, %Y"),
'created': item.created.strftime("%B %d, %Y"),
'certificate_generated': certificate_generated and certificate_generated.strftime("%B %d, %Y"),
'notes': unicode(item.notes or ''),
})
return result
......@@ -248,6 +264,40 @@ class CertificateGenerationHistory(TimeStampedModel):
instructor_task = models.ForeignKey(InstructorTask)
is_regeneration = models.BooleanField(default=False)
def get_task_name(self):
"""
Return "regenerated" if record corresponds to Certificate Regeneration task, otherwise returns 'generated'
"""
return "regenerated" if self.is_regeneration else "generated"
def get_certificate_generation_candidates(self):
"""
Return the candidates for certificate generation task. It could either be students or certificate statuses
depending upon the nature of certificate generation task. Returned value could be one of the following,
1. "All learners" Certificate Generation task was initiated for all learners of the given course.
2. Comma separated list of certificate statuses, This usually happens when instructor regenerates certificates.
3. "for exceptions", This is the case when instructor generates certificates for white-listed
students.
"""
task_input = self.instructor_task.task_input
try:
task_input_json = json.loads(task_input)
except ValueError:
# if task input is empty, it means certificates were generated for all learners
return "All learners"
# get statuses_to_regenerate from task_input convert statuses to human readable strings and return
statuses = task_input_json.get('statuses_to_regenerate', None)
if statuses:
return ", ".join(
[CertificateStatuses.readable_statuses.get(status, "") for status in statuses]
)
# If statuses_to_regenerate is not present in task_input then, certificate generation task was run to
# generate certificates for white listed students
return "for exceptions"
class Meta(object):
app_label = "certificates"
......
......@@ -37,7 +37,13 @@ from student.models import CourseEnrollment
from shoppingcart.models import Coupon, PaidCourseRegistration, CourseRegCodeItem
from course_modes.models import CourseMode, CourseModesArchive
from student.roles import CourseFinanceAdminRole, CourseSalesAdminRole
from certificates.models import CertificateGenerationConfiguration, CertificateWhitelist, GeneratedCertificate
from certificates.models import (
CertificateGenerationConfiguration,
CertificateWhitelist,
GeneratedCertificate,
CertificateStatuses,
CertificateGenerationHistory,
)
from certificates import api as certs_api
from util.date_utils import get_default_time_display
......@@ -300,6 +306,10 @@ def _section_certificates(course):
)
)
instructor_generation_enabled = settings.FEATURES.get('CERTIFICATES_INSTRUCTOR_GENERATION', False)
certificate_statuses_with_count = {
certificate['status']: certificate['count']
for certificate in GeneratedCertificate.get_unique_statuses(course_key=course.id)
}
return {
'section_key': 'certificates',
......@@ -310,7 +320,9 @@ def _section_certificates(course):
'instructor_generation_enabled': instructor_generation_enabled,
'html_cert_enabled': html_cert_enabled,
'active_certificate': certs_api.get_active_web_certificate(course),
'certificate_statuses': GeneratedCertificate.get_unique_statuses(course_key=course.id),
'certificate_statuses_with_count': certificate_statuses_with_count,
'status': CertificateStatuses,
'certificate_generation_history': CertificateGenerationHistory.objects.filter(course_id=course.id),
'urls': {
'generate_example_certificates': reverse(
'generate_example_certificates',
......
......@@ -21,6 +21,7 @@
user_name: '',
user_email: '',
created: '',
certificate_generated: '',
notes: ''
},
......
......@@ -200,7 +200,7 @@
if (event && event.preventDefault) { event.preventDefault(); }
if (event.currentTarget.files.length === 1) {
this.$el.find(DOM_SELECTORS.upload_csv_button).removeClass('is-disabled')
.addClass('button-blue');
.addClass('btn-blue');
this.$el.find(DOM_SELECTORS.browse_file).val(
event.currentTarget.value.substring(event.currentTarget.value.lastIndexOf("\\") + 1));
}
......
......@@ -81,23 +81,20 @@ var onCertificatesReady = null;
success: function (data) {
$btn_regenerating_certs.attr('disabled','disabled');
if(data.success){
$certificate_regeneration_status.text(data.message).
removeClass('msg-error').addClass('msg-success');
$certificate_regeneration_status.text(data.message).addClass("message");
}
else{
$certificate_regeneration_status.text(data.message).
removeClass('msg-success').addClass("msg-error");
$certificate_regeneration_status.text(data.message).addClass("message");
}
},
error: function(jqXHR) {
try{
var response = JSON.parse(jqXHR.responseText);
$certificate_regeneration_status.text(gettext(response.message)).
removeClass('msg-success').addClass("msg-error");
$certificate_regeneration_status.text(gettext(response.message)).addClass("message");
}catch(error){
$certificate_regeneration_status.
text(gettext('Error while regenerating certificates. Please try again.')).
removeClass('msg-success').addClass("msg-error");
addClass("message");
}
}
});
......
......@@ -98,7 +98,7 @@ define([
{
id: 1, user_id: 1, user_name: 'test1', user_email: 'test1@test.com',
course_id: 'edX/test/course', created: "Thursday, October 29, 2015",
notes: 'test notes for test certificate exception'
notes: 'test notes for test certificate exception', certificate_generated: ''
}
);
......@@ -106,7 +106,7 @@ define([
{
id: 2, user_id: 2, user_name: 'test2', user_email: 'test2@test.com',
course_id: 'edX/test/course', created: "Thursday, October 29, 2015",
notes: 'test notes for test certificate exception'
notes: 'test notes for test certificate exception', certificate_generated: ''
}
);
});
......@@ -142,6 +142,7 @@ define([
user_email: "",
created: "",
notes: "test3 notes",
certificate_generated : '',
new: true}
]
};
......
......@@ -17,8 +17,6 @@ define([
server_error_message: "Error while regenerating certificates. Please try again."
};
var expected = {
error_class: 'msg-error',
success_class: 'msg-success',
url: 'test/url/',
postData : [],
selected_statuses: ['downloadable', 'error'],
......@@ -27,29 +25,37 @@ define([
var select_options = function(option_values){
$.each(option_values, function(index, element){
$("#certificate-statuses option[value=" + element + "]").attr('selected', 'selected');
$("#certificate-regenerating-form input[value=" + element + "]").click();
});
};
beforeEach(function() {
var fixture = '<section id = "certificates"><h2>Regenerate Certificates</h2>' +
var fixture = '<section id="certificates">' +
'<form id="certificate-regenerating-form" method="post" action="' + expected.url + '">' +
' <p id="status-multi-select-tip">Select one or more certificate statuses ' +
' below using your mouse and ctrl or command key.</p>' +
' <select class="multi-select" multiple id="certificate-statuses" ' +
' name="certificate_statuses" aria-describedby="status-multi-select-tip">' +
' <option value="downloadable">Downloadable (2)</option>' +
' <option value="error">Error (2)</option>' +
' <option value="generating">Generating (1)</option>' +
' </select>' +
' <label for="certificate-statuses">' +
' Select certificate statuses that need regeneration and click Regenerate ' +
' Certificates button.' +
' </label>' +
' <input type="button" id="btn-start-regenerating-certificates" value="Regenerate Certificates"' +
' data-endpoint="' + expected.url + '"/>' +
'<p class="under-heading">To regenerate certificates for your course, ' +
' chose the learners who will receive regenerated certificates and click <br> ' +
' Regenerate Certificates.' +
'</p>' +
'<input id="certificate_status_downloadable" type="checkbox" name="certificate_statuses" ' +
' value="downloadable">' +
'<label style="display: inline" for="certificate_status_downloadable">' +
' Regenerate for learners who have already received certificates. (3)' +
'</label><br>' +
'<input id="certificate_status_notpassing" type="checkbox" name="certificate_statuses" ' +
' value="notpassing">' +
'<label style="display: inline" for="certificate_status_notpassing"> ' +
' Regenerate for learners who have not received certificates. (1)' +
'</label><br>' +
'<input id="certificate_status_error" type="checkbox" name="certificate_statuses" ' +
' value="error">' +
'<label style="display: inline" for="certificate_status_error"> ' +
' Regenerate for learners in an error state. (0)' +
'</label><br>' +
'<input type="button" class="btn-blue" id="btn-start-regenerating-certificates" ' +
' value="Regenerate Certificates" data-endpoint="' + expected.url + '">' +
'</form>' +
'<div class="message certificate-regeneration-status"></div></section>';
'<div class="message certificate-regeneration-status"></div>' +
'</section>';
setFixtures(fixture);
onCertificatesReady();
......@@ -87,7 +93,6 @@ define([
$regenerate_certificates_button.click();
AjaxHelpers.respondWithError(requests, 500, {message: MESSAGES.server_error_message});
expect($certificate_regeneration_status).toHaveClass(expected.error_class);
expect($certificate_regeneration_status.text()).toEqual(MESSAGES.server_error_message);
});
......@@ -97,7 +102,6 @@ define([
$regenerate_certificates_button.click();
AjaxHelpers.respondWithError(requests, 400, {message: MESSAGES.error_message});
expect($certificate_regeneration_status).toHaveClass(expected.error_class);
expect($certificate_regeneration_status.text()).toEqual(MESSAGES.error_message);
});
......@@ -107,7 +111,6 @@ define([
$regenerate_certificates_button.click();
AjaxHelpers.respondWithJson(requests, {message: MESSAGES.success_message, success: true});
expect($certificate_regeneration_status).toHaveClass(expected.success_class);
expect($certificate_regeneration_status.text()).toEqual(MESSAGES.success_message);
});
......
......@@ -2105,7 +2105,7 @@ input[name="subject"] {
// --------------------
.instructor-dashboard-wrapper-2 section.idash-section#certificates {
%btn-blue {
.btn-blue {
@extend %btn-primary-blue;
padding: ($baseline/2.5) ($baseline/2);
text-shadow: none;
......@@ -2118,6 +2118,46 @@ input[name="subject"] {
border-top-style: groove;
color: $black;
}
.certificates-wrapper{
.message{
@extend %exception-message;
}
}
p.under-heading {
margin: 12px 0 12px 0;
line-height: 23px;
}
hr.section-divider{
margin: 25px 0;
border-top: 7px solid #646464;
}
.certificate-generation-history{
table{
thead{
tr{
td.task-name{
width: 150px;
}
td.task-date{
width: 200px;
}
}
}
tbody{
tr{
td{
padding: 5px;
vertical-align: middle;
text-align: left;;
}
}
}
}
}
#certificate-white-list-editor {
.certificate-exception-inputs {
......@@ -2134,10 +2174,6 @@ input[name="subject"] {
.message {
@extend %exception-message;
}
.button-blue {
@extend %btn-blue;
}
}
}
......@@ -2155,16 +2191,15 @@ input[name="subject"] {
text-align: left;
color: $gray;
&.date, &.email {
width: 230px;
}
&.user-id {
width: 60px;
&.date{
width: 150px;
}
&.user-name {
width: 150px;
width: 120px;
}
&.user-email {
width: 200px;
}
&.action {
......@@ -2211,10 +2246,6 @@ input[name="subject"] {
}
}
.button-blue {
@extend %btn-blue;
}
.message {
@extend %exception-message;
}
......@@ -2225,10 +2256,6 @@ input[name="subject"] {
border-bottom: 1px groove black;
display: inline-block;
}
p.under-heading {
margin: 12px 0 12px 0;
line-height: 23px;
}
}
.bulk-white-list-exception {
......@@ -2250,10 +2277,6 @@ input[name="subject"] {
.arrow {
font-weight: bold;
}
.button-blue {
@extend %btn-blue;
}
}
}
......
......@@ -6,7 +6,7 @@
<textarea class='notes-field' id="notes" rows="10" placeholder="Free text notes" aria-describedby='notes-field-tip'></textarea>
</div>
<div>
<button type="button" class="button-blue" id="add-exception" ><%= gettext("Add to Exception List") %> </button>
<button type="button" class="btn-blue" id="add-exception" ><%= gettext("Add to Exception List") %> </button>
</div>
<div class='message hidden'></div>
</div>
......@@ -10,7 +10,7 @@
<span id='generate-exception-certificates-radio-all-tip'><%- gettext('Generate a Certificate for all users on the Exception list') %></span>
</label>
</p>
<button id="generate-exception-certificates" class="button-blue" type="button"><%= gettext('Generate Exception Certificates') %></button>
<button id="generate-exception-certificates" class="btn-blue" type="button"><%= gettext('Generate Exception Certificates') %></button>
<br/>
<% if (certificates.length === 0) { %>
<p><%- gettext("No results") %></p>
......@@ -18,9 +18,9 @@
<table>
<thead>
<th class='user-name'><%- gettext("Name") %></th>
<th class='user-id'><%- gettext("User ID") %></th>
<th class='user-email'><%- gettext("User Email") %></th>
<th class='date'><%- gettext("Date Exception Granted") %></th>
<th class='date'><%- gettext("Exception Granted") %></th>
<th class='date'><%- gettext("Certificate Generated") %></th>
<th class='notes'><%- gettext("Notes") %></th>
<th class='action'><%- gettext("Action") %></th>
</thead>
......@@ -30,9 +30,9 @@
%>
<tr>
<td><%- cert.get("user_name") %></td>
<td><%- cert.get("user_id") %></td>
<td><%- cert.get("user_email") %></td>
<td><%- cert.get("created") %></td>
<td><%- cert.get("certificate_generated") %></td>
<td><%- cert.get("notes") %></td>
<td><button class='delete-exception' data-user_id='<%- cert.get("user_id") %>'><%- gettext("Remove from List") %></button></td>
</tr>
......
......@@ -20,7 +20,7 @@ import json
<form id="generate-example-certificates-form" method="post" action="${section_data['urls']['generate_example_certificates']}">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<input type="submit" id="generate-example-certificates-submit" value="${_('Generate Example Certificates')}"/>
<input type="submit" class="btn-blue" id="generate-example-certificates-submit" value="${_('Generate Example Certificates')}"/>
</form>
</div>
% endif
......@@ -40,7 +40,7 @@ import json
% endif
% endfor
</ul>
<button id="refresh-example-certificate-status">${_("Refresh Status")}</button>
<button class="btn-blue" id="refresh-example-certificate-status">${_("Refresh Status")}</button>
</div>
% endif
</div>
......@@ -53,13 +53,13 @@ import json
<form id="enable-certificates-form" method="post" action="${section_data['urls']['enable_certificate_generation']}">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<input type="hidden" id="certificates-enabled" name="certificates-enabled" value="false" />
<input type="submit" id="disable-certificates-submit" value="${_('Disable Student-Generated Certificates')}"/>
<input type="submit" class="btn-blue" id="disable-certificates-submit" value="${_('Disable Student-Generated Certificates')}"/>
</form>
% elif section_data['can_enable_for_course']:
<form id="enable-certificates-form" method="post" action="${section_data['urls']['enable_certificate_generation']}">
<input type="hidden" name="csrfmiddlewaretoken" value="${csrf_token}">
<input type="hidden" id="certificates-enabled" name="certificates-enabled" value="true" />
<input type="submit" id="enable-certificates-submit" value="${_('Enable Student-Generated Certificates')}"/>
<input type="submit" class="btn-blue" id="enable-certificates-submit" value="${_('Enable Student-Generated Certificates')}"/>
</form>
% else:
<p>${_("You must successfully generate example certificates before you enable student-generated certificates.")}</p>
......@@ -68,7 +68,7 @@ import json
</div>
% if section_data['instructor_generation_enabled'] and not (section_data['enabled_for_course'] and section_data['html_cert_enabled']):
<hr />
<hr class="section-divider" />
<div class="start-certificate-generation">
<h2>${_("Generate Certificates")}</h2>
......@@ -77,7 +77,10 @@ import json
<p>${_("Course certificate generation requires an activated web certificate configuration.")}</p>
<input type="button" id="disabled-btn-start-generating-certificates" class="is-disabled" aria-disabled="true" value="${_('Generate Certificates')}"/>
% else:
<input type="button" id="btn-start-generating-certificates" value="${_('Generate Certificates')}" data-endpoint="${section_data['urls']['start_certificate_generation']}"/>
<p class="under-heading">
${_("When you are ready to generate certificates for your course, click Generate Certificates. You do not need to do this<br/>if you have set the certificate mode to on-demand generation.")}
</p>
<input type="button" class="btn-blue" id="btn-start-generating-certificates" value="${_('Generate Certificates')}" data-endpoint="${section_data['urls']['start_certificate_generation']}"/>
%endif
</form>
<div class="certificate-generation-status"></div>
......@@ -101,22 +104,58 @@ import json
<p class="start-certificate-regeneration">
<h2>${_("Regenerate Certificates")}</h2>
<form id="certificate-regenerating-form" method="post" action="${section_data['urls']['start_certificate_regeneration']}">
<p id='status-multi-select-tip'>${_('Select one or more certificate statuses below using your mouse and ctrl or command key.')}</p>
<select class="multi-select" multiple id="certificate-statuses" name="certificate_statuses" aria-describedby="status-multi-select-tip">
%for status in section_data['certificate_statuses']:
<option value="${status['status']}">${status['status'].title() + " ({})".format(status['count'])}</option>
%endfor
</select>
<label for="certificate-statuses">
${_("Select certificate statuses that need regeneration and click Regenerate Certificates button.")}
</label>
<input type="button" id="btn-start-regenerating-certificates" value="${_('Regenerate Certificates')}" data-endpoint="${section_data['urls']['start_certificate_regeneration']}"/>
<p class="under-heading">
${_('To regenerate certificates for your course, chose the learners who will receive regenerated certificates and click <br/> Regenerate Certificates.')}
</p>
<label style="display: inline" for="certificate_status_${section_data['status'].downloadable}">
<input id="certificate_status_${section_data['status'].downloadable}" type="checkbox" name="certificate_statuses" value="${section_data['status'].downloadable}">
${_("Regenerate for learners who have already received certificates. ({count})").format(count=section_data['certificate_statuses_with_count'].get(section_data['status'].downloadable, 0))}
</label>
<br/>
<label style="display: inline" for="certificate_status_${section_data['status'].notpassing}">
<input id="certificate_status_${section_data['status'].notpassing}" type="checkbox" name="certificate_statuses" value="${section_data['status'].notpassing}">
${_("Regenerate for learners who have not received certificates. ({count})").format(count=section_data['certificate_statuses_with_count'].get(section_data['status'].notpassing, 0))}
</label>
<br/>
<label style="display: inline" for="certificate_status_${section_data['status'].error}">
<input id="certificate_status_${section_data['status'].error}" type="checkbox" name="certificate_statuses" value="${section_data['status'].error}">
${_("Regenerate for learners in an error state. ({count})").format(count=section_data['certificate_statuses_with_count'].get(section_data['status'].error, 0))}
</label>
<br/>
<br/>
<input type="button" class="btn-blue" id="btn-start-regenerating-certificates" value="${_('Regenerate Certificates')}" data-endpoint="${section_data['urls']['start_certificate_regeneration']}"/>
</form>
<div class="message certificate-regeneration-status"></div>
<div class="certificate-regeneration-status"></div>
<hr>
<div class="certificate-generation-history">
<h2 class="title">${_("Certificate Generation History")}</h2>
<div class="certificate-generation-history-content">
<table>
<thead>
<tr>
<td class="task-name"></td>
<td class="task-date"></td>
<td class="task-details"></td>
</tr>
</thead>
<tbody>
% for history in section_data['certificate_generation_history']:
<tr>
<td>${history.get_task_name().title()}</td>
<td>${history.created.strftime("%B %d, %Y")}</td>
<td>${history.get_certificate_generation_candidates()}</td>
</tr>
% endfor
</tbody>
</table>
</div>
</div>
<div class="certificate-exception-container">
<hr>
<hr class="section-divider">
<h2> ${_("SET CERTIFICATE EXCEPTIONS")} </h2>
<p class="under-heading info">
${_("Set exceptions to generate certificates for learners who did not qualify for a certificate but have " \
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment