diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 02a91e7d84de..89bbd95b728c 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -2308,6 +2308,7 @@ def _list_instructor_tasks(request, course_id): Internal function with common code for both DRF and and tradition views. """ + include_canvas = request.GET.get('include_canvas') is not None course_id = CourseKey.from_string(course_id) params = getattr(request, 'query_params', request.POST) problem_location_str = strip_if_string(params.get('problem_location_str', False)) @@ -2331,6 +2332,11 @@ def _list_instructor_tasks(request, course_id): else: # Specifying for single problem's history tasks = task_api.get_instructor_task_history(course_id, module_state_key) + elif include_canvas: + tasks = task_api.get_running_instructor_canvas_tasks( + course_id, + user=request.user + ) else: # If no problem or student, just get currently running tasks tasks = task_api.get_running_instructor_tasks(course_id) diff --git a/lms/djangoapps/instructor_task/api.py b/lms/djangoapps/instructor_task/api.py index 62095c3ae3aa..f66995eb8b0e 100644 --- a/lms/djangoapps/instructor_task/api.py +++ b/lms/djangoapps/instructor_task/api.py @@ -75,6 +75,34 @@ def get_running_instructor_tasks(course_id): return instructor_tasks.order_by('-id') +def _get_filtered_instructor_tasks(course_id, user, task_types): + """ + Returns a filtered query of InstructorTasks based on the course, user, and desired task types + """ + instructor_tasks = get_running_instructor_tasks(course_id) + now = datetime.datetime.now(pytz.utc) + filtered_tasks = InstructorTask.objects.filter( + course_id=course_id, + task_type__in=task_types, + updated__lte=now, + updated__gte=now - datetime.timedelta(days=2), + requester=user + ).order_by('-updated') + + return (instructor_tasks | filtered_tasks).distinct()[0:3] + + +def get_running_instructor_canvas_tasks(course_id, user): + """ + Returns a query of InstructorTask objects of running tasks for a given course + including canvas-specific tasks. + """ + # Inline import because we will install the plugin separately + from ol_openedx_canvas_integration.constants import CANVAS_TASK_TYPES # pylint: disable=import-error + + return _get_filtered_instructor_tasks(course_id, user, CANVAS_TASK_TYPES) + + def get_instructor_task_history(course_id, usage_key=None, student=None, task_type=None): """ Returns a query of InstructorTask objects of historical tasks for a given course, diff --git a/lms/djangoapps/instructor_task/views.py b/lms/djangoapps/instructor_task/views.py index 51e2812577fe..f21f21d4a631 100644 --- a/lms/djangoapps/instructor_task/views.py +++ b/lms/djangoapps/instructor_task/views.py @@ -138,7 +138,9 @@ def get_task_completion_info(instructor_task): # lint-amnesty, pylint: disable= student = None problem_url = None + entrance_exam_url = None email_id = None + course_id = None try: task_input = json.loads(instructor_task.task_input) except ValueError: @@ -149,6 +151,7 @@ def get_task_completion_info(instructor_task): # lint-amnesty, pylint: disable= problem_url = task_input.get('problem_url') entrance_exam_url = task_input.get('entrance_exam_url') email_id = task_input.get('email_id') + course_id = task_input.get('course_key') if instructor_task.task_state == PROGRESS: # special message for providing progress updates: @@ -192,6 +195,17 @@ def get_task_completion_info(instructor_task): # lint-amnesty, pylint: disable= else: # num_succeeded < num_attempted # Translators: {action} is a past-tense verb that is localized separately. {succeeded} and {attempted} are counts. # lint-amnesty, pylint: disable=line-too-long msg_format = _("Problem {action} for {succeeded} of {attempted} students") + elif course_id is not None: + # this reports on actions for a course as a whole + results = task_output.get('results', {}) + assignments_count = results.get("assignments", 0) + grades_count = results.get("grades", 0) + + msg_format = _("{grades_count} grades and {assignments_count} assignments updated or created").format( + grades_count=grades_count, + assignments_count=assignments_count, + ) + succeeded = True elif email_id is not None: # this reports on actions on bulk emails if num_attempted == 0: diff --git a/lms/static/js/instructor_dashboard/instructor_dashboard.js b/lms/static/js/instructor_dashboard/instructor_dashboard.js index 02972a93b6c4..d1dbb4f5183a 100644 --- a/lms/static/js/instructor_dashboard/instructor_dashboard.js +++ b/lms/static/js/instructor_dashboard/instructor_dashboard.js @@ -170,6 +170,9 @@ such that the value can be defined later than this assignment (file load order). }, { constructor: window.InstructorDashboard.sections.ECommerce, $element: idashContent.find('.' + CSS_IDASH_SECTION + '#e-commerce') + }, { + constructor: window.InstructorDashboard.sections.CanvasIntegration, + $element: idashContent.find('.' + CSS_IDASH_SECTION + '#canvas_integration') }, { constructor: window.InstructorDashboard.sections.Membership, $element: idashContent.find('.' + CSS_IDASH_SECTION + '#membership') diff --git a/lms/static/js/instructor_dashboard/util.js b/lms/static/js/instructor_dashboard/util.js index 8e39f4d03e66..1c8d4e00c1e5 100644 --- a/lms/static/js/instructor_dashboard/util.js +++ b/lms/static/js/instructor_dashboard/util.js @@ -51,7 +51,8 @@ enableColumnReorder: false, autoHeight: true, rowHeight: 100, - forceFitColumns: true + forceFitColumns: true, + enableTextSelectionOnCells: true }; columns = [ { @@ -98,7 +99,14 @@ */ name: gettext('Submitted'), - minWidth: 120 + minWidth: 120, + formatter: function(row, cell, value) { + if (!value) { + return value + } + var fromNow = moment(value).fromNow() + return value.concat("
(", fromNow, ")") + } }, { id: 'duration_sec', field: 'duration_sec', @@ -492,7 +500,8 @@ enableCellNavigation: true, enableColumnReorder: false, rowHeight: 30, - forceFitColumns: true + forceFitColumns: true, + enableTextSelectionOnCells: true }; columns = [ { diff --git a/lms/templates/instructor/instructor_dashboard_2/html-datatable.underscore b/lms/templates/instructor/instructor_dashboard_2/html-datatable.underscore new file mode 100644 index 000000000000..89da91b17881 --- /dev/null +++ b/lms/templates/instructor/instructor_dashboard_2/html-datatable.underscore @@ -0,0 +1,16 @@ +

+

<%- title %>

+ + <%_.forEach(header, function (h) {%> + + <%})%> + + <%_.forEach(data, function (row) {%> + + <%_.forEach(row, function (value) {%> + + <%})%> + + <%})%> +
<%- h %>
<%- value %>
+

diff --git a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html index 6fe80e7fdea0..0bb0b6054735 100644 --- a/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/instructor/instructor_dashboard_2/instructor_dashboard_2.html @@ -92,7 +92,7 @@ ## Include Underscore templates <%block name="header_extras"> -% for template_name in ["cohorts", "discussions", "enrollment-code-lookup-links", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification", "cohort-state", "divided-discussions-inline", "divided-discussions-course-wide", "cohort-discussions-category", "cohort-discussions-subcategory", "certificate-allowlist", "certificate-allowlist-editor", "certificate-bulk-allowlist", "certificate-invalidation", "membership-list-widget"]: +% for template_name in ["cohorts", "discussions", "enrollment-code-lookup-links", "cohort-editor", "cohort-group-header", "cohort-selector", "cohort-form", "notification", "cohort-state", "divided-discussions-inline", "divided-discussions-course-wide", "cohort-discussions-category", "cohort-discussions-subcategory", "certificate-allowlist", "certificate-allowlist-editor", "certificate-bulk-allowlist", "certificate-invalidation", "membership-list-widget", "html-datatable"]: diff --git a/xmodule/course_block.py b/xmodule/course_block.py index a74abdb3f617..43c4d4656861 100644 --- a/xmodule/course_block.py +++ b/xmodule/course_block.py @@ -503,6 +503,13 @@ class CourseFields: # lint-amnesty, pylint: disable=missing-class-docstring ), scope=Scope.settings ) + canvas_course_id = Integer( + display_name=_("Canvas Course Id"), + help=_( + "The id for the corresponding course on Canvas" + ), + scope=Scope.settings + ) enable_ccx = Boolean( # Translators: Custom Courses for edX (CCX) is an edX feature for re-using course content. CCX Coach is # a role created by a course Instructor to enable a person (the "Coach") to manage the custom course for