From e2aa883dcafa4f9231eaae47045813abfe05f629 Mon Sep 17 00:00:00 2001
From: Ahmed Khalid <106074266+ahmed-arb@users.noreply.github.com>
Date: Tue, 25 Jun 2024 18:44:16 +0500
Subject: [PATCH 1/9] feat: bypass course filtering rules set of lms in admin
dashboard
---
.../admin_dashboard/course_reports.py | 25 ++++++++++++++++++-
1 file changed, 24 insertions(+), 1 deletion(-)
diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_reports.py b/openedx/features/wikimedia_features/admin_dashboard/course_reports.py
index 8467b5665f5e..dffd0aa12fac 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/course_reports.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/course_reports.py
@@ -11,7 +11,9 @@
from common.djangoapps.edxmako.shortcuts import render_to_response
from common.djangoapps.util.cache import cache_if_anonymous
from lms.djangoapps.courseware.access import has_access
-from lms.djangoapps.courseware.courses import get_course_by_id, get_courses
+from lms.djangoapps.courseware.courses import get_course_by_id
+from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
+from openedx.core.lib.api.view_utils import LazySequence
def require_user_permission():
@@ -42,6 +44,27 @@ def course_reports(request):
courses_list = []
sections = {"key": {}}
+ def get_courses(user=None):
+ """
+ Retrieve a list of courses that a user has access to based on their permissions.
+
+ This function filters the list of all available courses to include only those
+ that the specified user has access to in the 'staff', 'instructor' or Global user role.
+
+ Args:
+ user (optional): The user for whom the course access is being checked.
+
+ Returns:
+ LazySequence: A lazily evaluated sequence of courses that the user has access to.
+ The sequence's length is estimated based on the total count of courses.
+ """
+ permissions = ['staff', 'instructor']
+ courses = CourseOverview.objects.all()
+ return LazySequence(
+ (c for c in courses if any(has_access(user, p, c) for p in permissions)),
+ est_len=courses.count()
+ )
+
courses_list = get_courses(request.user)
course = get_course_by_id(courses_list[0].id, depth=0)
From 4310868b623188709e2065fe65b00db20eaa9a7f Mon Sep 17 00:00:00 2001
From: Ahmed Khalid <106074266+ahmed-arb@users.noreply.github.com>
Date: Wed, 26 Jun 2024 14:01:36 +0500
Subject: [PATCH 2/9] feat: add new column to all courses enrollment report
---
.../admin_dashboard/admin_task/api.py | 1 +
.../course_versions/task_helper.py | 2 +-
.../admin_dashboard/course_versions/utils.py | 9 +++---
.../wikimedia_general/utils.py | 31 +++++++++++++------
4 files changed, 29 insertions(+), 14 deletions(-)
diff --git a/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py b/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py
index a30bc0d0142d..841a331e7cc7 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py
@@ -174,6 +174,7 @@ def all_courses_enrollment_report(request):
"total_learners_enrolled",
"total_learners_completed",
"completed_percentage",
+ "total_cert_generated",
]
submit_courses_enrollment_report(
request, query_features, report_type, task_all_courses_enrollment_report
diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py b/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py
index 45b65a0cc95d..efbb21c27bd7 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py
@@ -133,7 +133,7 @@ def upload_all_courses_enrollment_csv(
csv_type = task_input.get("csv_type", "all_enrollments_stats")
query_features_names = [
- 'Course URL', 'Course title', 'Course available since', 'Parent course URL', 'Parent course title', 'Total learners enrolled', 'Total learners completed', 'Percentage of learners who completed the course',
+ 'Course URL', 'Course title', 'Course available since', 'Parent course URL', 'Parent course title', 'Total learners enrolled', 'Total learners completed', 'Percentage of learners who completed the course', 'Certificates Generated',
]
data = list_all_courses_enrollment_data()
diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py b/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py
index d03e8dda9196..ca19a060997f 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py
@@ -257,7 +257,7 @@ def list_all_courses_enrollment_data():
continue
try:
- total_learners_completed, total_learners_enrolled, completed_percentage = \
+ course_completion_stats = \
get_course_enrollment_and_completion_stats(course.id)
except Exception as e:
log.error(f"An error occurred while fetching enrollment data for course ID {course.id}: {e}")
@@ -271,9 +271,10 @@ def list_all_courses_enrollment_data():
'available_since': course.enrollment_start.strftime("%Y-%m-%d") if course.enrollment_start else '',
"parent_course_url": parent_course_url,
"parent_course_title": parent_course_title,
- "total_learners_enrolled": total_learners_enrolled,
- "total_learners_completed": total_learners_completed,
- "completed_percentage": completed_percentage,
+ "total_learners_enrolled": course_completion_stats["total_learners_enrolled"],
+ "total_learners_completed": course_completion_stats["total_learners_completed"],
+ "completed_percentage": course_completion_stats["completed_percentage"],
+ "total_cert_generated": course_completion_stats["total_cert_generated"],
})
return courses_data
diff --git a/openedx/features/wikimedia_features/wikimedia_general/utils.py b/openedx/features/wikimedia_features/wikimedia_general/utils.py
index f6cb830b9a52..4d7f2d89f280 100644
--- a/openedx/features/wikimedia_features/wikimedia_general/utils.py
+++ b/openedx/features/wikimedia_features/wikimedia_general/utils.py
@@ -13,6 +13,7 @@
from django.contrib.auth import get_user_model
from common.djangoapps.student.models import CourseEnrollment
from common.djangoapps.student.roles import CourseInstructorRole, CourseStaffRole
+from lms.djangoapps.certificates.models import GeneratedCertificate
from opaque_keys.edx.keys import CourseKey
from openedx.features.course_experience.utils import get_course_outline_block_tree
@@ -162,6 +163,11 @@ def is_course_completed(user, course_key):
course_key = CourseKey.from_string(course_key)
return CourseGradeFactory().read(user, course_key=course_key).summary['grade'] == 'Pass'
+def is_certificate_generated(user, course_key):
+ if isinstance(course_key, str):
+ course_key = CourseKey.from_string(course_key)
+ return GeneratedCertificate.objects.filter(user=user, course_id=course_key).exists()
+
def get_user_completed_course_keys(user):
"""
@@ -230,23 +236,30 @@ def get_users_course_completion_stats(users, users_enrollments, course_keys):
def get_course_enrollment_and_completion_stats(course_id) -> dict:
- """Returns the count of student completed the provided course
- """
+ """Returns the count of student completed the provided course"""
enrollments = CourseEnrollment.objects.filter(
- course_id=course_id,
- is_active=True,
- )
+ course_id=course_id,
+ is_active=True,
+ )
- total_students_completed = 0
+ total_learners_completed = 0
+ total_cert_generated = 0
for enrollment in enrollments:
user = User.objects.get(id=enrollment.user_id)
if is_course_completed(user, course_id):
- total_students_completed += 1
+ total_learners_completed += 1
+ if is_certificate_generated(user, course_id):
+ total_cert_generated += 1
enrollment_count = enrollments.count()
- percentage_completed = (total_students_completed/enrollment_count) * 100 if enrollment_count else 0
+ completed_percentage = (total_learners_completed / enrollment_count) * 100 if enrollment_count else 0
- return (total_students_completed, enrollment_count, percentage_completed)
+ return {
+ "total_learners_completed": total_learners_completed,
+ "total_cert_generated": total_cert_generated,
+ "total_learners_enrolled": enrollment_count,
+ "completed_percentage": completed_percentage,
+ }
def get_paced_type(self_paced):
From 5669f87cb8024a1129942aa07f215e6866509345 Mon Sep 17 00:00:00 2001
From: Ahmed Khalid <106074266+ahmed-arb@users.noreply.github.com>
Date: Wed, 26 Jun 2024 18:54:05 +0500
Subject: [PATCH 3/9] feat: add users enrollment report in admin dashboard
---
.../admin_dashboard/admin_task/api.py | 22 ++++++++++
.../admin_dashboard/course_reports.py | 1 +
.../course_versions/task_helper.py | 41 +++++++++++++++++++
.../admin_dashboard/course_versions/utils.py | 30 ++++++++++++--
.../admin_dashboard/js/course_reports.js | 4 ++
.../admin_dashboard/tasks.py | 21 +++++++++-
.../course_report/course-reports.html | 15 +++++++
.../admin_dashboard/urls.py | 7 +++-
.../wikimedia_general/utils.py | 14 +++++--
9 files changed, 147 insertions(+), 8 deletions(-)
diff --git a/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py b/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py
index 841a331e7cc7..78c5536f1601 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/admin_task/api.py
@@ -25,6 +25,7 @@
task_courses_enrollment_report,
task_all_courses_enrollment_report,
task_user_pref_lang_report,
+ task_users_enrollment_info_report,
)
from openedx.features.wikimedia_features.admin_dashboard.course_versions import task_helper
@@ -236,6 +237,27 @@ def user_pref_lang_report(request):
return JsonResponse({"status": success_status})
+@transaction.non_atomic_requests
+@require_POST
+@ensure_csrf_cookie
+@cache_control(no_cache=True, no_store=True, must_revalidate=True)
+def users_enrollment_report(request):
+ """
+ Handles request to generate CSV of all users enrollments info.
+ """
+ report_type = _("all_users_enrollment")
+ success_status = SUCCESS_MESSAGE_TEMPLATE.format(
+ report_type="Users Enrollments Report"
+ )
+ task_input = {
+ 'features': ["username", "enrollments_count", "completions_count"],
+ 'csv_type': report_type,
+ }
+
+ submit_task(request, report_type, task_users_enrollment_info_report, 'all_courses', task_input, "")
+ return JsonResponse({"status": success_status})
+
+
def submit_average_calculate_grades_csv(request, course_key):
"""
AlreadyRunningError is raised if the course's grades are already being updated.
diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_reports.py b/openedx/features/wikimedia_features/admin_dashboard/course_reports.py
index dffd0aa12fac..cfd2506609b7 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/course_reports.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/course_reports.py
@@ -106,6 +106,7 @@ def section_data_download(course, access):
'courses_enrollments_csv_url': reverse('admin_dashboard:courses_enrollment_report'),
'all_courses_enrollments_csv_url': reverse('admin_dashboard:all_courses_enrollment_report'),
'user_pref_lang_csv_url': reverse('admin_dashboard:user_pref_lang_report'),
+ 'users_enrollment_url': reverse('admin_dashboard:users_enrollment_report'),
}
if not access.get('data_researcher'):
section_data['is_hidden'] = True
diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py b/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py
index efbb21c27bd7..4ccff4df716a 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/course_versions/task_helper.py
@@ -15,6 +15,7 @@
get_quarter_dates,
list_all_courses_enrollment_data,
list_user_pref_lang,
+ list_users_enrollments,
list_version_report_info_per_course,
list_version_report_info_total,
list_quarterly_courses_enrollment_data,
@@ -252,3 +253,43 @@ def upload_user_pref_lang_csv(
report_store.store_rows(course_id_str, report_name, rows)
return task_progress.update_task_state(extra_meta=current_step)
+
+
+def upload_users_enrollment_info_csv(
+ _xmodule_instance_args, _entry_id, course_id_str, task_input, action_name, user_ids
+):
+ """
+ Generate a CSV file containing information of users enrollments and course completions.
+ """
+ start_time = time()
+ start_date = datetime.now(UTC)
+ num_reports = 1
+ task_progress = TaskProgress(action_name, num_reports, start_time)
+ current_step = {"step": "Getting Users profile info"}
+ task_progress.update_task_state(extra_meta=current_step)
+
+ # Compute result table and format it
+ query_features = task_input.get("features")
+
+ query_features_names = ["Username", "Enrollments", "Courses Completed"]
+
+ data = list_users_enrollments()
+ __, rows = format_dictlist(data, query_features)
+
+ task_progress.attempted = task_progress.succeeded = len(rows)
+ task_progress.skipped = task_progress.total - task_progress.attempted
+
+ rows.insert(0, query_features_names)
+
+ current_step = {"step": "Uploading CSV"}
+ task_progress.update_task_state(extra_meta=current_step)
+
+ # Perform the upload
+ report_store = ReportStore.from_config("GRADES_DOWNLOAD")
+ csv_name = task_input.get("csv_type")
+ report_name = "{csv_name}_{timestamp_str}.csv".format(
+ csv_name=csv_name, timestamp_str=start_date.strftime("%Y-%m-%d-%H%M")
+ )
+ report_store.store_rows(course_id_str, report_name, rows)
+
+ return task_progress.update_task_state(extra_meta=current_step)
diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py b/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py
index ca19a060997f..533bfe2e4f61 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/course_versions/utils.py
@@ -6,6 +6,7 @@
from django.conf import settings
from django.db.models import OuterRef, Subquery, Value, TextField
from django.db.models.functions import Coalesce
+from django.db.models import Prefetch
from common.djangoapps.student.models import CourseEnrollment
@@ -18,7 +19,7 @@
from openedx.core.djangoapps.user_api.models import UserPreference
from openedx.features.wikimedia_features.meta_translations.models import CourseTranslation
from openedx.core.djangoapps.content.course_overviews.models import CourseOverview
-from openedx.features.wikimedia_features.wikimedia_general.utils import get_course_enrollment_and_completion_stats
+from openedx.features.wikimedia_features.wikimedia_general.utils import get_course_enrollment_and_completion_stats, get_user_course_completions
from edx_proctoring.api import get_last_exam_completion_date
@@ -237,7 +238,7 @@ def list_all_courses_enrollment_data():
Get all courses enrollment report
"""
- courses = get_visible_courses()
+ courses = CourseOverview.objects.all()
courses_data = []
for course in courses:
@@ -280,7 +281,6 @@ def list_all_courses_enrollment_data():
return courses_data
-
def list_quarterly_courses_enrollment_data(quarter):
"""
Get course reports
@@ -365,3 +365,27 @@ def list_user_pref_lang():
pref_lang_data.append(pref_lang)
return pref_lang_data
+
+
+def list_users_enrollments():
+
+ users_with_course_enrollments = User.objects.prefetch_related("courseenrollment_set__course").filter(
+ courseenrollment__is_active=1
+ ).distinct()
+
+ users_enrollments_data = []
+
+ for user in users_with_course_enrollments:
+ user_enrollments = user.courseenrollment_set.all()
+ user_completions = get_user_course_completions(user, user_enrollments)
+ user_enrollments_count = user_enrollments.count()
+
+ users_enrollments_data.append(
+ {
+ "username": user.username,
+ "enrollments_count": user_enrollments_count,
+ "completions_count": user_completions,
+ }
+ )
+
+ return users_enrollments_data
diff --git a/openedx/features/wikimedia_features/admin_dashboard/static/admin_dashboard/js/course_reports.js b/openedx/features/wikimedia_features/admin_dashboard/static/admin_dashboard/js/course_reports.js
index 8fb277a5a4f1..5a1735029899 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/static/admin_dashboard/js/course_reports.js
+++ b/openedx/features/wikimedia_features/admin_dashboard/static/admin_dashboard/js/course_reports.js
@@ -328,6 +328,10 @@
let data_url = $(this).attr('data-endpoint');
AjaxCall(data_url);
})
+ $("[name='users-enrollment-csv']").click(function() {
+ let data_url = $(this).attr('data-endpoint');
+ AjaxCall(data_url);
+ })
$(document).ready(function() {
endpoint = '/wikimedia/list_all_courses_report_downloads';
ReportDownloads();
diff --git a/openedx/features/wikimedia_features/admin_dashboard/tasks.py b/openedx/features/wikimedia_features/admin_dashboard/tasks.py
index 6fc8176ab18b..365964cbc687 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/tasks.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/tasks.py
@@ -37,6 +37,7 @@
upload_quarterly_courses_enrollment_csv,
upload_all_courses_enrollment_csv,
upload_user_pref_lang_csv,
+ upload_users_enrollment_info_csv,
)
from openedx.features.wikimedia_features.email.utils import send_notification
@@ -130,7 +131,7 @@ def task_courses_enrollment_report(entry_id, xmodule_instance_args, user_id):
@set_code_owner_attribute
def task_user_pref_lang_report(entry_id, xmodule_instance_args, user_id):
"""
- Generate a course enrollment report for users preferred languages and push the results to an S3 bucket for download.
+ Generate a report for users preferred languages and push the results to an S3 bucket for download.
"""
# Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
action_name = ugettext_noop('generated')
@@ -142,6 +143,24 @@ def task_user_pref_lang_report(entry_id, xmodule_instance_args, user_id):
return run_main_task(entry_id, task_fn, action_name, user_id)
+@shared_task(base=BaseAdminReportTask)
+@set_code_owner_attribute
+def task_users_enrollment_info_report(entry_id, xmodule_instance_args, user_id):
+ """
+ Generate a report for users enrollments and course completions.
+ """
+ # Translators: This is a past-tense verb that is inserted into task progress messages as {action}.
+ action_name = ugettext_noop("generated")
+ TASK_LOG.info(
+ "Task: %s, AdminReportTask ID: %s, Task type: %s, Preparing for task execution",
+ xmodule_instance_args.get("task_id"),
+ entry_id,
+ action_name,
+ )
+ task_fn = partial(upload_users_enrollment_info_csv, xmodule_instance_args)
+ return run_main_task(entry_id, task_fn, action_name, user_id)
+
+
@task(base=LoggedTask)
def send_report_ready_email_task(msg_class_key, context, subject, email):
TASK_LOG.info("Initiated task to send admin report ready notifications.")
diff --git a/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html b/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html
index 1115f6e4233a..1568324ac21a 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html
+++ b/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html
@@ -223,6 +223,21 @@
>${_("Generate")}
+
+
+ ${_("All Users enrollment report")}
+ ${_("Generates a report on users enrollments and their completed courses")}
+
+
+
+ ${_("Generate")}
+
+
diff --git a/openedx/features/wikimedia_features/admin_dashboard/urls.py b/openedx/features/wikimedia_features/admin_dashboard/urls.py
index 2892863336ae..d566b592cb44 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/urls.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/urls.py
@@ -6,7 +6,7 @@
from openedx.features.wikimedia_features.admin_dashboard.course_reports import course_reports
from openedx.features.wikimedia_features.admin_dashboard.admin_task.api import (
- average_calculate_grades_csv, progress_report_csv, course_version_report, courses_enrollment_report, all_courses_enrollment_report, user_pref_lang_report
+ average_calculate_grades_csv, progress_report_csv, course_version_report, courses_enrollment_report, all_courses_enrollment_report, user_pref_lang_report, users_enrollment_report
)
app_name = 'admin_dashboard'
@@ -47,5 +47,10 @@
user_pref_lang_report,
name='user_pref_lang_report'
),
+ url(
+ r'^users_enrollment_report',
+ users_enrollment_report,
+ name='users_enrollment_report'
+ ),
url(r'', course_reports, name='course_reports'),
]
diff --git a/openedx/features/wikimedia_features/wikimedia_general/utils.py b/openedx/features/wikimedia_features/wikimedia_general/utils.py
index 4d7f2d89f280..cbc731cc3aa3 100644
--- a/openedx/features/wikimedia_features/wikimedia_general/utils.py
+++ b/openedx/features/wikimedia_features/wikimedia_general/utils.py
@@ -31,9 +31,6 @@
from lms.djangoapps.courseware.courses import get_course_with_access
from lms.djangoapps.discussion.django_comment_client.utils import (
add_courseware_context)
-from common.djangoapps.student.models import (
- CourseEnrollment
-)
from openedx.core.djangoapps.user_api.models import UserPreference
from lms.djangoapps.discussion.notification_prefs import WEEKLY_NOTIFICATION_PREF_KEY
from opaque_keys.edx.keys import CourseKey, UsageKey, i4xEncoder
@@ -262,6 +259,17 @@ def get_course_enrollment_and_completion_stats(course_id) -> dict:
}
+def get_user_course_completions(user, user_enrollments):
+ total_completions = 0
+
+ for enrollment in user_enrollments:
+ course = getattr(enrollment, 'course', None)
+ if course and is_course_completed(user, course.id):
+ total_completions += 1
+
+ return total_completions
+
+
def get_paced_type(self_paced):
""" Paced Type Filter
Args:
From 6b69862b4fd4341863ece7003470d13885ba3274 Mon Sep 17 00:00:00 2001
From: Ahmed Khalid <106074266+ahmed-arb@users.noreply.github.com>
Date: Thu, 27 Jun 2024 11:28:40 +0500
Subject: [PATCH 4/9] feat: rearrange admin reports
---
.../course_report/course-reports.html | 143 +++++++++---------
1 file changed, 72 insertions(+), 71 deletions(-)
diff --git a/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html b/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html
index 1568324ac21a..df26fe2741a9 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html
+++ b/openedx/features/wikimedia_features/admin_dashboard/templates/course_report/course-reports.html
@@ -34,6 +34,78 @@
+
+
+
+ ${_("Quarterly courses enrollment report")}
+ ${_("Generate a report for last quarter enrollments of all courses.")}
+
+
+
+ ${_("Select Year")}
+ %for year in year_options:
+ ${year}
+ %endfor
+
+
+ ${_("Select Quarter")}
+
+
+
+
+ ${_("Generate")}
+
+
+
+
+ ${_("All courses enrollment report")}
+ ${_("Generate an all time report of enrollment/completion for all courses.")}
+
+
+
+ ${_("Generate")}
+
+
+
+
+ ${_("Users preferred language report")}
+ ${_("Generate a report on users preferred lanugage. Displays N/A for users who have not selected a lanugage")}
+
+
+
+ ${_("Generate")}
+
+
+
+
+ ${_("All Users enrollment report")}
+ ${_("Generates a report on users enrollments and their completed courses")}
+
+
+
+ ${_("Generate")}
+
+
${_("List profile information for enrolled students")}
@@ -167,77 +239,6 @@
-
-
- ${_("Quarterly courses enrollment report")}
- ${_("Generate a report for last quarter enrollments of all courses.")}
-
-
-
- ${_("Select Year")}
- %for year in year_options:
- ${year}
- %endfor
-
-
- ${_("Select Quarter")}
-
-
-
-
- ${_("Generate")}
-
-
-
-
- ${_("All courses enrollment report")}
- ${_("Generate an all time report of enrollment/completion for all courses.")}
-
-
-
- ${_("Generate")}
-
-
-
-
- ${_("Users preferred language report")}
- ${_("Generate a report on users preferred lanugage. Displays N/A for users who have not selected a lanugage")}
-
-
-
- ${_("Generate")}
-
-
-
-
- ${_("All Users enrollment report")}
- ${_("Generates a report on users enrollments and their completed courses")}
-
-
-
- ${_("Generate")}
-
-
From 879d64ede429c74ce3034cd70142a410216d37ad Mon Sep 17 00:00:00 2001
From: Ahmed Khalid <106074266+ahmed-arb@users.noreply.github.com>
Date: Thu, 27 Jun 2024 18:05:19 +0500
Subject: [PATCH 5/9] feat: add grade report in student admin tab
---
lms/djangoapps/instructor/views/api.py | 32 +++++++++++
lms/djangoapps/instructor/views/api_urls.py | 1 +
.../instructor/views/instructor_dashboard.py | 4 ++
.../js/instructor_dashboard/student_admin.js | 57 +++++++++++++++++--
lms/static/js/instructor_dashboard/util.js | 1 +
.../sass/course/instructor/_instructor_2.scss | 10 ++++
.../instructor_dashboard_2/student_admin.html | 36 +++++++++++-
7 files changed, 134 insertions(+), 7 deletions(-)
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 5de1d32b6fbd..6902c751bebe 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -2345,6 +2345,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('
{} ').format(HTML(url), Text(name)))
+ )
+
+ return JsonResponse(response_payload)
+
+
@require_GET
@ensure_csrf_cookie
def pending_tasks(request, course_id=None):
diff --git a/lms/djangoapps/instructor/views/api_urls.py b/lms/djangoapps/instructor/views/api_urls.py
index e18b99fca1a2..b88b1bbab8e5 100644
--- a/lms/djangoapps/instructor/views/api_urls.py
+++ b/lms/djangoapps/instructor/views/api_urls.py
@@ -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'),
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index 4143d39c098c..fdb176c7aeae 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -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)}'
diff --git a/lms/static/js/instructor_dashboard/student_admin.js b/lms/static/js/instructor_dashboard/student_admin.js
index 54b773faaae6..98529cf705a3 100644
--- a/lms/static/js/instructor_dashboard/student_admin.js
+++ b/lms/static/js/instructor_dashboard/student_admin.js
@@ -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);
@@ -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);
@@ -26,7 +30,6 @@
return item;
}
};
-
this.StudentAdmin = (function() {
function StudentAdmin($section) {
var studentadmin = this;
@@ -65,7 +68,11 @@
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');
@@ -73,6 +80,8 @@
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();
@@ -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) {
@@ -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;
diff --git a/lms/static/js/instructor_dashboard/util.js b/lms/static/js/instructor_dashboard/util.js
index 578efecb598f..2c110587ca3c 100644
--- a/lms/static/js/instructor_dashboard/util.js
+++ b/lms/static/js/instructor_dashboard/util.js
@@ -517,6 +517,7 @@
$tablePlaceholder = $('
', {
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) {
diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss
index 3bd8e8a0c251..dcc04c72ba4b 100644
--- a/lms/static/sass/course/instructor/_instructor_2.scss
+++ b/lms/static/sass/course/instructor/_instructor_2.scss
@@ -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
diff --git a/lms/templates/instructor/instructor_dashboard_2/student_admin.html b/lms/templates/instructor/instructor_dashboard_2/student_admin.html
index ec21e3a99316..b7465269d297 100644
--- a/lms/templates/instructor/instructor_dashboard_2/student_admin.html
+++ b/lms/templates/instructor/instructor_dashboard_2/student_admin.html
@@ -1,5 +1,8 @@
<%page args="section_data" expression_filter="h"/>
-<%! from django.utils.translation import ugettext as _ %>
+<%!
+from django.utils.translation import ugettext as _
+from openedx.core.djangolib.markup import HTML, Text
+%>
%if section_data['access']['staff'] or section_data['access']['instructor']:
%if section_data.get('writable_gradebook_url') or section_data.get('is_small_course'):
@@ -13,9 +16,12 @@
${_("View gradebook for enrolled learners")}
${_("View Gradebook")}
%endif
-
-
+
%endif
+
${_("Click to generate a CSV grade report for all currently enrolled students.")}
+
+
+
@@ -230,8 +236,32 @@
${_("Task Status")}
%endif
%endif
+
+
+
+
+
${_("Reports Available for Download")}
+
+ ${_("The reports listed below are available for download, identified by UTC date and time of generation.")}
+
+
+ ## Translators: a table of URL links to report files appears after this sentence.
+
+ ${Text(_("{strong_start}Note{strong_end}: {ul_start}{li_start}To keep student data secure, you cannot save or email these links for direct access. Copies of links expire within 5 minutes.{li_end}{li_start}Report files are deleted 90 days after generation. If you will need access to old reports, download and store the files, in accordance with your institution's data security policies.{li_end}{ul_end}")).format(
+ strong_start=HTML(""),
+ strong_end=HTML(" "),
+ ul_start=HTML("
"),
+ li_start=HTML("
"),
+ li_end=HTML(" "),
+ )}
+
+
+
+
%if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'):
+
${_("Pending Tasks")}
From 151dbff00ffcade0e8535e2fabd29f7b9634dc4b Mon Sep 17 00:00:00 2001
From: Abdul-Muqadim-Arbisoft
<139064778+Abdul-Muqadim-Arbisoft@users.noreply.github.com>
Date: Fri, 28 Jun 2024 12:31:51 +0500
Subject: [PATCH 6/9] Relocate and simplify grade and certificates instructor
reports (#439)
* Relocate and simplify grade and certificates instructor reports
* allow course staff to access data download section
---
lms/djangoapps/instructor/views/api.py | 5 +-
.../instructor/views/instructor_dashboard.py | 2 +-
lms/djangoapps/instructor_analytics/basic.py | 3 +-
.../instructor_task/tasks_helper/grades.py | 15 +++---
.../instructor_dashboard_2/data_download.html | 50 +++++++++----------
5 files changed, 38 insertions(+), 37 deletions(-)
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index 6902c751bebe..f766446937f3 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -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 = ['course_id', 'mode', 'total_issued_certificate', 'report_run_date', 'download_url']
query_features_names = [
('course_id', _('CourseID')),
('mode', _('Certificate Type')),
('total_issued_certificate', _('Total Certificates Issued')),
- ('report_run_date', _('Date Report Run'))
+ ('report_run_date', _('Date Report Run')),
+ ('download_url', _('Certificate link'))
]
certificates_data = instructor_analytics_basic.issued_certificates(course_key, query_features)
if csv_required.lower() == 'true':
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index fdb176c7aeae..c1de703fb395 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -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
diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py
index afce0620aab0..6a1ac6117bae 100644
--- a/lms/djangoapps/instructor_analytics/basic.py
+++ b/lms/djangoapps/instructor_analytics/basic.py
@@ -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')
UNAVAILABLE = "[unavailable]"
@@ -81,6 +81,7 @@ def issued_certificates(course_key, features):
data['report_run_date'] = report_run_date
data['course_id'] = str(data['course_id'])
+
return generated_certificates
diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py
index f4dcf3d49f04..81e8d2d0b26b 100644
--- a/lms/djangoapps/instructor_task/tasks_helper/grades.py
+++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py
@@ -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
@@ -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
@@ -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):
diff --git a/lms/templates/instructor/instructor_dashboard_2/data_download.html b/lms/templates/instructor/instructor_dashboard_2/data_download.html
index 20950eca11f1..c9c15269f0da 100644
--- a/lms/templates/instructor/instructor_dashboard_2/data_download.html
+++ b/lms/templates/instructor/instructor_dashboard_2/data_download.html
@@ -29,6 +29,31 @@
${_("Reports")}
${_("Please be patient and do not click these buttons multiple times. Clicking these buttons multiple times will significantly slow the generation process.")}
+ %if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
+
${_("Click to generate a CSV grade report for all currently enrolled students.")}
+
+
+
+
+
+
+
+
${_("Click to generate a ZIP file that contains all submission texts and attachments.")}
+
+
+
+ %endif
+
+
${_("Click to list certificates that are issued for this course:")}
+
+
+
+
+
+
+
+
+
${_("Click to generate a CSV file of all students enrolled in this course, along with profile information such as email address and username:")}
@@ -76,37 +101,12 @@
${_("Reports")}
)}
-
-
${_("Click to list certificates that are issued for this course:")}
-
-
-
-
-
-
-
-
% if not disable_buttons:
${_("For smaller courses, click to list profile information for enrolled students directly on this page:")}
%endif
- %if settings.FEATURES.get('ALLOW_COURSE_STAFF_GRADE_DOWNLOADS') or section_data['access']['admin']:
-
${_("Click to generate a CSV grade report for all currently enrolled students.")}
-
-
-
-
-
-
-
-
${_("Click to generate a ZIP file that contains all submission texts and attachments.")}
-
-
-
- %endif
-
From 9e31e528f0927701839616abd344eed87ee795a0 Mon Sep 17 00:00:00 2001
From: Abdul-Muqadim-Arbisoft
Date: Mon, 1 Jul 2024 10:29:36 +0500
Subject: [PATCH 7/9] Cetficate reports simplified and permissions updated
---
lms/djangoapps/instructor/views/api.py | 8 ++++----
.../instructor/views/instructor_dashboard.py | 2 +-
lms/djangoapps/instructor_analytics/basic.py | 10 +++++++---
.../admin_dashboard/course_reports.py | 2 +-
4 files changed, 13 insertions(+), 9 deletions(-)
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index f766446937f3..f7eafd968dd4 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -1233,12 +1233,12 @@ 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', 'download_url']
+ query_features = ['user', 'grade', 'mode', 'created_date', 'download_url']
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')),
('download_url', _('Certificate link'))
]
certificates_data = instructor_analytics_basic.issued_certificates(course_key, query_features)
diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py
index c1de703fb395..9de2e8f1b796 100644
--- a/lms/djangoapps/instructor/views/instructor_dashboard.py
+++ b/lms/djangoapps/instructor/views/instructor_dashboard.py
@@ -632,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
diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py
index 6a1ac6117bae..cead431e3db0 100644
--- a/lms/djangoapps/instructor_analytics/basic.py
+++ b/lms/djangoapps/instructor_analytics/basic.py
@@ -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', 'download_url')
+CERTIFICATE_FEATURES = ('course_id', 'mode', 'status', 'grade', 'created_date', 'is_active', 'error_reason', 'download_url', 'name', 'grade', 'user')
UNAVAILABLE = "[unavailable]"
@@ -74,12 +74,16 @@ 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
+
return generated_certificates
diff --git a/openedx/features/wikimedia_features/admin_dashboard/course_reports.py b/openedx/features/wikimedia_features/admin_dashboard/course_reports.py
index cfd2506609b7..0e17d5b753ed 100644
--- a/openedx/features/wikimedia_features/admin_dashboard/course_reports.py
+++ b/openedx/features/wikimedia_features/admin_dashboard/course_reports.py
@@ -108,6 +108,6 @@ def section_data_download(course, access):
'user_pref_lang_csv_url': reverse('admin_dashboard:user_pref_lang_report'),
'users_enrollment_url': reverse('admin_dashboard:users_enrollment_report'),
}
- 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
From 8bc68a4dbbee983f37f819797d28ebce5c2c47ef Mon Sep 17 00:00:00 2001
From: Abdul-Muqadim-Arbisoft
Date: Mon, 1 Jul 2024 14:33:59 +0500
Subject: [PATCH 8/9] Certficate link correction and grade report additional
row data removal
---
lms/djangoapps/instructor/views/api.py | 4 ++--
lms/djangoapps/instructor_analytics/basic.py | 18 ++++++++++++++----
.../instructor_task/tasks_helper/grades.py | 5 +----
3 files changed, 17 insertions(+), 10 deletions(-)
diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py
index f7eafd968dd4..66c572374168 100644
--- a/lms/djangoapps/instructor/views/api.py
+++ b/lms/djangoapps/instructor/views/api.py
@@ -1233,13 +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 = ['user', 'grade', 'mode', 'created_date', 'download_url']
+ query_features = ['user', 'grade', 'mode', 'created_date', 'verify_uuid']
query_features_names = [
('user', _('Username')),
('grade', _('Grade')),
('mode', _('Certificate Type')),
('created_date', _('Certificate Creation Date')),
- ('download_url', _('Certificate link'))
+ ('verify_uuid', _('Certificate link'))
]
certificates_data = instructor_analytics_basic.issued_certificates(course_key, query_features)
if csv_required.lower() == 'true':
diff --git a/lms/djangoapps/instructor_analytics/basic.py b/lms/djangoapps/instructor_analytics/basic.py
index cead431e3db0..4855834178cb 100644
--- a/lms/djangoapps/instructor_analytics/basic.py
+++ b/lms/djangoapps/instructor_analytics/basic.py
@@ -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__)
@@ -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', 'download_url', 'name', 'grade', 'user')
+CERTIFICATE_FEATURES = ('course_id', 'mode', 'status', 'grade', 'created_date', 'is_active', 'error_reason', 'download_url', 'name', 'grade', 'user', 'verify_uuid')
UNAVAILABLE = "[unavailable]"
@@ -76,7 +76,6 @@ def issued_certificates(course_key, features):
status=CertificateStatuses.downloadable
).values(*certificate_features))
- # Report run date
for data in generated_certificates:
data['report_run_date'] = report_run_date
user_id = data.pop('user', None)
@@ -84,10 +83,21 @@ def issued_certificates(course_key, features):
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):
"""
diff --git a/lms/djangoapps/instructor_task/tasks_helper/grades.py b/lms/djangoapps/instructor_task/tasks_helper/grades.py
index 81e8d2d0b26b..4d41f354a89b 100644
--- a/lms/djangoapps/instructor_task/tasks_helper/grades.py
+++ b/lms/djangoapps/instructor_task/tasks_helper/grades.py
@@ -708,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
From 525a6743e629c0ed51fb2688af2464850db80ade Mon Sep 17 00:00:00 2001
From: Abdul-Muqadim-Arbisoft
Date: Mon, 1 Jul 2024 18:39:38 +0500
Subject: [PATCH 9/9] assigning CAN_RESEARCH permission to staff and
instructors of course
---
lms/djangoapps/instructor/permissions.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/lms/djangoapps/instructor/permissions.py b/lms/djangoapps/instructor/permissions.py
index 20426761e66b..afe042828019 100644
--- a/lms/djangoapps/instructor/permissions.py
+++ b/lms/djangoapps/instructor/permissions.py
@@ -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')