Skip to content

Commit

Permalink
Sow 8 (#441)
Browse files Browse the repository at this point in the history
* feat: bypass course filtering rules set of lms in admin dashboard

* feat: add new column to all courses enrollment report

* feat: add users enrollment report in admin dashboard

* feat: rearrange admin reports

* feat: add grade report in student admin tab

* Relocate and simplify grade and certificates instructor reports

* allow course staff to access data download section

---------

Co-authored-by: Abdul-Muqadim-Arbisoft <[email protected]>
Co-authored-by: Abdul-Muqadim-Arbisoft <[email protected]>
  • Loading branch information
3 people authored Jul 1, 2024
1 parent c350990 commit 269a00a
Show file tree
Hide file tree
Showing 20 changed files with 453 additions and 136 deletions.
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

0 comments on commit 269a00a

Please sign in to comment.