def get_course_info(course): """Returns an object containing original edX course and some complementary properties.""" about_sections = {} for field in ABOUT_SECTION_FIELDS: about_sections[field] = get_course_about_section(course, field) about_sections['effort'] = about_sections['effort'].replace('\n', '') # clean the many CRs if about_sections['video']: try: # edX stores the Youtube iframe HTML code, let's extract the Dailymotion Cloud ID about_sections['video'] = re.findall('www.youtube.com/embed/(?P<hash>[\w]+)\?', about_sections['video'])[0] except IndexError: pass try: funcourse = Course.objects.get(key=course.id) except Course.DoesNotExist: funcourse = None course_info = FunCourse(course=course, fun=funcourse, course_image_url=course_image_url(course), students_count=CourseEnrollment.objects.filter(course_id=course.id).count(), url='https://%s%s' % (settings.LMS_BASE, reverse('about_course', args=[course.id.to_deprecated_string()])), studio_url=get_cms_course_link(course), **about_sections ) return course_info
def get_course_info(course_descriptor, course, students_count, course_modes): """Returns a dict containing original edX course and some complementary properties.""" return { 'course': course_descriptor, 'fun': course, 'course_image_url': course_image_url(course_descriptor), 'students_count': students_count, 'title': course_descriptor.display_name_with_default, 'university': course_descriptor.display_org_with_default, 'url': 'https://%s%s' % (settings.LMS_BASE, reverse('about_course', args=[course_descriptor.id.to_deprecated_string()])), 'studio_url': get_cms_course_link(course_descriptor), 'modes': course_modes[unicode(course_descriptor.id)] if course_modes else [], }
def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ course = get_course_by_id(course_id, depth=None) is_studio_course = (modulestore().get_modulestore_type(course_id) != XML_MODULESTORE_TYPE) access = { 'admin': request.user.is_staff, 'instructor': has_access(request.user, course, 'instructor'), 'staff': has_access(request.user, course, 'staff'), 'forum_admin': has_forum_access( request.user, course_id, FORUM_ROLE_ADMINISTRATOR ), } if not access['staff']: raise Http404() sections = [ _section_course_info(course_id, access), _section_membership(course_id, access), _section_student_admin(course_id, access), _section_data_download(course_id, access), _section_analytics(course_id, access), ] if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']): sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization if settings.FEATURES['ENABLE_INSTRUCTOR_EMAIL'] and \ is_studio_course and CourseAuthorization.instructor_email_enabled(course_id): sections.append(_section_send_email(course_id, access, course)) # Gate access to Metrics tab by featue flag and staff authorization if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: sections.append(_section_metrics(course_id, access)) studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) enrollment_count = sections[0]['enrollment_count'] disable_buttons = False max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") if max_enrollment_for_buttons is not None: disable_buttons = enrollment_count > max_enrollment_for_buttons context = { 'course': course, 'old_dashboard_url': reverse('instructor_dashboard', kwargs={'course_id': course_id}), 'studio_url': studio_url, 'sections': sections, 'disable_buttons': disable_buttons, } return render_to_response('instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
def test_get_cms_course_block_link(self): """ Tests that get_cms_course_link_by_id and get_cms_block_link_by_id return the right thing """ self.course = CourseFactory.create(org="org", number="num", display_name="name") cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, unicode(self.course.id)) self.assertEqual(cms_url, get_cms_course_link(self.course)) cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, unicode(self.course.location)) self.assertEqual(cms_url, get_cms_block_link(self.course, "course"))
def instructor_dashboard_2(request, course_id): """Display the instructor dashboard for a course.""" course = get_course_by_id(course_id, depth=None) is_studio_course = modulestore().get_modulestore_type(course_id) == MONGO_MODULESTORE_TYPE access = { "admin": request.user.is_staff, "instructor": has_access(request.user, course, "instructor"), "staff": has_access(request.user, course, "staff"), "forum_admin": has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR), } if not access["staff"]: raise Http404() sections = [ _section_course_info(course_id, access), _section_membership(course_id, access), _section_student_admin(course_id, access), _section_data_download(course_id, access), _section_analytics(course_id, access), ] if settings.FEATURES.get("INDIVIDUAL_DUE_DATES") and access["instructor"]: sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization if ( settings.FEATURES["ENABLE_INSTRUCTOR_EMAIL"] and is_studio_course and CourseAuthorization.instructor_email_enabled(course_id) ): sections.append(_section_send_email(course_id, access, course)) studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) enrollment_count = sections[0]["enrollment_count"] disable_buttons = False max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") if max_enrollment_for_buttons is not None: disable_buttons = enrollment_count > max_enrollment_for_buttons context = { "course": course, "old_dashboard_url": reverse("instructor_dashboard", kwargs={"course_id": course_id}), "studio_url": studio_url, "sections": sections, "disable_buttons": disable_buttons, } return render_to_response("instructor/instructor_dashboard_2/instructor_dashboard_2.html", context)
def test_get_cms_course_block_link(self): """ Tests that get_cms_course_link_by_id and get_cms_block_link_by_id return the right thing """ self.course = CourseFactory.create( org='org', number='num', display_name='name' ) cms_url = u"//{}/course/slashes:org+num+name".format(CMS_BASE_TEST) self.assertEqual(cms_url, get_cms_course_link(self.course)) cms_url = u"//{}/course/location:org+num+name+course+name".format(CMS_BASE_TEST) self.assertEqual(cms_url, get_cms_block_link(self.course, 'course'))
def test_get_cms_course_block_link(self): """ Tests that get_cms_course_link_by_id and get_cms_block_link_by_id return the right thing """ self.course = CourseFactory.create( org='org', number='num', display_name='name' ) cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, unicode(self.course.id)) self.assertEqual(cms_url, get_cms_course_link(self.course)) cms_url = u"//{}/course/{}".format(CMS_BASE_TEST, unicode(self.course.location)) self.assertEqual(cms_url, get_cms_block_link(self.course, 'course'))
def test_get_cms_course_link(self): """ Tests that get_cms_course_link_by_id returns the right thing """ self.course = CourseFactory.create(org='org', number='num', display_name='name') self.assertEqual( u"//{}/course/org.num.name/branch/draft/block/name".format( CMS_BASE_TEST), get_cms_course_link(self.course))
def test_get_cms_course_block_link(self): """ Tests that get_cms_course_link_by_id and get_cms_block_link_by_id return the right thing """ cms_url = u"//{}/course/org.num.name/branch/draft/block/name".format(CMS_BASE_TEST) self.course = CourseFactory.create( org='org', number='num', display_name='name' ) self.assertEqual(cms_url, get_cms_course_link(self.course)) self.assertEqual(cms_url, get_cms_block_link(self.course, 'course'))
def get_course_info(course_descriptor, course, students_count): """Returns an object containing original edX course and some complementary properties.""" about_sections = get_about_sections(course_descriptor) course_info = FunCourse( course=course_descriptor, fun=course, course_image_url=course_image_url(course_descriptor), students_count=students_count, title=course_descriptor.display_name_with_default, university=course_descriptor.display_org_with_default, url='https://%s%s' % (settings.LMS_BASE, reverse('about_course', args=[course_descriptor.id.to_deprecated_string()])), studio_url=get_cms_course_link(course_descriptor), **about_sections ) return course_info
def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_key, depth=None) is_studio_course = (modulestore().get_modulestore_type(course_key) != ModuleStoreEnum.Type.xml) access = { 'admin': request.user.is_staff, 'instructor': has_access(request.user, 'instructor', course), 'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user), 'staff': has_access(request.user, 'staff', course), 'forum_admin': has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR), } if not access['staff']: raise Http404() sections = [ _section_course_info(course_key, access), _section_membership(course_key, access), _section_student_admin(course_key, access), _section_data_download(course_key, access), _section_analytics(course_key, access), ] #check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course course_honor_mode = CourseMode.mode_for_course(course_key, 'honor') course_mode_has_price = False if course_honor_mode and course_honor_mode.min_price > 0: course_mode_has_price = True if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']): sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization if bulk_email_is_enabled_for_course(course_key): sections.append(_section_send_email(course_key, access, course)) # Gate access to Metrics tab by featue flag and staff authorization if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: sections.append(_section_metrics(course_key, access)) # Gate access to Ecommerce tab if course_mode_has_price: sections.append(_section_e_commerce(course_key, access)) studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) enrollment_count = sections[0]['enrollment_count'] disable_buttons = False max_enrollment_for_buttons = settings.FEATURES.get( "MAX_ENROLLMENT_INSTR_BUTTONS") if max_enrollment_for_buttons is not None: disable_buttons = enrollment_count > max_enrollment_for_buttons context = { 'course': course, 'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': course_key.to_deprecated_string()}), 'studio_url': studio_url, 'sections': sections, 'disable_buttons': disable_buttons, } return render_to_response( 'instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)
def instructor_dashboard(request, course_id): """Display the instructor dashboard for a course.""" course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'staff', course_key, depth=None) instructor_access = has_access(request.user, 'instructor', course) # an instructor can manage staff lists forum_admin_access = has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR) msg = '' show_email_tab = False problems = [] plots = [] datatable = {} # the instructor dashboard page is modal: grades, psychometrics, admin # keep that state in request.session (defaults to grades mode) idash_mode = request.POST.get('idash_mode', '') idash_mode_key = u'idash_mode:{0}'.format(course_id) if idash_mode: request.session[idash_mode_key] = idash_mode else: idash_mode = request.session.get(idash_mode_key, 'Grades') enrollment_number = CourseEnrollment.num_enrolled_in(course_key) # assemble some course statistics for output to instructor def get_course_stats_table(): datatable = { 'header': ['Statistic', 'Value'], 'title': _('Course Statistics At A Glance'), } data = [['Date', timezone.now().isoformat()]] data += compute_course_stats(course).items() if request.user.is_staff: for field in course.fields.values(): if getattr(field.scope, 'user', False): continue data.append([ field.name, json.dumps(field.read_json(course), cls=i4xEncoder) ]) datatable['data'] = data return datatable def return_csv(func, datatable, file_pointer=None): """Outputs a CSV file from the contents of a datatable.""" if file_pointer is None: response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = (u'attachment; filename={0}'.format(func)).encode('utf-8') else: response = file_pointer writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) encoded_row = [unicode(s).encode('utf-8') for s in datatable['header']] writer.writerow(encoded_row) for datarow in datatable['data']: # 's' here may be an integer, float (eg score) or string (eg student name) encoded_row = [ # If s is already a UTF-8 string, trying to make a unicode # object out of it will fail unless we pass in an encoding to # the constructor. But we can't do that across the board, # because s is often a numeric type. So just do this. s if isinstance(s, str) else unicode(s).encode('utf-8') for s in datarow ] writer.writerow(encoded_row) return response # process actions from form POST action = request.POST.get('action', '') use_offline = request.POST.get('use_offline_grades', False) if settings.FEATURES['ENABLE_MANUAL_GIT_RELOAD']: if 'GIT pull' in action: data_dir = course.data_dir log.debug('git pull {0}'.format(data_dir)) gdir = settings.DATA_DIR / data_dir if not os.path.exists(gdir): msg += "====> ERROR in gitreload - no such directory {0}".format(gdir) else: cmd = "cd {0}; git reset --hard HEAD; git clean -f -d; git pull origin; chmod g+w course.xml".format(gdir) msg += "git pull on {0}:<p>".format(data_dir) msg += "<pre>{0}</pre></p>".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 {0} ({1})'.format(course_key, course)) try: data_dir = course.data_dir modulestore().try_load_course(data_dir) msg += "<br/><p>Course reloaded from {0}</p>".format(data_dir) track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard") course_errors = modulestore().get_course_errors(course.id) msg += '<ul>' for cmsg, cerr in course_errors: msg += "<li>{0}: <pre>{1}</pre>".format(cmsg, escape(cerr)) msg += '</ul>' except Exception as err: # pylint: disable=broad-except msg += '<br/><p>Error: {0}</p>'.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=<pre>%s</pre>' % 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 += "<font color='red'>{text}</font>".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 += "<font color='red'>{text}</font>".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('No grade for assignment {idx} ({name}) for student {email}'.format( 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 #---------------------------------------- # DataDump elif 'Download CSV of all responses to problem' in action: problem_to_dump = request.POST.get('problem_to_dump', '') if problem_to_dump[-4:] == ".xml": problem_to_dump = problem_to_dump[:-4] try: module_state_key = course_key.make_usage_key_from_deprecated_string(problem_to_dump) smdat = StudentModule.objects.filter( course_id=course_key, module_state_key=module_state_key ) smdat = smdat.order_by('student') msg += _("Found {num} records to dump.").format(num=smdat) except Exception as err: # pylint: disable=broad-except msg += "<font color='red'>{text}</font><pre>{err}</pre>".format( text=_("Couldn't find module with that urlname."), err=escape(err) ) smdat = [] if smdat: datatable = {'header': ['username', 'state']} datatable['data'] = [[x.student.username, x.state] for x in smdat] datatable['title'] = _('Student state for problem {problem}').format(problem=problem_to_dump) return return_csv('student_state_from_{problem}.csv'.format(problem=problem_to_dump), datatable) #---------------------------------------- # enrollment elif action == 'List students who may enroll but may not have yet signed up': ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) datatable = {'header': ['StudentEmail']} datatable['data'] = [[x.email] for x in ceaset] datatable['title'] = action 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'] #---------------------------------------- # psychometrics elif action == 'Generate Histogram and IRT Plot': problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard") if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_key) #---------------------------------------- # 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 += "<br/><font color='orange'>{text}</font>".format( text=_("Grades from {course_id}").format( course_id=offline_grades_available(course_key) ) ) # generate list of pending background tasks if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): instructor_tasks = get_running_instructor_tasks(course_key) else: instructor_tasks = None # determine if this is a studio-backed course so we can provide a link to edit this course in studio is_studio_course = modulestore().get_modulestore_type(course_key) != ModuleStoreEnum.Type.xml studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) if bulk_email_is_enabled_for_course(course_key): show_email_tab = True # display course stats only if there is no other table to display: course_stats = None if not datatable: course_stats = get_course_stats_table() # disable buttons for large courses disable_buttons = False max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") if max_enrollment_for_buttons is not None: disable_buttons = enrollment_number > max_enrollment_for_buttons #---------------------------------------- # context for rendering context = { 'course': course, 'staff_access': True, 'admin_access': request.user.is_staff, 'instructor_access': instructor_access, 'forum_admin_access': forum_admin_access, 'datatable': datatable, 'course_stats': course_stats, 'msg': msg, 'modeflag': {idash_mode: 'selectedmode'}, 'studio_url': studio_url, 'show_email_tab': show_email_tab, # email 'problems': problems, # psychometrics 'plots': plots, # psychometrics 'course_errors': modulestore().get_course_errors(course.id), 'instructor_tasks': instructor_tasks, 'offline_grade_log': offline_grades_available(course_key), 'analytics_results': analytics_results, 'disable_buttons': disable_buttons, 'metrics_results': metrics_results, } context['standard_dashboard_url'] = reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}) return render_to_response('courseware/legacy_instructor_dashboard.html', context)
def instructor_dashboard(request, course_id): """Display the instructor dashboard for a course.""" course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_with_access(request.user, 'staff', course_key, depth=None) instructor_access = has_access(request.user, 'instructor', course) # an instructor can manage staff lists forum_admin_access = has_forum_access(request.user, course_key, FORUM_ROLE_ADMINISTRATOR) msg = '' show_email_tab = False problems = [] plots = [] datatable = {} # the instructor dashboard page is modal: grades, psychometrics, admin # keep that state in request.session (defaults to grades mode) idash_mode = request.POST.get('idash_mode', '') idash_mode_key = u'idash_mode:{0}'.format(course_id) if idash_mode: request.session[idash_mode_key] = idash_mode else: idash_mode = request.session.get(idash_mode_key, 'Grades') enrollment_number = CourseEnrollment.num_enrolled_in(course_key) # assemble some course statistics for output to instructor def get_course_stats_table(): datatable = { 'header': ['Statistic', 'Value'], 'title': _('Course Statistics At A Glance'), } data = [['Date', timezone.now().isoformat()]] data += compute_course_stats(course).items() if request.user.is_staff: for field in course.fields.values(): if getattr(field.scope, 'user', False): continue data.append([ field.name, json.dumps(field.read_json(course), cls=i4xEncoder) ]) datatable['data'] = data return datatable def return_csv(func, datatable, file_pointer=None): """Outputs a CSV file from the contents of a datatable.""" if file_pointer is None: response = HttpResponse(mimetype='text/csv') response['Content-Disposition'] = (u'attachment; filename={0}'.format(func)).encode('utf-8') else: response = file_pointer writer = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL) encoded_row = [unicode(s).encode('utf-8') for s in datatable['header']] writer.writerow(encoded_row) for datarow in datatable['data']: # 's' here may be an integer, float (eg score) or string (eg student name) encoded_row = [ # If s is already a UTF-8 string, trying to make a unicode # object out of it will fail unless we pass in an encoding to # the constructor. But we can't do that across the board, # because s is often a numeric type. So just do this. s if isinstance(s, str) else unicode(s).encode('utf-8') for s in datarow ] writer.writerow(encoded_row) return response # process actions from form POST action = request.POST.get('action', '') use_offline = request.POST.get('use_offline_grades', False) if settings.FEATURES['ENABLE_MANUAL_GIT_RELOAD']: if 'GIT pull' in action: data_dir = course.data_dir log.debug('git pull {0}'.format(data_dir)) gdir = settings.DATA_DIR / data_dir if not os.path.exists(gdir): msg += "====> ERROR in gitreload - no such directory {0}".format(gdir) else: cmd = "cd {0}; git reset --hard HEAD; git clean -f -d; git pull origin; chmod g+w course.xml".format(gdir) msg += "git pull on {0}:<p>".format(data_dir) msg += "<pre>{0}</pre></p>".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 {0} ({1})'.format(course_key, course)) try: data_dir = course.data_dir modulestore().try_load_course(data_dir) msg += "<br/><p>Course reloaded from {0}</p>".format(data_dir) track.views.server_track(request, "reload", {"directory": data_dir}, page="idashboard") course_errors = modulestore().get_course_errors(course.id) msg += '<ul>' for cmsg, cerr in course_errors: msg += "<li>{0}: <pre>{1}</pre>".format(cmsg, escape(cerr)) msg += '</ul>' except Exception as err: # pylint: disable=broad-except msg += '<br/><p>Error: {0}</p>'.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=<pre>%s</pre>' % 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 += "<font color='red'>{text}</font>".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 += "<font color='red'>{text}</font>".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('No grade for assignment {idx} ({name}) for student {email}'.format( 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 #---------------------------------------- # DataDump elif 'Download CSV of all responses to problem' in action: problem_to_dump = request.POST.get('problem_to_dump', '') if problem_to_dump[-4:] == ".xml": problem_to_dump = problem_to_dump[:-4] try: module_state_key = course_key.make_usage_key_from_deprecated_string(problem_to_dump) smdat = StudentModule.objects.filter( course_id=course_key, module_state_key=module_state_key ) smdat = smdat.order_by('student') msg += _("Found {num} records to dump.").format(num=smdat) except Exception as err: # pylint: disable=broad-except msg += "<font color='red'>{text}</font><pre>{err}</pre>".format( text=_("Couldn't find module with that urlname."), err=escape(err) ) smdat = [] if smdat: datatable = {'header': ['username', 'state']} datatable['data'] = [[x.student.username, x.state] for x in smdat] datatable['title'] = _('Student state for problem {problem}').format(problem=problem_to_dump) return return_csv('student_state_from_{problem}.csv'.format(problem=problem_to_dump), datatable) #---------------------------------------- # enrollment elif action == 'List students who may enroll but may not have yet signed up': ceaset = CourseEnrollmentAllowed.objects.filter(course_id=course_key) datatable = {'header': ['StudentEmail']} datatable['data'] = [[x.email] for x in ceaset] datatable['title'] = action 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 not 'List' 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'] #---------------------------------------- # psychometrics elif action == 'Generate Histogram and IRT Plot': problem = request.POST['Problem'] nmsg, plots = psychoanalyze.generate_plots_for_problem(problem) msg += nmsg track.views.server_track(request, "psychometrics-histogram-generation", {"problem": unicode(problem)}, page="idashboard") if idash_mode == 'Psychometrics': problems = psychoanalyze.problems_with_psychometric_data(course_key) #---------------------------------------- # 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 += "<br/><font color='orange'>{text}</font>".format( text=_("Grades from {course_id}").format( course_id=offline_grades_available(course_key) ) ) # generate list of pending background tasks if settings.FEATURES.get('ENABLE_INSTRUCTOR_BACKGROUND_TASKS'): instructor_tasks = get_running_instructor_tasks(course_key) else: instructor_tasks = None # determine if this is a studio-backed course so we can provide a link to edit this course in studio is_studio_course = modulestore().get_modulestore_type(course_key) != ModuleStoreEnum.Type.xml studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) if bulk_email_is_enabled_for_course(course_key): show_email_tab = True # display course stats only if there is no other table to display: course_stats = None if not datatable: course_stats = get_course_stats_table() # disable buttons for large courses disable_buttons = False max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") if max_enrollment_for_buttons is not None: disable_buttons = enrollment_number > max_enrollment_for_buttons #---------------------------------------- # context for rendering context = { 'course': course, 'staff_access': True, 'admin_access': request.user.is_staff, 'instructor_access': instructor_access, 'forum_admin_access': forum_admin_access, 'datatable': datatable, 'course_stats': course_stats, 'msg': msg, 'modeflag': {idash_mode: 'selectedmode'}, 'studio_url': studio_url, 'show_email_tab': show_email_tab, # email 'problems': problems, # psychometrics 'plots': plots, # psychometrics 'course_errors': modulestore().get_course_errors(course.id), 'instructor_tasks': instructor_tasks, 'offline_grade_log': offline_grades_available(course_key), 'analytics_results': analytics_results, 'disable_buttons': disable_buttons, 'metrics_results': metrics_results, } context['standard_dashboard_url'] = reverse('instructor_dashboard', kwargs={'course_id': course_key.to_deprecated_string()}) return render_to_response('courseware/legacy_instructor_dashboard.html', context)
def instructor_dashboard_2(request, course_id): """ Display the instructor dashboard for a course. """ course_key = SlashSeparatedCourseKey.from_deprecated_string(course_id) course = get_course_by_id(course_key, depth=None) is_studio_course = (modulestore().get_modulestore_type(course_key) != ModuleStoreEnum.Type.xml) access = { 'admin': request.user.is_staff, 'instructor': has_access(request.user, 'instructor', course), 'finance_admin': CourseFinanceAdminRole(course_key).has_user(request.user), 'staff': has_access(request.user, 'staff', course), 'forum_admin': has_forum_access( request.user, course_key, FORUM_ROLE_ADMINISTRATOR ), } if not access['staff']: raise Http404() sections = [ _section_course_info(course_key, access), _section_membership(course_key, access), _section_student_admin(course_key, access), _section_data_download(course_key, access), _section_analytics(course_key, access), ] #check if there is corresponding entry in the CourseMode Table related to the Instructor Dashboard course course_honor_mode = CourseMode.mode_for_course(course_key, 'honor') course_mode_has_price = False if course_honor_mode and course_honor_mode.min_price > 0: course_mode_has_price = True if (settings.FEATURES.get('INDIVIDUAL_DUE_DATES') and access['instructor']): sections.insert(3, _section_extensions(course)) # Gate access to course email by feature flag & by course-specific authorization if bulk_email_is_enabled_for_course(course_key): sections.append(_section_send_email(course_key, access, course)) # Gate access to Metrics tab by featue flag and staff authorization if settings.FEATURES['CLASS_DASHBOARD'] and access['staff']: sections.append(_section_metrics(course_key, access)) # Gate access to Ecommerce tab if course_mode_has_price: sections.append(_section_e_commerce(course_key, access)) studio_url = None if is_studio_course: studio_url = get_cms_course_link(course) enrollment_count = sections[0]['enrollment_count']['total'] disable_buttons = False max_enrollment_for_buttons = settings.FEATURES.get("MAX_ENROLLMENT_INSTR_BUTTONS") if max_enrollment_for_buttons is not None: disable_buttons = enrollment_count > max_enrollment_for_buttons context = { 'course': course, 'old_dashboard_url': reverse('instructor_dashboard_legacy', kwargs={'course_id': course_key.to_deprecated_string()}), 'studio_url': studio_url, 'sections': sections, 'disable_buttons': disable_buttons, } return render_to_response('instructor/instructor_dashboard_2/instructor_dashboard_2.html', context)