Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sow 8 #441

Merged
merged 12 commits into from
Jul 1, 2024
Merged

Sow 8 #441

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lms/djangoapps/instructor/permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@
perms[VIEW_ISSUED_CERTIFICATES] = HasAccessRule('staff') | HasRolesRule('data_researcher')
# only global staff or those with the data_researcher role can access the data download tab
# HasAccessRule('staff') also includes course staff
perms[CAN_RESEARCH] = is_staff | HasRolesRule('data_researcher')
perms[CAN_RESEARCH] = is_staff | HasRolesRule('data_researcher', 'staff', 'instructor')
perms[CAN_ENROLL] = HasAccessRule('staff')
perms[CAN_BETATEST] = HasAccessRule('instructor')
perms[ENROLLMENT_REPORT] = HasAccessRule('staff') | HasRolesRule('data_researcher')
Expand Down
41 changes: 37 additions & 4 deletions lms/djangoapps/instructor/views/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1233,12 +1233,13 @@ def get_issued_certificates(request, course_id):
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 = ['user', 'grade', 'mode', 'created_date', 'verify_uuid']
query_features_names = [
('course_id', _('CourseID')),
('user', _('Username')),
('grade', _('Grade')),
('mode', _('Certificate Type')),
('total_issued_certificate', _('Total Certificates Issued')),
('report_run_date', _('Date Report Run'))
('created_date', _('Certificate Creation Date')),
('verify_uuid', _('Certificate link'))
]
certificates_data = instructor_analytics_basic.issued_certificates(course_key, query_features)
if csv_required.lower() == 'true':
Expand Down Expand Up @@ -2345,6 +2346,38 @@ def _list_report_downloads(request, course_id):
return JsonResponse(response_payload)


@require_POST
@ensure_csrf_cookie
def list_report_downloads_student_admin(request, course_id):
"""
List grade CSV files that are available for download for this course.
"""
return _list_report_downloads_student_admin(request=request, course_id=course_id)


@cache_control(no_cache=True, no_store=True, must_revalidate=True)
@require_course_permission(permissions.CAN_RESEARCH)
def _list_report_downloads_student_admin(request, course_id):
"""
List grade CSV files that are available for download for this course for student admins.

Internal function with common code shared between DRF and functional views.
"""
course_id = CourseKey.from_string(course_id)
report_store = ReportStore.from_config(config_name="GRADES_DOWNLOAD")
report_names = ["grade_report"]
course_run = course_id.run # because filenames don't have course version

response_payload = {"downloads": []}
for name, url in report_store.links_for(course_id):
if any(f"{course_run}_{report_name}" in name for report_name in report_names):
response_payload["downloads"].append(
dict(name=name, url=url, link=HTML('<a href="{}">{}</a>').format(HTML(url), Text(name)))
)

return JsonResponse(response_payload)


@require_GET
@ensure_csrf_cookie
def pending_tasks(request, course_id=None):
Expand Down
1 change: 1 addition & 0 deletions lms/djangoapps/instructor/views/api_urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@

# Grade downloads...
path('list_report_downloads', api.list_report_downloads, name='list_report_downloads'),
path('list_report_downloads_student_admin', api.list_report_downloads_student_admin, name='list_report_downloads_student_admin'),
path('calculate_grades_csv', api.calculate_grades_csv, name='calculate_grades_csv'),
path('problem_grade_report', api.problem_grade_report, name='problem_grade_report'),

Expand Down
8 changes: 6 additions & 2 deletions lms/djangoapps/instructor/views/instructor_dashboard.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ def instructor_dashboard_2(request, course_id): # lint-amnesty, pylint: disable
sections_content.append(_section_discussions_management(course, access))
sections.extend(sections_content)

if access['data_researcher']:
if access['data_researcher'] or access['staff'] or access['instructor']:
sections.append(_section_data_download(course, access))

analytics_dashboard_message = None
Expand Down Expand Up @@ -563,6 +563,10 @@ def _section_student_admin(course, access):
kwargs={'course_id': str(course_key)}
),
'spoc_gradebook_url': reverse('spoc_gradebook', kwargs={'course_id': str(course_key)}),
'calculate_grades_csv_url': reverse('calculate_grades_csv', kwargs={'course_id': str(course_key)}),
'list_report_downloads_url': reverse(
'list_report_downloads_student_admin', kwargs={'course_id': str(course_key)}
),
}
if is_writable_gradebook_enabled(course_key) and settings.WRITABLE_GRADEBOOK_URL:
section_data['writable_gradebook_url'] = f'{settings.WRITABLE_GRADEBOOK_URL}/{str(course_key)}'
Expand Down Expand Up @@ -628,7 +632,7 @@ def _section_data_download(course, access):
),
'export_ora2_summary_url': reverse('export_ora2_summary', kwargs={'course_id': str(course_key)}),
}
if not access.get('data_researcher'):
if not (access.get('data_researcher') or access.get('staff') or access.get('instructor')):
section_data['is_hidden'] = True
return section_data

Expand Down
25 changes: 20 additions & 5 deletions lms/djangoapps/instructor_analytics/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from lms.djangoapps.verify_student.services import IDVerificationService
from openedx.core.djangoapps.site_configuration import helpers as configuration_helpers
from openedx.core.djangolib.markup import HTML, Text

from openedx.core.djangoapps.theming.helpers import get_current_site
log = logging.getLogger(__name__)


Expand All @@ -52,7 +52,7 @@
AVAILABLE_FEATURES = STUDENT_FEATURES + PROFILE_FEATURES + PROGRAM_ENROLLMENT_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')
CERTIFICATE_FEATURES = ('course_id', 'mode', 'status', 'grade', 'created_date', 'is_active', 'error_reason', 'download_url', 'name', 'grade', 'user', 'verify_uuid')

UNAVAILABLE = "[unavailable]"

Expand All @@ -74,15 +74,30 @@ def issued_certificates(course_key, features):
generated_certificates = list(GeneratedCertificate.eligible_certificates.filter(
course_id=course_key,
status=CertificateStatuses.downloadable
).values(*certificate_features).annotate(total_issued_certificate=Count('mode')))
).values(*certificate_features))

# Report run date
for data in generated_certificates:
data['report_run_date'] = report_run_date
data['course_id'] = str(data['course_id'])
user_id = data.pop('user', None)
if user_id:
user = User.objects.get(id=user_id)
data['user'] = user.username

verify_uuid = data.pop('verify_uuid', None)
if verify_uuid:
certificate_url = generate_certificate_url(verify_uuid)
data['verify_uuid'] = certificate_url

return generated_certificates

def generate_certificate_url(verify_uuid):
"""
Generate the certificate URL based on the verify_uuid.
"""
current_site = get_current_site()
scheme = 'https' if settings.HTTPS == 'on' else 'http'
base_url = f"{scheme}://{current_site.domain}"
return f"{base_url}/certificates/{verify_uuid}"

def enrolled_students_features(course_key, features):
"""
Expand Down
20 changes: 8 additions & 12 deletions lms/djangoapps/instructor_task/tasks_helper/grades.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
from xmodule.modulestore.django import modulestore
from xmodule.partitions.partitions_service import PartitionService
from xmodule.split_test_module import get_split_user_partitions
from xmodule.util.misc import get_default_short_labeler

from .runner import TaskProgress
from .utils import upload_csv_to_report_store
Expand Down Expand Up @@ -252,14 +253,15 @@ def graded_assignments(self):
"""
grading_cxt = grades_context.grading_context(self.course, self.course_structure)
graded_assignments_map = OrderedDict()
short_labeler = get_default_short_labeler(self.course)

for assignment_type_name, subsection_infos in grading_cxt['all_graded_subsections_by_type'].items():
graded_subsections_map = OrderedDict()
for subsection_index, subsection_info in enumerate(subsection_infos, start=1):
subsection = subsection_info['subsection_block']
header_name = "{assignment_type} {subsection_index}: {subsection_name}".format(
assignment_type=assignment_type_name,
subsection_index=subsection_index,
subsection_name=subsection.display_name,
short_label = short_labeler(subsection.format)
header_name = "{short_label}".format(
short_label=short_label,
)
graded_subsections_map[subsection.location] = header_name

Expand Down Expand Up @@ -443,10 +445,7 @@ def _success_headers(self, context):
self._grades_header(context) +
(['Cohort Name'] if context.cohorts_enabled else []) +
[f'Experiment Group ({partition.name})' for partition in context.course_experiments] +
(['Team Name'] if context.teams_enabled else []) +
['Enrollment Track', 'Verification Status'] +
['Certificate Eligible', 'Certificate Delivered', 'Certificate Type'] +
['Enrollment Status']
(['Team Name'] if context.teams_enabled else [])
)

def _error_headers(self):
Expand Down Expand Up @@ -709,10 +708,7 @@ def _rows_for_users(self, context, users):
self._user_grades(course_grade, context) +
self._user_cohort_group_names(user, context) +
self._user_experiment_group_names(user, context) +
self._user_team_names(user, bulk_context.teams) +
self._user_verification_mode(user, context, bulk_context.enrollments) +
self._user_certificate_info(user, context, course_grade, bulk_context.certs) +
[_user_enrollment_status(user, context.course_id)]
self._user_team_names(user, bulk_context.teams)
)
return success_rows, error_rows

Expand Down
57 changes: 53 additions & 4 deletions lms/static/js/instructor_dashboard/student_admin.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

(function() {
'use strict';
var PendingInstructorTasks, createTaskListTable, findAndAssert, statusAjaxError;
var PendingInstructorTasks, ReportDownloads, createTaskListTable, findAndAssert, statusAjaxError;

statusAjaxError = function() {
return window.InstructorDashboard.util.statusAjaxError.apply(this, arguments);
Expand All @@ -16,6 +16,10 @@
return window.InstructorDashboard.util.PendingInstructorTasks;
};

ReportDownloads = function() {
return window.InstructorDashboard.util.ReportDownloads;
};

findAndAssert = function($root, selector) {
var item, msg;
item = $root.find(selector);
Expand All @@ -26,7 +30,6 @@
return item;
}
};

this.StudentAdmin = (function() {
function StudentAdmin($section) {
var studentadmin = this;
Expand Down Expand Up @@ -65,14 +68,20 @@
this.$btn_rescore_problem_if_higher_all = this.$section.find("input[name='rescore-problem-all-if-higher']");
this.$btn_task_history_all = this.$section.find("input[name='task-history-all']");
this.$table_task_history_all = this.$section.find('.task-history-all-table');
this.report_downloads = new (ReportDownloads())(this.$section);
this.instructor_tasks = new (PendingInstructorTasks())(this.$section);
this.$reports = this.$section.find('.reports-download-container');
this.$reports_request_response = this.$reports.find('.request-response');
this.$reports_request_response_error = this.$reports.find('.request-response-error');
this.$request_err_enrollment_status = findAndAssert(this.$section, '.student-enrollment-status-container .request-response-error');
this.$request_err_progress = findAndAssert(this.$section, '.student-progress-container .request-response-error');
this.$request_err_grade = findAndAssert(this.$section, '.student-grade-container .request-response-error');
this.$request_err_ee = this.$section.find('.entrance-exam-grade-container .request-response-error');
this.$request_response_error_all = this.$section.find('.course-specific-container .request-response-error');
this.$enrollment_status_link = findAndAssert(this.$section, 'a.enrollment-status-link');
this.$enrollment_status = findAndAssert(this.$section, '.student-enrollment-status');
this.$async_report_btn = this.$section.find("input[class='async-report-btn']");
this.clear_display();
this.$enrollment_status_link.click(function(e) {
var errorMessage, fullErrorMessage, uniqStudentIdentifier;
e.preventDefault();
Expand Down Expand Up @@ -476,6 +485,33 @@
})
});
});
this.$async_report_btn.click(function(e) {
var url = $(e.target).data('endpoint');
var errorMessage = '';
studentadmin.clear_display();
return $.ajax({
type: 'POST',
dataType: 'json',
url: url,
error: function(error) {
if (error.responseText) {
errorMessage = JSON.parse(error.responseText);
} else if (e.target.name === 'calculate-grades-csv') {
errorMessage = gettext('Error generating grades. Please try again.');
}
studentadmin.$reports_request_response_error.text(errorMessage);
return studentadmin.$reports_request_response_error.css({
display: 'block'
});
},
success: function(data) {
studentadmin.$reports_request_response.text(data.status);
return $('.msg-confirm').css({
display: 'block'
});
}
});
});
}

StudentAdmin.prototype.rescore_problem_single = function(onlyIfHigher) {
Expand Down Expand Up @@ -686,11 +722,24 @@
};

StudentAdmin.prototype.onClickTitle = function() {
return this.instructor_tasks.task_poller.start();
this.clear_display();
this.instructor_tasks.task_poller.start();
return this.report_downloads.downloads_poller.start();
};

StudentAdmin.prototype.onExit = function() {
return this.instructor_tasks.task_poller.stop();
this.instructor_tasks.task_poller.stop();
return this.report_downloads.downloads_poller.stop();
};
StudentAdmin.prototype.clear_display = function() {
this.$reports_request_response.empty();
this.$reports_request_response_error.empty();
$('.msg-confirm').css({
display: 'none'
});
return $('.msg-error').css({
display: 'none'
});
};

return StudentAdmin;
Expand Down
1 change: 1 addition & 0 deletions lms/static/js/instructor_dashboard/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -517,6 +517,7 @@
$tablePlaceholder = $('<div/>', {
class: 'slickgrid'
});
$tablePlaceholder.css("height", "300px");
this.$report_downloads_table.append($tablePlaceholder);
grid = new Slick.Grid($tablePlaceholder, reportDownloadsData, columns, options);
grid.onClick.subscribe(function(event) {
Expand Down
10 changes: 10 additions & 0 deletions lms/static/sass/course/instructor/_instructor_2.scss
Original file line number Diff line number Diff line change
Expand Up @@ -1504,6 +1504,16 @@
font-style: italic;
font-size: 0.9em;
}
.report-downloads-table {
.slickgrid {
height: 300px;
padding: ($baseline/4);
}
// Disable horizontal scroll bar when grid only has 1 column. Remove this CSS class when more columns added.
.slick-viewport {
overflow-x: hidden !important;
}
}
}

// view - data download
Expand Down
Loading