diff --git a/lms/djangoapps/instructor/management/commands/dump_grades.py b/lms/djangoapps/instructor/management/commands/dump_grades.py deleted file mode 100644 index a0a1dcea18..0000000000 --- a/lms/djangoapps/instructor/management/commands/dump_grades.py +++ /dev/null @@ -1,76 +0,0 @@ -#!/usr/bin/python -""" -django management command: dump grades to csv files -for use by batch processes -""" -import csv - -from instructor.views.legacy import get_student_grade_summary_data -from courseware.courses import get_course_by_id -from opaque_keys import InvalidKeyError -from opaque_keys.edx.keys import CourseKey -from opaque_keys.edx.locations import SlashSeparatedCourseKey - -from django.core.management.base import BaseCommand -from instructor.utils import DummyRequest - - -class Command(BaseCommand): - help = "dump grades to CSV file. Usage: dump_grades course_id_or_dir filename dump_type\n" - help += " course_id_or_dir: either course_id or course_dir\n" - help += " filename: where the output CSV is to be stored\n" - # help += " start_date: end date as M/D/Y H:M (defaults to end of available data)" - help += " dump_type: 'all' or 'raw' (see instructor dashboard)" - - def handle(self, *args, **options): - - # current grading logic and data schema doesn't handle dates - # datetime.strptime("21/11/06 16:30", "%m/%d/%y %H:%M") - - print "args = ", args - - course_id = 'MITx/8.01rq_MW/Classical_Mechanics_Reading_Questions_Fall_2012_MW_Section' - fn = "grades.csv" - get_raw_scores = False - - if len(args) > 0: - course_id = args[0] - if len(args) > 1: - fn = args[1] - if len(args) > 2: - get_raw_scores = args[2].lower() == 'raw' - - request = DummyRequest() - # parse out the course into a coursekey - try: - course_key = CourseKey.from_string(course_id) - # if it's not a new-style course key, parse it from an old-style - # course key - except InvalidKeyError: - course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) - - try: - course = get_course_by_id(course_key) - # Ok with catching general exception here because this is run as a management command - # and the exception is exposed right away to the user. - except Exception as err: # pylint: disable=broad-except - print "-----------------------------------------------------------------------------" - print "Sorry, cannot find course with id {}".format(course_id) - print "Got exception {}".format(err) - print "Please provide a course ID or course data directory name, eg content-mit-801rq" - return - - print "-----------------------------------------------------------------------------" - print "Dumping grades from {} to file {} (get_raw_scores={})".format(course.id, fn, get_raw_scores) - datatable = get_student_grade_summary_data(request, course, get_raw_scores=get_raw_scores) - - fp = open(fn, 'w') - - writer = csv.writer(fp, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) - writer.writerow([unicode(s).encode('utf-8') for s in datatable['header']]) - for datarow in datatable['data']: - encoded_row = [unicode(s).encode('utf-8') for s in datarow] - writer.writerow(encoded_row) - - fp.close() - print "Done: {} records dumped".format(len(datatable['data'])) diff --git a/lms/djangoapps/instructor/tests/test_legacy_enrollment.py b/lms/djangoapps/instructor/tests/test_legacy_enrollment.py deleted file mode 100644 index e7ed19ae73..0000000000 --- a/lms/djangoapps/instructor/tests/test_legacy_enrollment.py +++ /dev/null @@ -1,357 +0,0 @@ -""" -Unit tests for enrollment methods in views.py - -""" - -import ddt -from mock import patch -from nose.plugins.attrib import attr - -from django.contrib.auth.models import User -from django.core.urlresolvers import reverse -from courseware.tests.helpers import LoginEnrollmentTestCase -from xmodule.modulestore.tests.factories import CourseFactory -from student.tests.factories import UserFactory, CourseEnrollmentFactory, AdminFactory -from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase -from student.models import CourseEnrollment, CourseEnrollmentAllowed -from instructor.views.legacy import get_and_clean_student_list, send_mail_to_student -from django.core import mail - -USER_COUNT = 4 - - -@attr('shard_1') -@ddt.ddt -class TestInstructorEnrollsStudent(SharedModuleStoreTestCase, LoginEnrollmentTestCase): - """ - Check Enrollment/Unenrollment with/without auto-enrollment on activation and with/without email notification - """ - @classmethod - def setUpClass(cls): - super(TestInstructorEnrollsStudent, cls).setUpClass() - cls.course = CourseFactory.create() - - def setUp(self): - super(TestInstructorEnrollsStudent, self).setUp() - - instructor = AdminFactory.create() - self.client.login(username=instructor.username, password='test') - - self.users = [ - UserFactory.create(username="student%d" % i, email="student%d@test.com" % i) - for i in xrange(USER_COUNT) - ] - - for user in self.users: - CourseEnrollmentFactory.create(user=user, course_id=self.course.id) - - # Empty the test outbox - mail.outbox = [] - - def test_unenrollment_email_off(self): - """ - Do un-enrollment email off test - """ - - course = self.course - - # Run the Un-enroll students command - url = reverse('instructor_dashboard_legacy', kwargs={'course_id': course.id.to_deprecated_string()}) - response = self.client.post( - url, - { - 'action': 'Unenroll multiple students', - 'multiple_students': 'student0@test.com student1@test.com' - } - ) - - # Check the page output - self.assertContains(response, '
".format(data_dir) - msg += "
{0}".format(escape(os.popen(cmd).read()))
- track.views.server_track(request, "git-pull", {"directory": data_dir}, page="idashboard")
-
- if 'Reload course' in action:
- log.debug('reloading %s (%s)', course_key, course)
- try:
- data_dir = course.data_dir
- modulestore().try_load_course(data_dir)
- msg += "Course reloaded from {0}
".format(data_dir) - track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard") - course_errors = modulestore().get_course_errors(course.id) - msg += '{1}".format(cmsg, escape(cerr))
- msg += 'Error: {0}
'.format(escape(err)) - - if action == 'Dump list of enrolled students' or action == 'List enrolled students': - log.debug(action) - datatable = get_student_grade_summary_data(request, course, get_grades=False, use_offline=use_offline) - datatable['title'] = _('List of students enrolled in {course_key}').format(course_key=course_key.to_deprecated_string()) - track.views.server_track(request, "list-students", {}, page="idashboard") - - elif 'Dump all RAW grades' in action: - log.debug(action) - datatable = get_student_grade_summary_data(request, course, get_grades=True, - get_raw_scores=True, use_offline=use_offline) - datatable['title'] = _('Raw Grades of students enrolled in {course_key}').format(course_key=course_key) - track.views.server_track(request, "dump-grades-raw", {}, page="idashboard") - - elif 'Download CSV of all RAW grades' in action: - track.views.server_track(request, "dump-grades-csv-raw", {}, page="idashboard") - return return_csv('grades_{0}_raw.csv'.format(course_key.to_deprecated_string()), - get_student_grade_summary_data(request, course, get_raw_scores=True, use_offline=use_offline)) - - elif 'Download CSV of answer distributions' in action: - track.views.server_track(request, "dump-answer-dist-csv", {}, page="idashboard") - return return_csv('answer_dist_{0}.csv'.format(course_key.to_deprecated_string()), get_answers_distribution(request, course_key)) - - #---------------------------------------- - # export grades to remote gradebook - - elif action == 'List assignments available in remote gradebook': - msg2, datatable = _do_remote_gradebook(request.user, course, 'get-assignments') - msg += msg2 - - elif action == 'List assignments available for this course': - log.debug(action) - allgrades = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline) - - assignments = [[x] for x in allgrades['assignments']] - datatable = {'header': [_('Assignment Name')]} - datatable['data'] = assignments - datatable['title'] = action - - msg += 'assignments=%s' % assignments - - elif action == 'List enrolled students matching remote gradebook': - stud_data = get_student_grade_summary_data(request, course, get_grades=False, use_offline=use_offline) - msg2, rg_stud_data = _do_remote_gradebook(request.user, course, 'get-membership') - datatable = {'header': ['Student email', 'Match?']} - rg_students = [x['email'] for x in rg_stud_data['retdata']] - - def domatch(student): - """Returns 'yes' if student is pressent in the remote gradebook student list, else returns 'No'""" - return 'yes' if student.email in rg_students else 'No' - datatable['data'] = [[x.email, domatch(x)] for x in stud_data['students']] - datatable['title'] = action - - elif action in ['Display grades for assignment', 'Export grades for assignment to remote gradebook', - 'Export CSV file of grades for assignment']: - - log.debug(action) - datatable = {} - aname = request.POST.get('assignment_name', '') - if not aname: - msg += "{text}".format(text=_("Please enter an assignment name")) - else: - allgrades = get_student_grade_summary_data(request, course, get_grades=True, use_offline=use_offline) - if aname not in allgrades['assignments']: - msg += "{text}".format( - text=_("Invalid assignment name '{name}'").format(name=aname) - ) - else: - aidx = allgrades['assignments'].index(aname) - datatable = {'header': [_('External email'), aname]} - ddata = [] - for student in allgrades['students']: # do one by one in case there is a student who has only partial grades - try: - ddata.append([student.email, student.grades[aidx]]) - except IndexError: - log.debug(u'No grade for assignment %(idx)s (%(name)s) for student %(email)s', { - "idx": aidx, - "name": aname, - "email": student.email, - }) - datatable['data'] = ddata - - datatable['title'] = _('Grades for assignment "{name}"').format(name=aname) - - if 'Export CSV' in action: - # generate and return CSV file - return return_csv('grades {name}.csv'.format(name=aname), datatable) - - elif 'remote gradebook' in action: - file_pointer = StringIO() - return_csv('', datatable, file_pointer=file_pointer) - file_pointer.seek(0) - files = {'datafile': file_pointer} - msg2, __ = _do_remote_gradebook(request.user, course, 'post-grades', files=files) - msg += msg2 - - #---------------------------------------- - # enrollment - - elif action == 'Enroll multiple students': - - is_shib_course = uses_shib(course) - students = request.POST.get('multiple_students', '') - auto_enroll = bool(request.POST.get('auto_enroll')) - email_students = bool(request.POST.get('email_students')) - secure = request.is_secure() - ret = _do_enroll_students(course, course_key, students, secure=secure, auto_enroll=auto_enroll, email_students=email_students, is_shib_course=is_shib_course) - datatable = ret['datatable'] - - elif action == 'Unenroll multiple students': - - students = request.POST.get('multiple_students', '') - email_students = bool(request.POST.get('email_students')) - ret = _do_unenroll_students(course_key, students, email_students=email_students) - datatable = ret['datatable'] - - elif action == 'List sections available in remote gradebook': - - msg2, datatable = _do_remote_gradebook(request.user, course, 'get-sections') - msg += msg2 - - elif action in ['List students in section in remote gradebook', - 'Overload enrollment list using remote gradebook', - 'Merge enrollment list with remote gradebook']: - - section = request.POST.get('gradebook_section', '') - msg2, datatable = _do_remote_gradebook(request.user, course, 'get-membership', dict(section=section)) - msg += msg2 - - if 'List' not in action: - students = ','.join([x['email'] for x in datatable['retdata']]) - overload = 'Overload' in action - secure = request.is_secure() - ret = _do_enroll_students(course, course_key, students, secure=secure, overload=overload) - datatable = ret['datatable'] - - #---------------------------------------- - # analytics - def get_analytics_result(analytics_name): - """Return data for an Analytic piece, or None if it doesn't exist. It - logs and swallows errors. - """ - url = settings.ANALYTICS_SERVER_URL + \ - u"get?aname={}&course_id={}&apikey={}".format( - analytics_name, urllib.quote(unicode(course_key)), settings.ANALYTICS_API_KEY - ) - try: - res = requests.get(url) - except Exception: # pylint: disable=broad-except - log.exception("Error trying to access analytics at %s", url) - return None - - if res.status_code == codes.OK: - # WARNING: do not use req.json because the preloaded json doesn't - # preserve the order of the original record (hence OrderedDict). - payload = json.loads(res.content, object_pairs_hook=OrderedDict) - add_block_ids(payload) - return payload - else: - log.error("Error fetching %s, code: %s, msg: %s", - url, res.status_code, res.content) - return None - - analytics_results = {} - - if idash_mode == 'Analytics': - dashboard_analytics = [ - # "StudentsAttemptedProblems", # num students who tried given problem - "StudentsDailyActivity", # active students by day - "StudentsDropoffPerDay", # active students dropoff by day - # "OverallGradeDistribution", # overall point distribution for course - # "StudentsPerProblemCorrect", # foreach problem, num students correct - "ProblemGradeDistribution", # foreach problem, grade distribution - ] - - for analytic_name in dashboard_analytics: - analytics_results[analytic_name] = get_analytics_result(analytic_name) - - #---------------------------------------- - # Metrics - - metrics_results = {} - if settings.FEATURES.get('CLASS_DASHBOARD') and idash_mode == 'Metrics': - metrics_results['section_display_name'] = dashboard_data.get_section_display_name(course_key) - metrics_results['section_has_problem'] = dashboard_data.get_array_section_has_problem(course_key) - - #---------------------------------------- - # offline grades? - - if use_offline: - msg += "
{msg}'.format(msg=retdict['msg'].replace('\n', '%s' % msg.replace('<', '<') - return msg - - -def get_background_task_table(course_key, problem_url=None, student=None, task_type=None): - """ - Construct the "datatable" structure to represent background task history. - - Filters the background task history to the specified course and problem. - If a student is provided, filters to only those tasks for which that student - was specified. - - Returns a tuple of (msg, datatable), where the msg is a possible error message, - and the datatable is the datatable to be used for display. - """ - history_entries = get_instructor_task_history(course_key, problem_url, student, task_type) - datatable = {} - msg = "" - # first check to see if there is any history at all - # (note that we don't have to check that the arguments are valid; it - # just won't find any entries.) - if (history_entries.count()) == 0: - if problem_url is None: - msg += 'Failed to find any background tasks for course "{course}".'.format( - course=course_key.to_deprecated_string() - ) - elif student is not None: - template = '' + _('Failed to find any background tasks for course "{course}", module "{problem}" and student "{student}".') + '' - msg += template.format(course=course_key.to_deprecated_string(), problem=problem_url, student=student.username) - else: - msg += '' + _('Failed to find any background tasks for course "{course}" and module "{problem}".').format( - course=course_key.to_deprecated_string(), problem=problem_url - ) + '' - else: - datatable['header'] = ["Task Type", - "Task Id", - "Requester", - "Submitted", - "Duration (sec)", - "Task State", - "Task Status", - "Task Output"] - - datatable['data'] = [] - for instructor_task in history_entries: - # get duration info, if known: - duration_sec = 'unknown' - if hasattr(instructor_task, 'task_output') and instructor_task.task_output is not None: - task_output = json.loads(instructor_task.task_output) - if 'duration_ms' in task_output: - duration_sec = int(task_output['duration_ms'] / 1000.0) - # get progress status message: - success, task_message = get_task_completion_info(instructor_task) - status = "Complete" if success else "Incomplete" - # generate row for this task: - row = [ - str(instructor_task.task_type), - str(instructor_task.task_id), - str(instructor_task.requester), - instructor_task.created.isoformat(' '), - duration_sec, - str(instructor_task.task_state), - status, - task_message - ] - datatable['data'].append(row) - - if problem_url is None: - datatable['title'] = "{course_id}".format(course_id=course_key.to_deprecated_string()) - elif student is not None: - datatable['title'] = "{course_id} > {location} > {student}".format( - course_id=course_key.to_deprecated_string(), - location=problem_url, - student=student.username - ) - else: - datatable['title'] = "{course_id} > {location}".format( - course_id=course_key.to_deprecated_string(), location=problem_url - ) - - return msg, datatable - - -def uses_shib(course): - """ - Used to return whether course has Shibboleth as the enrollment domain - - Returns a boolean indicating if Shibboleth authentication is set for this course. - """ - return course.enrollment_domain and course.enrollment_domain.startswith(SHIBBOLETH_DOMAIN_PREFIX) diff --git a/lms/envs/common.py b/lms/envs/common.py index ef0528df20..0d4cce8722 100644 --- a/lms/envs/common.py +++ b/lms/envs/common.py @@ -205,9 +205,6 @@ FEATURES = { # Enable Custom Courses for EdX 'CUSTOM_COURSES_EDX': False, - # Enable legacy instructor dashboard - 'ENABLE_INSTRUCTOR_LEGACY_DASHBOARD': False, - # Is this an edX-owned domain? (used for edX specific messaging and images) 'IS_EDX_DOMAIN': False, diff --git a/lms/envs/dev.py b/lms/envs/dev.py index 5c48e6ca0b..f84fac987d 100644 --- a/lms/envs/dev.py +++ b/lms/envs/dev.py @@ -28,7 +28,6 @@ FEATURES['ENABLE_SERVICE_STATUS'] = True FEATURES['ENABLE_INSTRUCTOR_EMAIL'] = True # Enable email for all Studio courses FEATURES['REQUIRE_COURSE_EMAIL_AUTH'] = False # Give all courses email (don't require django-admin perms) FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True -FEATURES['ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'] = False FEATURES['MULTIPLE_ENROLLMENT_ROLES'] = True FEATURES['ENABLE_SHOPPING_CART'] = True FEATURES['AUTOMATIC_VERIFY_STUDENT_IDENTITY_FOR_TESTING'] = True diff --git a/lms/envs/test.py b/lms/envs/test.py index 8ec7054124..582e825764 100644 --- a/lms/envs/test.py +++ b/lms/envs/test.py @@ -62,8 +62,6 @@ FEATURES['ENABLE_SERVICE_STATUS'] = True FEATURES['ENABLE_HINTER_INSTRUCTOR_VIEW'] = True -FEATURES['ENABLE_INSTRUCTOR_LEGACY_DASHBOARD'] = True - FEATURES['ENABLE_SHOPPING_CART'] = True FEATURES['ENABLE_VERIFIED_CERTIFICATES'] = True diff --git a/lms/static/sass/_build-course.scss b/lms/static/sass/_build-course.scss index 2883ab38e6..d2ebc453b1 100644 --- a/lms/static/sass/_build-course.scss +++ b/lms/static/sass/_build-course.scss @@ -49,7 +49,6 @@ @import "views/teams"; // course - instructor-only views -@import "course/instructor/instructor"; @import "course/instructor/instructor_2"; @import "course/instructor/email"; @import "xmodule/descriptors/css/module-styles.scss"; diff --git a/lms/static/sass/course/instructor/_instructor.scss b/lms/static/sass/course/instructor/_instructor.scss deleted file mode 100644 index f153459559..0000000000 --- a/lms/static/sass/course/instructor/_instructor.scss +++ /dev/null @@ -1,189 +0,0 @@ -.instructor-dashboard-wrapper { - display: table; - position: relative; - - .beta-button-wrapper { - position: absolute; - top: 2em; - right: 2em; - } - - .studio-edit-link{ - position: absolute; - top: 3.5em; - right: 2em; - } - - section.instructor-dashboard-content { - @extend .content; - padding: 40px; - width: 100%; - - h1 { - @extend .top-header; - } - } - - // form fields - .list-fields { - @extend %ui-no-list; - - .field { - margin-bottom: $baseline; - - &:last-child { - margin-bottom: 0; - } - - .tip { - display: block; - margin-top: ($baseline/4); - color: tint(rgb(127,127,127),50%); - @include font-size(12); - } - - } - - } - - // ==================== - - // system feedback - messages - .msg { - border-radius: 1px; - padding: 10px 15px; - margin-bottom: $baseline; - - .copy { - font-weight: 600; - } - } - - // TYPE: warning - .msg-warning { - border-top: 2px solid $warning-color; - background: tint($warning-color,95%); - - .copy { - color: $warning-color; - } - } - - // TYPE: confirm - .msg-confirm { - border-top: 2px solid $confirm-color; - background: tint($confirm-color,95%); - - .copy { - color: $confirm-color; - } - } - - // TYPE: confirm - .msg-error { - border-top: 2px solid $error-color; - background: tint($error-color,95%); - - .copy { - color: $error-color; - } - } - - // ==================== - - // inline copy - .copy-confirm { - color: $confirm-color; - } - - .copy-warning { - color: $warning-color; - } - - .copy-error { - color: $error-color; - } - - .list-advice { - list-style: none; - padding: 0; - margin: 20px 0; - - .item { - font-weight: 600; - margin-bottom: ($baseline/2); - - &:last-child { - margin-bottom: 0; - } - } - } - - //Metrics tab - - .metrics-container { - position: relative; - width: 100%; - float: left; - clear: both; - margin-top: 25px; - } - .metrics-left { - position: relative; - width: 30%; - height: 640px; - float: left; - margin-right: 2.5%; - } - .metrics-right { - position: relative; - width: 65%; - height: 295px; - float: left; - margin-left: 2.5%; - margin-bottom: 25px; - } - .metrics-tooltip { - width: 250px; - background-color: lightgray; - padding: 3px; - } - .stacked-bar-graph-legend { - fill: white; - } - - p.loading { - padding-top: 100px; - text-align: center; - } - - p.nothing { - padding-top: 25px; - } - - h3.attention { - padding: 10px; - border: 1px solid #999; - border-radius: 5px; - margin-top: 25px; - } - - .wrapper-msg { - margin-bottom: ($baseline*1.5); - - .msg { - margin-bottom: 0; - } - - .note { - margin: 0; - } - } - -} - -.rtl .instructor-dashboard-wrapper .beta-button-wrapper, -.rtl .instructor-dashboard-wrapper .studio-edit-link { - left: 2em; - right: auto; -} diff --git a/lms/templates/courseware/legacy_instructor_dashboard.html b/lms/templates/courseware/legacy_instructor_dashboard.html deleted file mode 100644 index 2ec867f363..0000000000 --- a/lms/templates/courseware/legacy_instructor_dashboard.html +++ /dev/null @@ -1,496 +0,0 @@ -## NOTE: This is the template for the LEGACY instructor dashboard ## -## We are no longer supporting this file or accepting changes into it. ## -## Please see lms/templates/instructor for instructor dashboard templates ## - -<%inherit file="../main.html" /> -<%namespace name='static' file='/static_content.html'/> -<%! -from django.utils.translation import ugettext as _ -from django.core.urlresolvers import reverse -%> - -<%block name="pagetitle">${_("Legacy Instructor Dashboard")}%block> -<%block name="nav_skip">#instructor-dashboard-content%block> - -<%block name="headextra"> -<%static:css group='style-course-vendor'/> -<%static:css group='style-vendor-tinymce-content'/> -<%static:css group='style-vendor-tinymce-skin'/> -<%static:css group='style-course'/> - - - - - - - - - - - - <%static:js group='module-descriptor-js'/> -%if instructor_tasks is not None: - -%endif -%block> - -<%include file="/courseware/course_navigation.html" args="active_page='instructor'" /> - - - - - -
${_("You are using the legacy instructor dashboard, which we will retire in the near future.")} ${_("Return to the Instructor Dashboard")}
-${_("If the Instructor Dashboard is missing functionality, please contact your PM to let us know.")}
-${msg}
-%endif - -##----------------------------------------------------------------------------- - -%if datatable: - --
| ${hname | h} | - %endfor -
|---|
| ${value | h} | - %endfor -
| ${_("Task Type")} | -${_("Task inputs")} | -${_("Task Id")} | -${_("Requester")} | -${_("Submitted")} | -${_("Task State")} | -${_("Duration (sec)")} | -${_("Task Progress")} | -
|---|---|---|---|---|---|---|---|
| ${instructor_task.task_type} | -${instructor_task.task_input} | -${instructor_task.task_id} | -${instructor_task.requester} | -${instructor_task.created} | -${instructor_task.task_state} | -${_("unknown")} | -${_("unknown")} | -
-
| ${hname | h} | - %endfor -
|---|
| ${value | h} | - %endfor -
- ${_("View course statistics in the Admin section of this legacy instructor dashboard.")} -
-%endif - -##----------------------------------------------------------------------------- -%if modeflag.get('Admin'): - % if course_errors is not UNDEFINED: -${err | h}- % endif -