diff --git a/lms/djangoapps/class_dashboard/__init__.py b/lms/djangoapps/class_dashboard/__init__.py deleted file mode 100644 index 71ff059ee1..0000000000 --- a/lms/djangoapps/class_dashboard/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -init.py file for class_dashboard -""" diff --git a/lms/djangoapps/class_dashboard/dashboard_data.py b/lms/djangoapps/class_dashboard/dashboard_data.py deleted file mode 100644 index 25a693fb00..0000000000 --- a/lms/djangoapps/class_dashboard/dashboard_data.py +++ /dev/null @@ -1,605 +0,0 @@ -""" -Computes the data to display on the Instructor Dashboard -""" - - -import decimal -import json - -from django.db.models import Count -from django.utils.translation import ugettext as _ -from opaque_keys.edx.locator import BlockUsageLocator -from six import text_type - -from lms.djangoapps.courseware import models -from lms.djangoapps.instructor_analytics.csvs import create_csv_response -from util.json_request import JsonResponse -from xmodule.modulestore.django import modulestore -from xmodule.modulestore.inheritance import own_metadata -from openedx.core.lib.grade_utils import round_away_from_zero - -# Used to limit the length of list displayed to the screen. -MAX_SCREEN_LIST_LENGTH = 250 - - -def get_problem_grade_distribution(course_id): - """ - Returns the grade distribution per problem for the course - - `course_id` the course ID for the course interested in - - Output is 2 dicts: - 'prob-grade_distrib' where the key is the problem 'module_id' and the value is a dict with: - 'max_grade' - max grade for this problem - 'grade_distrib' - array of tuples (`grade`,`count`). - 'total_student_count' where the key is problem 'module_id' and the value is number of students - attempting the problem - """ - - # Aggregate query on studentmodule table for grade data for all problems in course - db_query = models.StudentModule.objects.filter( - course_id__exact=course_id, - grade__isnull=False, - module_type__exact="problem", - ).values('module_state_key', 'grade', 'max_grade').annotate(count_grade=Count('grade')) - - prob_grade_distrib = {} - total_student_count = {} - - # Loop through resultset building data for each problem - for row in db_query: - curr_problem = row['module_state_key'].map_into_course(course_id) - - # Build set of grade distributions for each problem that has student responses - if curr_problem in prob_grade_distrib: - prob_grade_distrib[curr_problem]['grade_distrib'].append((row['grade'], row['count_grade'])) - - if (prob_grade_distrib[curr_problem]['max_grade'] != row['max_grade']) and \ - (prob_grade_distrib[curr_problem]['max_grade'] < row['max_grade']): - prob_grade_distrib[curr_problem]['max_grade'] = row['max_grade'] - - else: - prob_grade_distrib[curr_problem] = { - 'max_grade': row['max_grade'], - 'grade_distrib': [(row['grade'], row['count_grade'])] - } - - # Build set of total students attempting each problem - total_student_count[curr_problem] = total_student_count.get(curr_problem, 0) + row['count_grade'] - - return prob_grade_distrib, total_student_count - - -def get_sequential_open_distrib(course_id): - """ - Returns the number of students that opened each subsection/sequential of the course - - `course_id` the course ID for the course interested in - - Outputs a dict mapping the 'module_id' to the number of students that have opened that subsection/sequential. - """ - - # Aggregate query on studentmodule table for "opening a subsection" data - db_query = models.StudentModule.objects.filter( - course_id__exact=course_id, - module_type__exact="sequential", - ).values('module_state_key').annotate(count_sequential=Count('module_state_key')) - - # Build set of "opened" data for each subsection that has "opened" data - sequential_open_distrib = {} - for row in db_query: - row_loc = row['module_state_key'].map_into_course(course_id) - sequential_open_distrib[row_loc] = row['count_sequential'] - - return sequential_open_distrib - - -def get_problem_set_grade_distrib(course_id, problem_set): - """ - Returns the grade distribution for the problems specified in `problem_set`. - - `course_id` the course ID for the course interested in - - `problem_set` an array of UsageKeys representing problem module_id's. - - Requests from the database the a count of each grade for each problem in the `problem_set`. - - Returns a dict, where the key is the problem 'module_id' and the value is a dict with two parts: - 'max_grade' - the maximum grade possible for the course - 'grade_distrib' - array of tuples (`grade`,`count`) ordered by `grade` - """ - - # Aggregate query on studentmodule table for grade data for set of problems in course - db_query = models.StudentModule.objects.filter( - course_id__exact=course_id, - grade__isnull=False, - module_type__exact="problem", - module_state_key__in=problem_set, - ).values( - 'module_state_key', - 'grade', - 'max_grade', - ).annotate(count_grade=Count('grade')).order_by('module_state_key', 'grade') - - prob_grade_distrib = {} - - # Loop through resultset building data for each problem - for row in db_query: - row_loc = row['module_state_key'].map_into_course(course_id) - if row_loc not in prob_grade_distrib: - prob_grade_distrib[row_loc] = { - 'max_grade': 0, - 'grade_distrib': [], - } - - curr_grade_distrib = prob_grade_distrib[row_loc] - curr_grade_distrib['grade_distrib'].append((row['grade'], row['count_grade'])) - - if curr_grade_distrib['max_grade'] < row['max_grade']: - curr_grade_distrib['max_grade'] = row['max_grade'] - - return prob_grade_distrib - - -def get_d3_problem_grade_distrib(course_id): - """ - Returns problem grade distribution information for each section, data already in format for d3 function. - - `course_id` the course ID for the course interested in - - Returns an array of dicts in the order of the sections. Each dict has: - 'display_name' - display name for the section - 'data' - data for the d3_stacked_bar_graph function of the grade distribution for that problem - """ - - prob_grade_distrib, total_student_count = get_problem_grade_distribution(course_id) - d3_data = [] - - # Retrieve course object down to problems - course = modulestore().get_course(course_id, depth=4) - - # Iterate through sections, subsections, units, problems - for section in course.get_children(): - curr_section = {} - curr_section['display_name'] = own_metadata(section).get('display_name', '') - data = [] - c_subsection = 0 - for subsection in section.get_children(): - c_subsection += 1 - c_unit = 0 - for unit in subsection.get_children(): - c_unit += 1 - c_problem = 0 - for child in unit.get_children(): - - # Student data is at the problem level - if child.location.block_type == 'problem': - c_problem += 1 - stack_data = [] - - # Construct label to display for this problem - label = "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem) - - # Only problems in prob_grade_distrib have had a student submission. - if child.location in prob_grade_distrib: - - # Get max_grade, grade_distribution for this problem - problem_info = prob_grade_distrib[child.location] - - # Get problem_name for tooltip - problem_name = own_metadata(child).get('display_name', '') - - # Compute percent of this grade over max_grade - max_grade = float(problem_info['max_grade']) - for (grade, count_grade) in problem_info['grade_distrib']: - percent = 0.0 - if max_grade > 0: - percent = round_away_from_zero((grade * 100.0) / max_grade, 1) - - # Compute percent of students with this grade - student_count_percent = 0 - if total_student_count.get(child.location, 0) > 0: - student_count_percent = count_grade * 100 / total_student_count[child.location] - - # Tooltip parameters for problem in grade distribution view - tooltip = { - 'type': 'problem', - 'label': label, - 'problem_name': problem_name, - 'count_grade': count_grade, - 'percent': percent, - 'grade': grade, - 'max_grade': max_grade, - 'student_count_percent': student_count_percent, - } - - # Construct data to be sent to d3 - stack_data.append({ - 'color': percent, - 'value': count_grade, - 'tooltip': tooltip, - 'module_url': text_type(child.location), - }) - - problem = { - 'xValue': label, - 'stackData': stack_data, - } - data.append(problem) - curr_section['data'] = data - - d3_data.append(curr_section) - - return d3_data - - -def get_d3_sequential_open_distrib(course_id): - """ - Returns how many students opened a sequential/subsection for each section, data already in format for d3 function. - - `course_id` the course ID for the course interested in - - Returns an array in the order of the sections and each dict has: - 'display_name' - display name for the section - 'data' - data for the d3_stacked_bar_graph function of how many students opened each sequential/subsection - """ - sequential_open_distrib = get_sequential_open_distrib(course_id) - - d3_data = [] - - # Retrieve course object down to subsection - course = modulestore().get_course(course_id, depth=2) - - # Iterate through sections, subsections - for section in course.get_children(): - curr_section = {} - curr_section['display_name'] = own_metadata(section).get('display_name', '') - data = [] - c_subsection = 0 - - # Construct data for each subsection to be sent to d3 - for subsection in section.get_children(): - c_subsection += 1 - subsection_name = own_metadata(subsection).get('display_name', '') - - num_students = 0 - if subsection.location in sequential_open_distrib: - num_students = sequential_open_distrib[subsection.location] - - stack_data = [] - - # Tooltip parameters for subsection in open_distribution view - tooltip = { - 'type': 'subsection', - 'num_students': num_students, - 'subsection_num': c_subsection, - 'subsection_name': subsection_name - } - - stack_data.append({ - 'color': 0, - 'value': num_students, - 'tooltip': tooltip, - 'module_url': text_type(subsection.location), - }) - subsection = { - 'xValue': u"SS {0}".format(c_subsection), - 'stackData': stack_data, - } - data.append(subsection) - - curr_section['data'] = data - d3_data.append(curr_section) - - return d3_data - - -def get_d3_section_grade_distrib(course_id, section): - """ - Returns the grade distribution for the problems in the `section` section in a format for the d3 code. - - `course_id` a string that is the course's ID. - - `section` an int that is a zero-based index into the course's list of sections. - - Navigates to the section specified to find all the problems associated with that section and then finds the grade - distribution for those problems. Finally returns an object formated the way the d3_stacked_bar_graph.js expects its - data object to be in. - - If this is requested multiple times quickly for the same course, it is better to call - get_d3_problem_grade_distrib and pick out the sections of interest. - - Returns an array of dicts with the following keys (taken from d3_stacked_bar_graph.js's documentation) - 'xValue' - Corresponding value for the x-axis - 'stackData' - Array of objects with key, value pairs that represent a bar: - 'color' - Defines what "color" the bar will map to - 'value' - Maps to the height of the bar, along the y-axis - 'tooltip' - (Optional) Text to display on mouse hover - """ - - # Retrieve course object down to problems - course = modulestore().get_course(course_id, depth=4) - - problem_set = [] - problem_info = {} - c_subsection = 0 - for subsection in course.get_children()[section].get_children(): - c_subsection += 1 - c_unit = 0 - for unit in subsection.get_children(): - c_unit += 1 - c_problem = 0 - for child in unit.get_children(): - if child.location.block_type == 'problem': - c_problem += 1 - problem_set.append(child.location) - problem_info[child.location] = { - 'id': text_type(child.location), - 'x_value': "P{0}.{1}.{2}".format(c_subsection, c_unit, c_problem), - 'display_name': own_metadata(child).get('display_name', ''), - } - - # Retrieve grade distribution for these problems - grade_distrib = get_problem_set_grade_distrib(course_id, problem_set) - - d3_data = [] - - # Construct data for each problem to be sent to d3 - for problem in problem_set: - stack_data = [] - - if problem in grade_distrib: # Some problems have no data because students have not tried them yet. - max_grade = float(grade_distrib[problem]['max_grade']) - for (grade, count_grade) in grade_distrib[problem]['grade_distrib']: - percent = 0.0 - if max_grade > 0: - percent = round_away_from_zero((grade * 100.0) / max_grade, 1) - - # Construct tooltip for problem in grade distibution view - tooltip = { - 'type': 'problem', - 'problem_info_x': problem_info[problem]['x_value'], - 'count_grade': count_grade, - 'percent': percent, - 'problem_info_n': problem_info[problem]['display_name'], - 'grade': grade, - 'max_grade': max_grade, - } - - stack_data.append({ - 'color': percent, - 'value': count_grade, - 'tooltip': tooltip, - }) - - d3_data.append({ - 'xValue': problem_info[problem]['x_value'], - 'stackData': stack_data, - }) - - return d3_data - - -def get_section_display_name(course_id): - """ - Returns an array of the display names for each section in the course. - - `course_id` the course ID for the course interested in - - The ith string in the array is the display name of the ith section in the course. - """ - - course = modulestore().get_course(course_id, depth=4) - - section_display_name = [""] * len(course.get_children()) - i = 0 - for section in course.get_children(): - section_display_name[i] = own_metadata(section).get('display_name', '') - i += 1 - - return section_display_name - - -def get_array_section_has_problem(course_id): - """ - Returns an array of true/false whether each section has problems. - - `course_id` the course ID for the course interested in - - The ith value in the array is true if the ith section in the course contains problems and false otherwise. - """ - - course = modulestore().get_course(course_id, depth=4) - - b_section_has_problem = [False] * len(course.get_children()) - i = 0 - for section in course.get_children(): - for subsection in section.get_children(): - for unit in subsection.get_children(): - for child in unit.get_children(): - if child.location.block_type == 'problem': - b_section_has_problem[i] = True - break # out of child loop - if b_section_has_problem[i]: - break # out of unit loop - if b_section_has_problem[i]: - break # out of subsection loop - - i += 1 - - return b_section_has_problem - - -def get_students_opened_subsection(request, csv=False): - """ - Get a list of students that opened a particular subsection. - If 'csv' is False, returns a dict of student's name: username. - - If 'csv' is True, returns a header array, and an array of arrays in the format: - student names, usernames for CSV download. - """ - module_state_key = BlockUsageLocator.from_string(request.GET.get('module_id')) - csv = request.GET.get('csv') - - # Query for "opened a subsection" students - students = models.StudentModule.objects.select_related('student').filter( - module_state_key__exact=module_state_key, - module_type__exact='sequential', - ).values('student__username', 'student__profile__name').order_by('student__profile__name') - - results = [] - if not csv: - # Restrict screen list length - # Adding 1 so can tell if list is larger than MAX_SCREEN_LIST_LENGTH - # without doing another select. - for student in students[0:MAX_SCREEN_LIST_LENGTH + 1]: - results.append({ - 'name': student['student__profile__name'], - 'username': student['student__username'], - }) - - max_exceeded = False - if len(results) > MAX_SCREEN_LIST_LENGTH: - # Remove the last item so list length is exactly MAX_SCREEN_LIST_LENGTH - del results[-1] - max_exceeded = True - response_payload = { - 'results': results, - 'max_exceeded': max_exceeded, - } - return JsonResponse(response_payload) - else: - tooltip = request.GET.get('tooltip') - - # Subsection name is everything after 3rd space in tooltip - filename = sanitize_filename(' '.join(tooltip.split(' ')[3:])) - - header = [_("Name"), _("Username")] - for student in students: - results.append([student['student__profile__name'], student['student__username']]) - - response = create_csv_response(filename, header, results) - return response - - -def get_students_problem_grades(request, csv=False): - """ - Get a list of students and grades for a particular problem. - If 'csv' is False, returns a dict of student's name: username: grade: percent. - - If 'csv' is True, returns a header array, and an array of arrays in the format: - student names, usernames, grades, percents for CSV download. - """ - module_state_key = BlockUsageLocator.from_string(request.GET.get('module_id')) - csv = request.GET.get('csv') - - # Query for "problem grades" students - students = models.StudentModule.objects.select_related('student').filter( - module_state_key=module_state_key, - module_type__exact='problem', - grade__isnull=False, - ).values('student__username', 'student__profile__name', 'grade', 'max_grade').order_by('student__profile__name') - - results = [] - if not csv: - # Restrict screen list length - # Adding 1 so can tell if list is larger than MAX_SCREEN_LIST_LENGTH - # without doing another select. - for student in students[0:MAX_SCREEN_LIST_LENGTH + 1]: - student_dict = { - 'name': student['student__profile__name'], - 'username': student['student__username'], - 'grade': student['grade'], - } - - student_dict['percent'] = 0 - if student['max_grade'] > 0: - student_dict['percent'] = round_away_from_zero(student['grade'] * 100 / student['max_grade']) - results.append(student_dict) - - max_exceeded = False - if len(results) > MAX_SCREEN_LIST_LENGTH: - # Remove the last item so list length is exactly MAX_SCREEN_LIST_LENGTH - del results[-1] - max_exceeded = True - - response_payload = { - 'results': results, - 'max_exceeded': max_exceeded, - } - return JsonResponse(response_payload) - else: - tooltip = request.GET.get('tooltip') - filename = sanitize_filename(tooltip[:tooltip.rfind(' - ')]) - - header = [_("Name"), _("Username"), _("Grade"), _("Percent")] - for student in students: - percent = 0 - if student['max_grade'] > 0: - percent = round_away_from_zero((student['grade'] * 100 / student['max_grade']), 1) - results.append([student['student__profile__name'], student['student__username'], student['grade'], percent]) - - response = create_csv_response(filename, header, results) - return response - - -def post_metrics_data_csv(request): - """ - Generate a list of opened subsections or problems for the entire course for CSV download. - Returns a header array, and an array of arrays in the format: - section, subsection, count of students for subsections - or section, problem, name, count of students, percent of students, score for problems. - """ - - data = json.loads(request.POST['data']) - sections = json.loads(data['sections']) - tooltips = json.loads(data['tooltips']) - course_id = data['course_id'] - data_type = data['data_type'] - - results = [] - if data_type == 'subsection': - header = [_("Section"), _("Subsection"), _("Opened by this number of students")] - filename = sanitize_filename(_('subsections') + '_' + course_id) - elif data_type == 'problem': - header = [ - _("Section"), _("Problem"), _("Name"), _("Count of Students"), - _("Percent of Students"), _("Score"), - ] - filename = sanitize_filename(_('problems') + '_' + course_id) - - for index, section in enumerate(sections): - results.append([section]) - - # tooltips array is array of dicts for subsections and - # array of array of dicts for problems. - if data_type == 'subsection': - for tooltip_dict in tooltips[index]: - num_students = tooltip_dict['num_students'] - subsection = tooltip_dict['subsection_name'] - # Append to results offsetting 1 column to the right. - results.append(['', subsection, num_students]) - - elif data_type == 'problem': - for tooltip in tooltips[index]: - for tooltip_dict in tooltip: - label = tooltip_dict['label'] - problem_name = tooltip_dict['problem_name'] - count_grade = tooltip_dict['count_grade'] - student_count_percent = tooltip_dict['student_count_percent'] - percent = tooltip_dict['percent'] - # Append to results offsetting 1 column to the right. - results.append(['', label, problem_name, count_grade, student_count_percent, percent]) - - response = create_csv_response(filename, header, results) - return response - - -def sanitize_filename(filename): - """ - Utility function - """ - filename = filename.replace(" ", "_") - filename = filename.encode('utf-8') - filename = filename[0:25].decode('utf-8') + '.csv' - return filename diff --git a/lms/djangoapps/class_dashboard/tests/__init__.py b/lms/djangoapps/class_dashboard/tests/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py b/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py deleted file mode 100644 index cea11e9ac1..0000000000 --- a/lms/djangoapps/class_dashboard/tests/test_dashboard_data.py +++ /dev/null @@ -1,331 +0,0 @@ -""" -Tests for class dashboard (Metrics tab in instructor dashboard) -""" - - -import json - -from django.test.client import RequestFactory -from django.urls import reverse -from mock import patch -from six import text_type -from six.moves import range - -from capa.tests.response_xml_factory import StringResponseXMLFactory -from class_dashboard.dashboard_data import ( - get_array_section_has_problem, - get_d3_problem_grade_distrib, - get_d3_section_grade_distrib, - get_d3_sequential_open_distrib, - get_problem_grade_distribution, - get_problem_set_grade_distrib, - get_section_display_name, - get_sequential_open_distrib, - get_students_opened_subsection, - get_students_problem_grades -) -from class_dashboard.views import has_instructor_access_for_class -from lms.djangoapps.courseware.tests.factories import StudentModuleFactory -from student.tests.factories import AdminFactory, CourseEnrollmentFactory, UserFactory -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory, ItemFactory - -USER_COUNT = 11 - - -class TestGetProblemGradeDistribution(SharedModuleStoreTestCase): - """ - Tests related to class_dashboard/dashboard_data.py - """ - - @classmethod - def setUpClass(cls): - super(TestGetProblemGradeDistribution, cls).setUpClass() - cls.course = CourseFactory.create( - display_name=u"test course omega \u03a9", - ) - with cls.store.bulk_operations(cls.course.id, emit_signals=False): - section = ItemFactory.create( - parent_location=cls.course.location, - category="chapter", - display_name=u"test factory section omega \u03a9", - ) - cls.sub_section = ItemFactory.create( - parent_location=section.location, - category="sequential", - display_name=u"test subsection omega \u03a9", - ) - cls.unit = ItemFactory.create( - parent_location=cls.sub_section.location, - category="vertical", - metadata={'graded': True, 'format': 'Homework'}, - display_name=u"test unit omega \u03a9", - ) - cls.items = [] - for i in range(USER_COUNT - 1): - item = ItemFactory.create( - parent_location=cls.unit.location, - category="problem", - data=StringResponseXMLFactory().build_xml(answer='foo'), - metadata={'rerandomize': 'always'}, - display_name=u"test problem omega \u03a9 " + str(i) - ) - cls.items.append(item) - cls.item = item - - def setUp(self): - super(TestGetProblemGradeDistribution, self).setUp() - - self.request_factory = RequestFactory() - self.instructor = AdminFactory.create() - self.client.login(username=self.instructor.username, password='test') - self.attempts = 3 - self.users = [ - UserFactory.create(username="metric" + str(__)) - for __ in range(USER_COUNT) - ] - - for user in self.users: - CourseEnrollmentFactory.create(user=user, course_id=self.course.id) - - for i, item in enumerate(self.items): - for j, user in enumerate(self.users): - StudentModuleFactory.create( - grade=1 if i < j else 0, - max_grade=1 if i < j else 0.5, - student=user, - course_id=self.course.id, - module_state_key=item.location, - state=json.dumps({'attempts': self.attempts}), - ) - for j, user in enumerate(self.users): - StudentModuleFactory.create( - course_id=self.course.id, - module_type='sequential', - module_state_key=item.location, - ) - - def test_get_problem_grade_distribution(self): - - prob_grade_distrib, total_student_count = get_problem_grade_distribution(self.course.id) - - for problem in prob_grade_distrib: - max_grade = prob_grade_distrib[problem]['max_grade'] - self.assertEqual(1, max_grade) - - for val in total_student_count.values(): - self.assertEqual(USER_COUNT, val) - - def test_get_sequential_open_distibution(self): - - sequential_open_distrib = get_sequential_open_distrib(self.course.id) - - for problem in sequential_open_distrib: - num_students = sequential_open_distrib[problem] - self.assertEqual(USER_COUNT, num_students) - - def test_get_problemset_grade_distrib(self): - - prob_grade_distrib, __ = get_problem_grade_distribution(self.course.id) - probset_grade_distrib = get_problem_set_grade_distrib(self.course.id, prob_grade_distrib) - - for problem in probset_grade_distrib: - max_grade = probset_grade_distrib[problem]['max_grade'] - self.assertEqual(1, max_grade) - - grade_distrib = probset_grade_distrib[problem]['grade_distrib'] - sum_attempts = 0 - for item in grade_distrib: - sum_attempts += item[1] - self.assertEqual(USER_COUNT, sum_attempts) - - def test_get_d3_problem_grade_distrib(self): - - d3_data = get_d3_problem_grade_distrib(self.course.id) - for data in d3_data: - for stack_data in data['data']: - sum_values = 0 - for problem in stack_data['stackData']: - sum_values += problem['value'] - self.assertEqual(USER_COUNT, sum_values) - - def test_get_d3_sequential_open_distrib(self): - - d3_data = get_d3_sequential_open_distrib(self.course.id) - - for data in d3_data: - for stack_data in data['data']: - for problem in stack_data['stackData']: - value = problem['value'] - self.assertEqual(0, value) - - def test_get_d3_section_grade_distrib(self): - - d3_data = get_d3_section_grade_distrib(self.course.id, 0) - - for stack_data in d3_data: - sum_values = 0 - for problem in stack_data['stackData']: - sum_values += problem['value'] - self.assertEqual(USER_COUNT, sum_values) - - def test_get_students_problem_grades(self): - - attributes = '?module_id=' + text_type(self.item.location) - request = self.request_factory.get(reverse('get_students_problem_grades') + attributes) - - response = get_students_problem_grades(request) - response_content = json.loads(response.content.decode('utf-8'))['results'] - response_max_exceeded = json.loads(response.content.decode('utf-8'))['max_exceeded'] - - self.assertEqual(USER_COUNT, len(response_content)) - self.assertEqual(False, response_max_exceeded) - for item in response_content: - if item['grade'] == 0: - self.assertEqual(0, item['percent']) - else: - self.assertEqual(100, item['percent']) - - def test_get_students_problem_grades_max(self): - - with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2): - attributes = '?module_id=' + text_type(self.item.location) - request = self.request_factory.get(reverse('get_students_problem_grades') + attributes) - - response = get_students_problem_grades(request) - response_results = json.loads(response.content.decode('utf-8'))['results'] - response_max_exceeded = json.loads(response.content.decode('utf-8'))['max_exceeded'] - - # Only 2 students in the list and response_max_exceeded is True - self.assertEqual(2, len(response_results)) - self.assertEqual(True, response_max_exceeded) - - def test_get_students_problem_grades_csv(self): - - tooltip = u'P1.2.1 Q1 - 3382 Students (100%: 1/1 questions)' - attributes = '?module_id=' + text_type(self.item.location) + '&tooltip=' + tooltip + '&csv=true' - request = self.request_factory.get(reverse('get_students_problem_grades') + attributes) - - response = get_students_problem_grades(request) - # Check header and a row for each student in csv response - self.assertContains(response, '"Name","Username","Grade","Percent"') - self.assertContains(response, '"metric0","0.0","0.0"') - self.assertContains(response, '"metric1","0.0","0.0"') - self.assertContains(response, '"metric2","0.0","0.0"') - self.assertContains(response, '"metric3","0.0","0.0"') - self.assertContains(response, '"metric4","0.0","0.0"') - self.assertContains(response, '"metric5","0.0","0.0"') - self.assertContains(response, '"metric6","0.0","0.0"') - self.assertContains(response, '"metric7","0.0","0.0"') - self.assertContains(response, '"metric8","0.0","0.0"') - self.assertContains(response, '"metric9","0.0","0.0"') - self.assertContains(response, '"metric10","1.0","100.0"') - - def test_get_students_opened_subsection(self): - - attributes = '?module_id=' + text_type(self.item.location) - request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes) - - response = get_students_opened_subsection(request) - response_results = json.loads(response.content.decode('utf-8'))['results'] - response_max_exceeded = json.loads(response.content.decode('utf-8'))['max_exceeded'] - self.assertEqual(USER_COUNT, len(response_results)) - self.assertEqual(False, response_max_exceeded) - - def test_get_students_opened_subsection_max(self): - - with patch('class_dashboard.dashboard_data.MAX_SCREEN_LIST_LENGTH', 2): - - attributes = '?module_id=' + text_type(self.item.location) - request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes) - - response = get_students_opened_subsection(request) - response_results = json.loads(response.content.decode('utf-8'))['results'] - response_max_exceeded = json.loads(response.content.decode('utf-8'))['max_exceeded'] - - # Only 2 students in the list and response_max_exceeded is True - self.assertEqual(2, len(response_results)) - self.assertEqual(True, response_max_exceeded) - - def test_get_students_opened_subsection_csv(self): - - tooltip = '4162 students opened Subsection 5: Relational Algebra Exercises' - attributes = '?module_id=' + text_type(self.item.location) + '&tooltip=' + tooltip + '&csv=true' - request = self.request_factory.get(reverse('get_students_opened_subsection') + attributes) - - response = get_students_opened_subsection(request) - self.assertContains(response, '"Name","Username"') - # Check response contains 1 line for each user +1 for the header - self.assertEqual(USER_COUNT + 1, len(response.content.splitlines())) - - def test_post_metrics_data_subsections_csv(self): - - url = reverse('post_metrics_data_csv') - - sections = json.dumps(["Introduction"]) - tooltips = json.dumps([[{"subsection_name": "Pre-Course Survey", "subsection_num": 1, "type": "subsection", "num_students": 18963}]]) - course_id = self.course.id - data_type = 'subsection' - - data = json.dumps({'sections': sections, - 'tooltips': tooltips, - 'course_id': text_type(course_id), - 'data_type': data_type, - }) - - response = self.client.post(url, {'data': data}) - # Check response contains 1 line for header, 1 line for Section and 1 line for Subsection - self.assertEqual(3, len(response.content.splitlines())) - - def test_post_metrics_data_problems_csv(self): - - url = reverse('post_metrics_data_csv') - - sections = json.dumps(["Introduction"]) - tooltips = json.dumps([[[ - {'student_count_percent': 0, - 'problem_name': 'Q1', - 'grade': 0, - 'percent': 0, - 'label': 'P1.2.1', - 'max_grade': 1, - 'count_grade': 26, - 'type': u'problem'}, - {'student_count_percent': 99, - 'problem_name': 'Q1', - 'grade': 1, - 'percent': 100, - 'label': 'P1.2.1', - 'max_grade': 1, - 'count_grade': 4763, - 'type': 'problem'}, - ]]]) - course_id = self.course.id - data_type = 'problem' - - data = json.dumps({'sections': sections, - 'tooltips': tooltips, - 'course_id': text_type(course_id), - 'data_type': data_type, - }) - - response = self.client.post(url, {'data': data}) - # Check response contains 1 line for header, 1 line for Sections and 2 lines for problems - self.assertEqual(4, len(response.content.splitlines())) - - def test_get_section_display_name(self): - - section_display_name = get_section_display_name(self.course.id) - self.assertMultiLineEqual(section_display_name[0], u"test factory section omega \u03a9") - - def test_get_array_section_has_problem(self): - - b_section_has_problem = get_array_section_has_problem(self.course.id) - self.assertEqual(b_section_has_problem[0], True) - - def test_has_instructor_access_for_class(self): - """ - Test for instructor access - """ - ret_val = bool(has_instructor_access_for_class(self.instructor, self.course.id)) - self.assertEqual(ret_val, True) diff --git a/lms/djangoapps/class_dashboard/tests/test_views.py b/lms/djangoapps/class_dashboard/tests/test_views.py deleted file mode 100644 index b9c8ab84c0..0000000000 --- a/lms/djangoapps/class_dashboard/tests/test_views.py +++ /dev/null @@ -1,113 +0,0 @@ -""" -Tests for class dashboard (Metrics tab in instructor dashboard) -""" - - -import json - -from django.test.client import RequestFactory -from mock import patch -from six import text_type - -from class_dashboard import views -from student.tests.factories import AdminFactory -from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase -from xmodule.modulestore.tests.factories import CourseFactory - - -class TestViews(ModuleStoreTestCase): - """ - Tests related to class_dashboard/views.py - """ - - def setUp(self): - super(TestViews, self).setUp() - - self.request_factory = RequestFactory() - self.request = self.request_factory.get('') - self.request.user = None - self.simple_data = {'error': 'error'} - - @patch('class_dashboard.views.has_instructor_access_for_class') - def test_all_problem_grade_distribution_has_access(self, has_access): - """ - Test returns proper value when have proper access - """ - has_access.return_value = True - response = views.all_problem_grade_distribution(self.request, 'test/test/test') - - self.assertEqual(json.dumps(self.simple_data), response.content.decode('utf-8')) - - @patch('class_dashboard.views.has_instructor_access_for_class') - def test_all_problem_grade_distribution_no_access(self, has_access): - """ - Test for no access - """ - has_access.return_value = False - response = views.all_problem_grade_distribution(self.request, 'test/test/test') - - self.assertEqual( - "{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", - response.content.decode('utf-8') - ) - - @patch('class_dashboard.views.has_instructor_access_for_class') - def test_all_sequential_open_distribution_has_access(self, has_access): - """ - Test returns proper value when have proper access - """ - has_access.return_value = True - response = views.all_sequential_open_distrib(self.request, 'test/test/test') - - self.assertEqual(json.dumps(self.simple_data), response.content.decode('utf-8')) - - @patch('class_dashboard.views.has_instructor_access_for_class') - def test_all_sequential_open_distribution_no_access(self, has_access): - """ - Test for no access - """ - has_access.return_value = False - response = views.all_sequential_open_distrib(self.request, 'test/test/test') - - self.assertEqual( - "{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", - response.content.decode('utf-8') - ) - - @patch('class_dashboard.views.has_instructor_access_for_class') - def test_section_problem_grade_distribution_has_access(self, has_access): - """ - Test returns proper value when have proper access - """ - has_access.return_value = True - response = views.section_problem_grade_distrib(self.request, 'test/test/test', '1') - - self.assertEqual(json.dumps(self.simple_data), response.content.decode('utf-8')) - - @patch('class_dashboard.views.has_instructor_access_for_class') - def test_section_problem_grade_distribution_no_access(self, has_access): - """ - Test for no access - """ - has_access.return_value = False - response = views.section_problem_grade_distrib(self.request, 'test/test/test', '1') - - self.assertEqual( - "{\"error\": \"Access Denied: User does not have access to this course\'s data\"}", - response.content.decode('utf-8') - ) - - def test_sending_deprecated_id(self): - - course = CourseFactory.create() - instructor = AdminFactory.create() - self.request.user = instructor - - response = views.all_sequential_open_distrib(self.request, text_type(course.id)) - self.assertEqual('[]', response.content.decode('utf-8')) - - response = views.all_problem_grade_distribution(self.request, text_type(course.id)) - self.assertEqual('[]', response.content.decode('utf-8')) - - response = views.section_problem_grade_distrib(self.request, text_type(course.id), 'no section') - self.assertEqual('{"error": "error"}', response.content.decode('utf-8')) diff --git a/lms/djangoapps/class_dashboard/urls.py b/lms/djangoapps/class_dashboard/urls.py deleted file mode 100644 index dbc2670a16..0000000000 --- a/lms/djangoapps/class_dashboard/urls.py +++ /dev/null @@ -1,37 +0,0 @@ -""" -Class Dashboard API endpoint urls. -""" - - -from django.conf import settings -from django.conf.urls import url - -import class_dashboard.dashboard_data -import class_dashboard.views - -COURSE_ID_PATTERN = settings.COURSE_ID_PATTERN - -urlpatterns = [ - # Json request data for metrics for entire course - url(r'^{}/all_sequential_open_distrib$'.format(settings.COURSE_ID_PATTERN), - class_dashboard.views.all_sequential_open_distrib, name="all_sequential_open_distrib"), - - url(r'^{}/all_problem_grade_distribution$'.format(settings.COURSE_ID_PATTERN), - class_dashboard.views.all_problem_grade_distribution, name="all_problem_grade_distribution"), - - # Json request data for metrics for particular section - url(r'^{}/problem_grade_distribution/(?P
\d+)$'.format(settings.COURSE_ID_PATTERN), - class_dashboard.views.section_problem_grade_distrib, name="section_problem_grade_distrib"), - - # For listing students that opened a sub-section - url(r'^get_students_opened_subsection$', - class_dashboard.dashboard_data.get_students_opened_subsection, name="get_students_opened_subsection"), - - # For listing of students' grade per problem - url(r'^get_students_problem_grades$', - class_dashboard.dashboard_data.get_students_problem_grades, name="get_students_problem_grades"), - - # For generating metrics data as a csv - url(r'^post_metrics_data_csv_url', - class_dashboard.dashboard_data.post_metrics_data_csv, name="post_metrics_data_csv"), -] diff --git a/lms/djangoapps/class_dashboard/views.py b/lms/djangoapps/class_dashboard/views.py deleted file mode 100644 index 16cb40054f..0000000000 --- a/lms/djangoapps/class_dashboard/views.py +++ /dev/null @@ -1,109 +0,0 @@ -""" -Handles requests for data, returning a json -""" - - -import json -import logging - -from django.http import HttpResponse -from opaque_keys.edx.keys import CourseKey - -from class_dashboard import dashboard_data -from lms.djangoapps.courseware.access import has_access -from lms.djangoapps.courseware.courses import get_course_overview_with_access - -log = logging.getLogger(__name__) - - -def has_instructor_access_for_class(user, course_id): - """ - Returns true if the `user` is an instructor for the course. - """ - - course = get_course_overview_with_access(user, 'staff', course_id) - return bool(has_access(user, 'staff', course)) - - -def all_sequential_open_distrib(request, course_id): - """ - Creates a json with the open distribution for all the subsections in the course. - - `request` django request - - `course_id` the course ID for the course interested in - - Returns the format in dashboard_data.get_d3_sequential_open_distrib - """ - - data = {} - - # Only instructor for this particular course can request this information - course_key = CourseKey.from_string(course_id) - if has_instructor_access_for_class(request.user, course_key): - try: - data = dashboard_data.get_d3_sequential_open_distrib(course_key) - except Exception as ex: # pylint: disable=broad-except - log.error(u'Generating metrics failed with exception: %s', ex) - data = {'error': "error"} - else: - data = {'error': "Access Denied: User does not have access to this course's data"} - - return HttpResponse(json.dumps(data), content_type="application/json") - - -def all_problem_grade_distribution(request, course_id): - """ - Creates a json with the grade distribution for all the problems in the course. - - `Request` django request - - `course_id` the course ID for the course interested in - - Returns the format in dashboard_data.get_d3_problem_grade_distrib - """ - data = {} - - # Only instructor for this particular course can request this information - course_key = CourseKey.from_string(course_id) - if has_instructor_access_for_class(request.user, course_key): - try: - data = dashboard_data.get_d3_problem_grade_distrib(course_key) - except Exception as ex: # pylint: disable=broad-except - log.error(u'Generating metrics failed with exception: %s', ex) - data = {'error': "error"} - else: - data = {'error': "Access Denied: User does not have access to this course's data"} - - return HttpResponse(json.dumps(data), content_type="application/json") - - -def section_problem_grade_distrib(request, course_id, section): - """ - Creates a json with the grade distribution for the problems in the specified section. - - `request` django request - - `course_id` the course ID for the course interested in - - `section` The zero-based index of the section for the course - - Returns the format in dashboard_data.get_d3_section_grade_distrib - - If this is requested multiple times quickly for the same course, it is better to call all_problem_grade_distribution - and pick out the sections of interest. - """ - data = {} - - # Only instructor for this particular course can request this information - course_key = CourseKey.from_string(course_id) - if has_instructor_access_for_class(request.user, course_key): - try: - data = dashboard_data.get_d3_section_grade_distrib(course_key, section) - except Exception as ex: # pylint: disable=broad-except - log.error(u'Generating metrics failed with exception: %s', ex) - data = {'error': "error"} - else: - data = {'error': "Access Denied: User does not have access to this course's data"} - - return HttpResponse(json.dumps(data), content_type="application/json") diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 61bd7a079e..aa87e18c5f 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -30,7 +30,6 @@ from xblock.field_data import DictFieldData from xblock.fields import ScopeIds from bulk_email.api import is_bulk_email_feature_enabled -from class_dashboard.dashboard_data import get_array_section_has_problem, get_section_display_name from course_modes.models import CourseMode, CourseModesArchive from edxmako.shortcuts import render_to_response from lms.djangoapps.certificates import api as certs_api @@ -174,10 +173,6 @@ def instructor_dashboard_2(request, course_id): if is_bulk_email_feature_enabled(course_key): sections.append(_section_send_email(course, access)) - # Gate access to Metrics tab by featue flag and staff authorization - if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: - sections.append(_section_metrics(course, access)) - # Gate access to Ecommerce tab if course_mode_has_price and (access['finance_admin'] or access['sales_admin']): sections.append(_section_e_commerce(course, access, paid_modes[0], is_white_label, reports_enabled)) @@ -798,23 +793,6 @@ def _section_analytics(course, access): return section_data -def _section_metrics(course, access): - """Provide data for the corresponding dashboard section """ - course_key = course.id - section_data = { - 'section_key': 'metrics', - 'section_display_name': _('Metrics'), - 'access': access, - 'course_id': six.text_type(course_key), - 'sub_section_display_name': get_section_display_name(course_key), - 'section_has_problem': get_array_section_has_problem(course_key), - 'get_students_opened_subsection_url': reverse('get_students_opened_subsection'), - 'get_students_problem_grades_url': reverse('get_students_problem_grades'), - 'post_metrics_data_csv_url': reverse('post_metrics_data_csv'), - } - return section_data - - def _section_open_response_assessment(request, course, openassessment_blocks, access): """Provide data for the corresponding dashboard section """ course_key = course.id diff --git a/lms/envs/common.py b/lms/envs/common.py index a1b054f09b..68d52779d9 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -2708,11 +2708,6 @@ VERIFICATION_EXPIRY_EMAIL = { DISABLE_ACCOUNT_ACTIVATION_REQUIREMENT_SWITCH = "verify_student_disable_account_activation_requirement" -### This enables the Metrics tab for the Instructor dashboard ########### -FEATURES['CLASS_DASHBOARD'] = False -if FEATURES.get('CLASS_DASHBOARD'): - INSTALLED_APPS.append('class_dashboard') - ################ Enable credit eligibility feature #################### ENABLE_CREDIT_ELIGIBILITY = True FEATURES['ENABLE_CREDIT_ELIGIBILITY'] = ENABLE_CREDIT_ELIGIBILITY diff --git a/lms/templates/class_dashboard/all_section_metrics.js b/lms/templates/class_dashboard/all_section_metrics.js deleted file mode 100644 index 05edde730f..0000000000 --- a/lms/templates/class_dashboard/all_section_metrics.js +++ /dev/null @@ -1,108 +0,0 @@ -<%page args="id_opened_prefix, id_grade_prefix, id_attempt_prefix, id_tooltip_prefix, course_id, allSubsectionTooltipArr, allProblemTooltipArr, **kwargs"/> -<%! - import json - from django.urls import reverse - from six import text_type -%> - -$(function () { - - d3.json("${reverse('all_sequential_open_distrib', kwargs=dict(course_id=text_type(course_id)))}", function(error, json) { - var section, paramOpened, barGraphOpened, error; - var i, curr_id; - var errorMessage = gettext('Unable to retrieve data, please try again later.'); - - error = json.error; - if (error) { - $('.metrics-left .loading').text(errorMessage); - return - } - - i = 0; - for (section in json) { - curr_id = "#${id_opened_prefix}"+i; - paramOpened = { - data: json[section].data, - width: $(curr_id).width(), - height: $(curr_id).height()-25, // Account for header - tag: "opened"+i, - bVerticalXAxisLabel : true, - bLegend : false, - margin: {left:0}, - }; - - // Construct array of tooltips for all sections for the "Download Subsection Data" button. - var sectionTooltipArr = new Array(); - paramOpened.data.forEach( function(element, index, array) { - sectionTooltipArr[index] = element.stackData[0].tooltip; - }); - allSubsectionTooltipArr[i] = sectionTooltipArr; - - barGraphOpened = edx_d3CreateStackedBarGraph(paramOpened, d3.select(curr_id).append("svg"), - d3.select("#${id_tooltip_prefix}"+i)); - barGraphOpened.scale.stackColor.range(["#555555","#555555"]); - - if (paramOpened.data.length > 0) { - barGraphOpened.drawGraph(); - - $('svg').siblings('.loading').remove(); - } else { - $('svg').siblings('.loading').text(errorMessage); - } - - i+=1; - } - }); - - d3.json("${reverse('all_problem_grade_distribution', kwargs=dict(course_id=text_type(course_id)))}", function(error, json) { - var section, paramGrade, barGraphGrade, error; - var i, curr_id; - var errorMessage = gettext('Unable to retrieve data, please try again later.'); - - error = json.error; - if (error) { - $('.metrics-right .loading').text(errorMessage); - return - } - - i = 0; - for (section in json) { - curr_id = "#${id_grade_prefix}"+i; - paramGrade = { - data: json[section].data, - width: $(curr_id).width(), - height: $(curr_id).height()-25, // Account for header - tag: "grade"+i, - bVerticalXAxisLabel : true, - }; - - // Construct array of tooltips for all sections for the "Download Problem Data" button. - var sectionTooltipArr = new Array(); - paramGrade.data.forEach( function(element, index, array) { - var stackDataArr = new Array(); - for (var j = 0; j < element.stackData.length; j++) { - stackDataArr[j] = element.stackData[j].tooltip - } - sectionTooltipArr[index] = stackDataArr; - }); - allProblemTooltipArr[i] = sectionTooltipArr; - - barGraphGrade = edx_d3CreateStackedBarGraph(paramGrade, d3.select(curr_id).append("svg"), - d3.select("#${id_tooltip_prefix}"+i)); - barGraphGrade.scale.stackColor.domain([0,50,100]).range(["#e13f29","#cccccc","#17a74d"]); - barGraphGrade.legend.width += 2; - - if ( paramGrade.data.length > 0 ) { - barGraphGrade.drawGraph(); - - $('svg').siblings('.loading').remove(); - } else { - $('svg').siblings('.loading').text(errorMessage); - } - - i+=1; - } - - }); - -}); diff --git a/lms/templates/class_dashboard/d3_stacked_bar_graph.js b/lms/templates/class_dashboard/d3_stacked_bar_graph.js deleted file mode 100644 index 2b4ca936de..0000000000 --- a/lms/templates/class_dashboard/d3_stacked_bar_graph.js +++ /dev/null @@ -1,445 +0,0 @@ -/* -There are three parameters: -(1) Parameter is of type object. Inside can include (* marks required): - data* - Array of objects with key, value pairs that represent a single stack of bars: - xValue - Corresponding value for the x-axis - stackData - Array of objects with key, value pairs that represent a bar: - color - Defines what "color" the bar will map to - value - Maps to the height of the bar, along the y-axis - tooltip - (Optional) Text to display on mouse hover - - height - Height of the SVG the graph will be displayed in (default: 500) - - width - Width of the SVG the graph will be displayed in (default: 500) - - margin - Object with key, value pairs for the graph's margins within the SVG (default for all: 10) - top - Top margin - bottom - Bottom margin - right - Right margin - left - Left margin - - yRange - Array of two values, representing the min and max respectively. (default: [0, ]) - - xRange - Array of either the min and max or ordered ordinals (default: calculated min and max or ordered ordinals given in data) - - colorRange - Array of either the min and max or ordered ordinals (default: calculated min and max or ordered ordinals given in data) - - bVerticalXAxisLabel - Boolean whether to make the labels in the x-axis veritcal (default: false) - - bLegend - Boolean if false does not create the graph with a legend (default: true) - -(2) Parameter is a d3 pointer to the SVG the graph will draw itself in. - -(3) Parameter is a d3 pointer to a div that will be used for the graph's tooltip. - -****Does not actually draw graph.**** Returns an object that includes a function - drawGraph, for when ready to draw graph. Reason for this is, because of all - the defaults, some changes may be needed before drawing the graph - -returns an object with the following: - state - All information that can be put in parameters and adding: - margin.axisX - margin to accomodate the x-axis - margin.axisY - margin to acommodate the y-axis - - drawGraph - function to call when ready to draw graph - - scale - Object containing three d3 scales - x - d3 scale for the x-axis - y - d3 scale for the y-axis - stackColor - d3 scale for the stack color - - axis - Object containg the graph's two d3 axis - x - d3 axis for the x-axis - y - d3 axis for the y-axis - - svg - d3 pointer to the svg holding the graph - - svgGroup - object holding the svg groups - main - svg group holding all other groups - xAxis - svg group holding the x-axis - yAxis - svg group holding the x-axis - bars - svg groups holding the bars - - yAxisLabel - d3 pointer to the text component that holds the y axis label - - divTooltip - d3 pointer to the div that is used as the tooltip for the graph - - rects - d3 collection of the rects used in the bars - - legend - object containing information for the legend - height - height of the legend - width - width of the legend (if change, need to update state.margin.axisY also) - range - array of values that appears in the legend - barHeight - height of a bar in the legend, based on height and length of range -*/ - -edx_d3CreateStackedBarGraph = function(parameters, svg, divTooltip) { - var graph = { - svg : svg, - state : { - data : undefined, - height : 500, - width : 500, - margin: {top: 10, bottom: 10, right: 10, left: 10}, - yRange: [0], - xRange : undefined, - colorRange : undefined, - tag : "", - bVerticalXAxisLabel : false, - bLegend : true, - }, - divTooltip : divTooltip, - }; - - var state = graph.state; - - // Handle parameters - state.data = parameters.data; - - if (parameters.margin != undefined) { - for (var key in state.margin) { - if ((state.margin.hasOwnProperty(key) && - (parameters.margin[key] != undefined))) { - state.margin[key] = parameters.margin[key]; - } - } - } - - for (var key in state) { - if ((key != "data") && (key != "margin")) { - if (state.hasOwnProperty(key) && (parameters[key] != undefined)) { - state[key] = parameters[key]; - } - } - } - - if (state.tag != "") - state.tag = state.tag+"-"; - - if ((state.xRange == undefined) || (state.yRange.length < 2 || - state.colorRange == undefined)) { - var aryXRange = []; - var bXIsOrdinal = false; - var maxYRange = 0; - var aryColorRange = []; - var bColorIsOrdinal = false; - - for (var stackKey in state.data) { - var stack = state.data[stackKey]; - aryXRange.push(stack.xValue); - if (isNaN(stack.xValue)) - bXIsOrdinal = true; - - var valueTotal = 0; - for (var barKey in stack.stackData) { - var bar = stack.stackData[barKey]; - valueTotal += bar.value; - - if (isNaN(bar.color)) - bColorIsOrdinal = true; - - if (aryColorRange.indexOf(bar.color) < 0) - aryColorRange.push(bar.color); - } - if (maxYRange < valueTotal) - maxYRange = valueTotal; - } - - if (state.xRange == undefined){ - if (bXIsOrdinal) - state.xRange = aryXRange; - else - state.xRange = [ - Math.min.apply(null,aryXRange), - Math.max.apply(null,aryXRange) - ]; - } - - if (state.yRange.length < 2) - state.yRange[1] = maxYRange; - - if (state.colorRange == undefined){ - if (bColorIsOrdinal) - state.colorRange = aryColorRange; - else - state.colorRange = [ - Math.min.apply(null,aryColorRange), - Math.max.apply(null,aryColorRange) - ]; - } - } - - // Find needed spacing for axes - var tmpEl = graph.svg.append("text").text(state.yRange[1]+"1234") - .attr("id",state.tag+"stacked-bar-graph-long-str"); - state.margin.axisY = document.getElementById(state.tag+"stacked-bar-graph-long-str") - .getComputedTextLength()+state.margin.left; - - var longestXAxisStr = ""; - if (isNaN(state.xRange[0])) { - for (var i in state.xRange) { - if (longestXAxisStr.length < state.xRange[i].length) - longestXAxisStr = state.xRange[i]+"1234"; - } - } else { - longestXAxisStr = state.xRange[1]+"1234"; - } - - tmpEl.text(longestXAxisStr); - if (state.bVerticalXAxisLabel) { - state.margin.axisX = document.getElementById(state.tag+"stacked-bar-graph-long-str") - .getComputedTextLength()+state.margin.bottom; - } else { - state.margin.axisX = document.getElementById(state.tag+"stacked-bar-graph-long-str") - .clientHeight+state.margin.bottom; - } - - tmpEl.remove(); - - // Add y0 and y1 of the y-axis based on the count and order of the colorRange. - // First, case if color is a number range - if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) && - !(isNaN(state.colorRange[1]))) { - for (var stackKey in state.data) { - var stack = state.data[stackKey]; - stack.stackData.sort(function(a,b) { return a.color - b.color; }); - - var currTotal = 0; - for (var barKey in stack.stackData) { - var bar = stack.stackData[barKey]; - bar.y0 = currTotal; - currTotal += bar.value; - bar.y1 = currTotal; - } - } - } else { - for (var stackKey in state.data) { - var stack = state.data[stackKey]; - - var tmpStackData = []; - for (var barKey in stack.stackData) { - var bar = stack.stackData[barKey]; - tmpStackData[state.colorRange.indexOf(bar.color)] = bar; - } - stack.stackData = tmpStackData; - - var currTotal = 0; - for (var barKey in stack.stackData) { - var bar = stack.stackData[barKey]; - bar.y0 = currTotal; - currTotal += bar.value; - bar.y1 = currTotal; - } - } - } - - // Add information to create legend - if (state.bLegend) { - graph.legend = { - height : (state.height-state.margin.top-state.margin.axisX), - width : 30, - range : state.colorRange, - }; - if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) && - !(isNaN(state.colorRange[1]))) { - graph.legend.range = []; - - var i = 0; - var min = state.colorRange[0]; - var max = state.colorRange[1]; - while (i <= 10) { - graph.legend.range[i] = min+((max-min)/10)*i; - i += 1; - } - } - graph.legend.barHeight = graph.legend.height/graph.legend.range.length; - - // Shifting the axis over to make room - graph.state.margin.axisY += graph.legend.width; - } - - // Make the scales - graph.scale = { - x: d3.scale.ordinal() - .domain(graph.state.xRange) - .rangeRoundBands([ - (graph.state.margin.axisY), - (graph.state.width-graph.state.margin.right)], - .3), - - y: d3.scale.linear() - .domain(graph.state.yRange) // yRange is the range of the y-axis values - .range([ - (graph.state.height-graph.state.margin.axisX), - graph.state.margin.top - ]), - - stackColor: d3.scale.ordinal() - .domain(graph.state.colorRange) - .range(["#ffeeee","#ffebeb","#ffd8d8","#ffc4c4","#ffb1b1","#ff9d9d","#ff8989","#ff7676","#ff6262","#ff4e4e","#ff3b3b"]) - }; - - if ((state.colorRange.length == 2) && !(isNaN(state.colorRange[0])) && - !(isNaN(state.colorRange[1]))) { - graph.scale.stackColor = d3.scale.linear() - .domain(state.colorRange) - .range(["#e13f29","#17a74d"]); - } - - // Setup axes - graph.axis = { - x: d3.svg.axis() - .scale(graph.scale.x), - y: d3.svg.axis() - .scale(graph.scale.y), - } - - graph.axis.x.orient("bottom"); - graph.axis.y.orient("left"); - - // Draw graph function, to call when ready. - graph.drawGraph = function() { - var graph = this; - - // Steup SVG - graph.svg.attr("id", graph.state.tag+"stacked-bar-graph") - .attr("class", "stacked-bar-graph") - .attr("width", graph.state.width) - .attr("height", graph.state.height); - graph.svgGroup = {}; - - graph.svgGroup.main = graph.svg.append("g"); - - // Draw Bars - graph.svgGroup.bars = graph.svgGroup.main.selectAll(".stacked-bar") - .data(graph.state.data) - .enter().append("g") - .attr("class", "stacked-bar") - .attr("transform", function(d) { - return "translate("+graph.scale.x(d.xValue)+",0)"; - }); - - graph.rects = graph.svgGroup.bars.selectAll("rect") - .data(function(d) { return d.stackData; }) - .enter().append("rect") - .attr("width", function(d) { - return graph.scale.x.rangeBand() - }) - .attr("y", function(d) { return graph.scale.y(d.y1); }) - .attr("height", function(d) { - return graph.scale.y(d.y0) - graph.scale.y(d.y1); - }) - .attr("id", function(d) { return d.module_url }) - .style("fill", function(d) { return graph.scale.stackColor(d.color); }) - .style("stroke", "white") - .style("stroke-width", "0.5px"); - - // Setup tooltip - if (graph.divTooltip != undefined) { - graph.divTooltip - .style("position", "absolute") - .style("z-index", "10") - .style("visibility", "hidden"); - } - - graph.rects - .on("mouseover", function(d) { - var pos = d3.mouse(graph.divTooltip.node().parentNode); - var left = pos[0]+10; - var top = pos[1]-10; - var width = $('#'+graph.divTooltip.attr("id")).width(); - - // Construct the tooltip - if (d.tooltip['type'] == 'subsection') { - stud_str = ngettext('%(num_students)s student opened Subsection', '%(num_students)s students opened Subsection', d.tooltip['num_students']); - stud_str = interpolate(stud_str, {'num_students': d.tooltip['num_students']}, true); - tooltip_str = stud_str + ' ' + d.tooltip['subsection_num'] + ': ' + d.tooltip['subsection_name']; - }else if (d.tooltip['type'] == 'problem') { - stud_str = ngettext('%(num_students)s student', '%(num_students)s students', d.tooltip['count_grade']); - stud_str = interpolate(stud_str, {'num_students': d.tooltip['count_grade']}, true); - q_str = ngettext('%(num_questions)s question', '%(num_questions)s questions', d.tooltip['max_grade']); - q_str = interpolate(q_str, {'num_questions': d.tooltip['max_grade']}, true); - - tooltip_str = d.tooltip['label'] + ' ' + d.tooltip['problem_name'] + ' - ' \ - + stud_str + ' (' + d.tooltip['student_count_percent'] + '%) (' \ - + d.tooltip['percent'] + '%: ' + d.tooltip['grade'] +'/' \ - + q_str + ')'; - } - graph.divTooltip.style("visibility", "visible") - .text(tooltip_str); - - if ((left+width+30) > $("#"+graph.divTooltip.node().parentNode.id).width()) - left -= (width+30); - - graph.divTooltip.style("top", top+"px") - .style("left", left+"px"); - }) - .on("mouseout", function(d){ - graph.divTooltip.style("visibility", "hidden") - }); - - // Add legend - if (graph.state.bLegend) { - graph.svgGroup.legendG = graph.svgGroup.main.append("g") - .attr("class","stacked-bar-graph-legend") - .attr("transform","translate("+graph.state.margin.left+","+ - graph.state.margin.top+")"); - graph.svgGroup.legendGs = graph.svgGroup.legendG.selectAll(".stacked-bar-graph-legend-g") - .data(graph.legend.range) - .enter().append("g") - .attr("class","stacked-bar-graph-legend-g") - .attr("id",function(d,i) { return graph.state.tag+"legend-"+i; }) - .attr("transform", function(d,i) { - return "translate(0,"+ - (graph.state.height-graph.state.margin.axisX-((i+1)*(graph.legend.barHeight))) + ")"; - }); - - graph.svgGroup.legendGs.append("rect") - .attr("class","stacked-bar-graph-legend-rect") - .attr("height", graph.legend.barHeight) - .attr("width", graph.legend.width) - .style("fill", graph.scale.stackColor) - .style("stroke", "white"); - - graph.svgGroup.legendGs.append("text") - .attr("class","axis-label") - .attr("transform", function(d) { - var str = "translate("+(graph.legend.width/2)+","+ - (graph.legend.barHeight/2)+")"; - return str; - }) - .attr("dy", ".35em") - .attr("dx", "-1px") - .style("text-anchor", "middle") - .text(function(d,i) { return d; }); - } - - - // Draw Axes - graph.svgGroup.xAxis = graph.svgGroup.main.append("g") - .attr("class","stacked-bar-graph-axis") - .attr("id",graph.state.tag+"x-axis"); - - var tmpS = "translate(0,"+(graph.state.height-graph.state.margin.axisX)+")"; - if (graph.state.bVerticalXAxisLabel) { - graph.axis.x.orient("left"); - tmpS = "rotate(270), translate(-"+(graph.state.height-graph.state.margin.axisX)+",0)"; - } - graph.svgGroup.xAxis.attr("transform", tmpS) - .call(graph.axis.x); - - graph.svgGroup.yAxis = graph.svgGroup.main.append("g") - .attr("class","stacked-bar-graph-axis") - .attr("id",graph.state.tag+"y-axis") - .attr("transform","translate("+ - (graph.state.margin.axisY)+",0)") - .call(graph.axis.y); - graph.yAxisLabel = graph.svgGroup.yAxis.append("text") - .attr("dy","1em") - .attr("transform","rotate(-90)") - .style("text-anchor","end") - .text(gettext("Number of Students")); - }; - - return graph; -}; diff --git a/lms/templates/instructor/instructor_dashboard_2/metrics.html b/lms/templates/instructor/instructor_dashboard_2/metrics.html deleted file mode 100644 index dfe6c6b63e..0000000000 --- a/lms/templates/instructor/instructor_dashboard_2/metrics.html +++ /dev/null @@ -1,276 +0,0 @@ -<%! -from django.utils.translation import ugettext as _ -from django.template.defaultfilters import escapejs -%> - -<%page args="section_data"/> - - - -%if not any (section_data.values()): -

${_("There is no data available to display at this time.")}

-%else: - <%namespace name="d3_stacked_bar_graph" file="/class_dashboard/d3_stacked_bar_graph.js"/> - <%namespace name="all_section_metrics" file="/class_dashboard/all_section_metrics.js"/> -
-

${_("Use Reload Graphs to refresh the graphs.")}

-

-
-
-
-

${_("Subsection Data")}

-

${_("Each bar shows the number of students that opened the subsection.")}

-

${_("You can click on any of the bars to list the students that opened the subsection.")}

-

${_("You can also download this data as a CSV file.")}

-

-
-
-

${_("Grade Distribution Data")}

-

${_("Each bar shows the grade distribution for that problem.")}

-

${_("You can click on any of the bars to list the students that attempted the problem, along with the grades they received.")}

-

${_("You can also download this data as a CSV file.")}

-

-
-
- - - %for i in range(0, len(section_data['sub_section_display_name'])): -
-

${_("Section")}: ${section_data['sub_section_display_name'][i]}

-
-
-
-
-

${_("Grade Distribution per Problem")}

-
-
-
-
- - - -
-
- - - ${_("Close")} -
-
-
- %endfor - -%endif diff --git a/lms/urls.py b/lms/urls.py index 478700da37..2de79115c4 100644 --- a/lms/urls.py +++ b/lms/urls.py @@ -771,11 +771,6 @@ if settings.FEATURES.get('ENABLE_STUDENT_HISTORY_VIEW'): ), ] -if settings.FEATURES.get('CLASS_DASHBOARD'): - urlpatterns += [ - url(r'^class_dashboard/', include('class_dashboard.urls')), - ] - if settings.DEBUG or settings.FEATURES.get('ENABLE_DJANGO_ADMIN_SITE'): # Jasmine and admin urlpatterns += [