add instructor dashboard beta (partial) (view, template, coffeescript, api endpoints)
This commit is contained in:
0
lms/djangoapps/analytics/__init__.py
Normal file
0
lms/djangoapps/analytics/__init__.py
Normal file
82
lms/djangoapps/analytics/basic.py
Normal file
82
lms/djangoapps/analytics/basic.py
Normal file
@@ -0,0 +1,82 @@
|
||||
"""
|
||||
Student and course analytics.
|
||||
|
||||
Serve miscellaneous course and student data
|
||||
"""
|
||||
|
||||
from django.contrib.auth.models import User
|
||||
import xmodule.graders as xmgraders
|
||||
|
||||
|
||||
AVAILABLE_STUDENT_FEATURES = ['username', 'first_name', 'last_name', 'is_staff', 'email']
|
||||
AVAILABLE_PROFILE_FEATURES = ['name', 'language', 'location', 'year_of_birth', 'gender', 'level_of_education', 'mailing_address', 'goals']
|
||||
|
||||
|
||||
def enrolled_students_profiles(course_id, features):
|
||||
"""
|
||||
Return array of student features e.g. [{?}, ...]
|
||||
"""
|
||||
# enrollments = CourseEnrollment.objects.filter(course_id=course_id)
|
||||
# students = [enrollment.user for enrollment in enrollments]
|
||||
students = User.objects.filter(courseenrollment__course_id=course_id).order_by('username').select_related('profile')
|
||||
|
||||
def extract_student(student):
|
||||
student_features = [feature for feature in features if feature in AVAILABLE_STUDENT_FEATURES]
|
||||
profile_features = [feature for feature in features if feature in AVAILABLE_PROFILE_FEATURES]
|
||||
|
||||
student_dict = dict((feature, getattr(student, feature)) for feature in student_features)
|
||||
profile = student.profile
|
||||
profile_dict = dict((feature, getattr(profile, feature)) for feature in profile_features)
|
||||
student_dict.update(profile_dict)
|
||||
return student_dict
|
||||
|
||||
return [extract_student(student) for student in students]
|
||||
|
||||
|
||||
def dump_grading_context(course):
|
||||
"""
|
||||
Dump information about course grading context (eg which problems are graded in what assignments)
|
||||
Useful for debugging grading_policy.json and policy.json
|
||||
|
||||
Returns HTML string
|
||||
"""
|
||||
msg = "-----------------------------------------------------------------------------\n"
|
||||
msg += "Course grader:\n"
|
||||
|
||||
msg += '%s\n' % course.grader.__class__
|
||||
graders = {}
|
||||
if isinstance(course.grader, xmgraders.WeightedSubsectionsGrader):
|
||||
msg += '\n'
|
||||
msg += "Graded sections:\n"
|
||||
for subgrader, category, weight in course.grader.sections:
|
||||
msg += " subgrader=%s, type=%s, category=%s, weight=%s\n" % (subgrader.__class__, subgrader.type, category, weight)
|
||||
subgrader.index = 1
|
||||
graders[subgrader.type] = subgrader
|
||||
msg += "-----------------------------------------------------------------------------\n"
|
||||
msg += "Listing grading context for course %s\n" % course.id
|
||||
|
||||
gc = course.grading_context
|
||||
msg += "graded sections:\n"
|
||||
|
||||
msg += '%s\n' % gc['graded_sections'].keys()
|
||||
for (gs, gsvals) in gc['graded_sections'].items():
|
||||
msg += "--> Section %s:\n" % (gs)
|
||||
for sec in gsvals:
|
||||
s = sec['section_descriptor']
|
||||
format = getattr(s.lms, 'format', None)
|
||||
aname = ''
|
||||
if format in graders:
|
||||
g = graders[format]
|
||||
aname = '%s %02d' % (g.short_label, g.index)
|
||||
g.index += 1
|
||||
elif s.display_name in graders:
|
||||
g = graders[s.display_name]
|
||||
aname = '%s' % g.short_label
|
||||
notes = ''
|
||||
if getattr(s, 'score_by_attempt', False):
|
||||
notes = ', score by attempt!'
|
||||
msg += " %s (format=%s, Assignment=%s%s)\n" % (s.display_name, format, aname, notes)
|
||||
msg += "all descriptors:\n"
|
||||
msg += "length=%d\n" % len(gc['all_descriptors'])
|
||||
msg = '<pre>%s</pre>' % msg.replace('<','<')
|
||||
return msg
|
||||
65
lms/djangoapps/analytics/csvs.py
Normal file
65
lms/djangoapps/analytics/csvs.py
Normal file
@@ -0,0 +1,65 @@
|
||||
"""
|
||||
Student and course analytics.
|
||||
|
||||
Format and create csv responses
|
||||
"""
|
||||
|
||||
import csv
|
||||
from django.http import HttpResponse
|
||||
|
||||
|
||||
def create_csv_response(filename, header, datarows):
|
||||
"""
|
||||
Create an HttpResponse with an attached .csv file
|
||||
|
||||
header e.g. ['Name', 'Email']
|
||||
datarows e.g. [['Jim', 'jim@edy.org'], ['Jake', 'jake@edy.org'], ...]
|
||||
"""
|
||||
response = HttpResponse(mimetype='text/csv')
|
||||
response['Content-Disposition'] = 'attachment; filename={0}'.format(filename)
|
||||
csvwriter = csv.writer(response, dialect='excel', quotechar='"', quoting=csv.QUOTE_ALL)
|
||||
csvwriter.writerow(header)
|
||||
for datarow in datarows:
|
||||
encoded_row = [unicode(s).encode('utf-8') for s in datarow]
|
||||
csvwriter.writerow(encoded_row)
|
||||
return response
|
||||
|
||||
|
||||
def format_dictlist(dictlist):
|
||||
"""
|
||||
Convert from [
|
||||
{
|
||||
'label1': 'value1,1',
|
||||
'label2': 'value2,1',
|
||||
'label3': 'value3,1',
|
||||
'label4': 'value4,1',
|
||||
},
|
||||
{
|
||||
'label1': 'value1,2',
|
||||
'label2': 'value2,2',
|
||||
'label3': 'value3,2',
|
||||
'label4': 'value4,2',
|
||||
}
|
||||
]
|
||||
|
||||
to {
|
||||
'header': ['label1', 'label2', 'label3', 'label4'],
|
||||
'datarows': ['value1,1', 'value2,1', 'value3,1', 'value4,1'], ['value1,2', 'value2,2', 'value3,2', 'value4,2']
|
||||
}
|
||||
|
||||
Do not handle empty lists.
|
||||
"""
|
||||
|
||||
header = dictlist[0].keys()
|
||||
|
||||
def dict_to_entry(d):
|
||||
ordered = sorted(d.items(), key=lambda (k, v): header.index(k))
|
||||
vals = map(lambda (k, v): v, ordered)
|
||||
return vals
|
||||
|
||||
datarows = map(dict_to_entry, dictlist)
|
||||
|
||||
return {
|
||||
'header': header,
|
||||
'datarows': datarows,
|
||||
}
|
||||
63
lms/djangoapps/analytics/distributions.py
Normal file
63
lms/djangoapps/analytics/distributions.py
Normal file
@@ -0,0 +1,63 @@
|
||||
"""
|
||||
Profile Distributions
|
||||
"""
|
||||
|
||||
from django.db.models import Count
|
||||
from django.contrib.auth.models import User, Group
|
||||
from student.models import CourseEnrollment, UserProfile
|
||||
|
||||
AVAILABLE_PROFILE_FEATURES = ['gender', 'level_of_education', 'year_of_birth']
|
||||
|
||||
|
||||
def profile_distribution(course_id, feature):
|
||||
"""
|
||||
Retrieve distribution of students over a given feature.
|
||||
feature is one of AVAILABLE_PROFILE_FEATURES.
|
||||
|
||||
Returna dictionary {'type': 'SOME_TYPE', 'data': {'key': 'val'}}
|
||||
data types e.g.
|
||||
EASY_CHOICE - choices with a restricted domain, e.g. level_of_education
|
||||
OPEN_CHOICE - choices with a larger domain e.g. year_of_birth
|
||||
"""
|
||||
|
||||
EASY_CHOICE_FEATURES = ['gender', 'level_of_education']
|
||||
OPEN_CHOICE_FEATURES = ['year_of_birth']
|
||||
|
||||
feature_results = {}
|
||||
|
||||
if not feature in AVAILABLE_PROFILE_FEATURES:
|
||||
raise ValueError("unsupported feature requested for distribution '%s'" % feature)
|
||||
|
||||
if feature in EASY_CHOICE_FEATURES:
|
||||
if feature == 'gender':
|
||||
choices = [(short, full) for (short, full) in UserProfile.GENDER_CHOICES] + [(None, 'No Data')]
|
||||
elif feature == 'level_of_education':
|
||||
choices = [(short, full) for (short, full) in UserProfile.LEVEL_OF_EDUCATION_CHOICES] + [(None, 'No Data')]
|
||||
else:
|
||||
raise ValueError("feature request not implemented for feature %s" % feature)
|
||||
|
||||
data = {}
|
||||
for (short, full) in choices:
|
||||
if feature == 'gender':
|
||||
count = CourseEnrollment.objects.filter(course_id=course_id, user__profile__gender=short).count()
|
||||
elif feature == 'level_of_education':
|
||||
count = CourseEnrollment.objects.filter(course_id=course_id, user__profile__level_of_education=short).count()
|
||||
else:
|
||||
raise ValueError("feature request not implemented for feature %s" % feature)
|
||||
data[full] = count
|
||||
|
||||
feature_results['data'] = data
|
||||
feature_results['type'] = 'EASY_CHOICE'
|
||||
elif feature in OPEN_CHOICE_FEATURES:
|
||||
profiles = UserProfile.objects.filter(user__courseenrollment__course_id=course_id)
|
||||
query_distribution = profiles.values('year_of_birth').annotate(Count('year_of_birth')).order_by()
|
||||
# query_distribution is of the form [{'attribute': 'value1', 'attribute__count': 4}, {'attribute': 'value2', 'attribute__count': 2}, ...]
|
||||
|
||||
distribution = dict((vald[feature], vald[feature + '__count']) for vald in query_distribution)
|
||||
# distribution is of the form {'value1': 4, 'value2': 2, ...}
|
||||
feature_results['data'] = distribution
|
||||
feature_results['type'] = 'OPEN_CHOICE'
|
||||
else:
|
||||
raise ValueError("feature requested for distribution has not been implemented but is advertised in AVAILABLE_PROFILE_FEATURES! '%s'" % feature)
|
||||
|
||||
return feature_results
|
||||
0
lms/djangoapps/analytics/management/__init__.py
Normal file
0
lms/djangoapps/analytics/management/__init__.py
Normal file
0
lms/djangoapps/analytics/tests/__init__.py
Normal file
0
lms/djangoapps/analytics/tests/__init__.py
Normal file
@@ -305,6 +305,11 @@ def get_course_tabs(user, course, active_page):
|
||||
tabs.append(CourseTab('Instructor',
|
||||
reverse('instructor_dashboard', args=[course.id]),
|
||||
active_page == 'instructor'))
|
||||
|
||||
if has_access(user, course, 'staff'):
|
||||
tabs.append(CourseTab('Instructor 2',
|
||||
reverse('instructor_dashboard_2', args=[course.id]),
|
||||
active_page == 'instructor_2'))
|
||||
return tabs
|
||||
|
||||
|
||||
@@ -356,6 +361,11 @@ def get_default_tabs(user, course, active_page):
|
||||
link = reverse('instructor_dashboard', args=[course.id])
|
||||
tabs.append(CourseTab('Instructor', link, active_page == 'instructor'))
|
||||
|
||||
if has_access(user, course, 'staff'):
|
||||
tabs.append(CourseTab('Instructor 2',
|
||||
reverse('instructor_dashboard_2', args=[course.id]),
|
||||
active_page == 'instructor_2'))
|
||||
|
||||
return tabs
|
||||
|
||||
|
||||
|
||||
134
lms/djangoapps/instructor/views/api.py
Normal file
134
lms/djangoapps/instructor/views/api.py
Normal file
@@ -0,0 +1,134 @@
|
||||
"""
|
||||
Instructor Dashboard API views
|
||||
|
||||
Non-html views which the instructor dashboard requests.
|
||||
|
||||
TODO add tracking
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.html import escape
|
||||
from django.http import HttpResponse, HttpResponseBadRequest
|
||||
|
||||
from django.conf import settings
|
||||
from courseware.access import has_access, get_access_group_name, course_beta_test_group_name
|
||||
from courseware.courses import get_course_with_access
|
||||
from django_comment_client.utils import has_forum_access
|
||||
from instructor.offline_gradecalc import student_grades, offline_grades_available
|
||||
from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from student.models import CourseEnrollment
|
||||
from django.contrib.auth.models import User
|
||||
|
||||
import analytics.basic
|
||||
import analytics.distributions
|
||||
import analytics.csvs
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def grading_config(request, course_id):
|
||||
"""
|
||||
Respond with json which contains a html formatted grade summary.
|
||||
|
||||
TODO maybe this shouldn't be html already
|
||||
"""
|
||||
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
|
||||
grading_config_summary = analytics.basic.dump_grading_context(course)
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'grading_config_summary': grading_config_summary,
|
||||
}
|
||||
response = HttpResponse(json.dumps(response_payload), content_type="application/json")
|
||||
return response
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def enrolled_students_profiles(request, course_id, csv=False):
|
||||
"""
|
||||
Respond with json which contains a summary of all enrolled students profile information.
|
||||
|
||||
Response {"students": [{-student-info-}, ...]}
|
||||
|
||||
TODO accept requests for different attribute sets
|
||||
"""
|
||||
|
||||
available_features = analytics.basic.AVAILABLE_STUDENT_FEATURES + analytics.basic.AVAILABLE_PROFILE_FEATURES
|
||||
query_features = ['username', 'name', 'language', 'location', 'year_of_birth', 'gender',
|
||||
'level_of_education', 'mailing_address', 'goals']
|
||||
|
||||
student_data = analytics.basic.enrolled_students_profiles(course_id, query_features)
|
||||
|
||||
if not csv:
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'students': student_data,
|
||||
'students_count': len(student_data),
|
||||
'available_features': available_features
|
||||
}
|
||||
response = HttpResponse(json.dumps(response_payload), content_type="application/json")
|
||||
return response
|
||||
else:
|
||||
formatted = analytics.csvs.format_dictlist(student_data)
|
||||
return analytics.csvs.create_csv_response("enrolled_profiles.csv", formatted['header'], formatted['datarows'])
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def profile_distribution(request, course_id):
|
||||
"""
|
||||
Respond with json of the distribution of students over selected fields which have choices.
|
||||
|
||||
Ask for features through the 'features' query parameter.
|
||||
The features query parameter can be either a single feature name, or a json string of feature names.
|
||||
e.g.
|
||||
http://localhost:8000/courses/MITx/6.002x/2013_Spring/instructor_dashboard/api/profile_distribution?features=level_of_education
|
||||
http://localhost:8000/courses/MITx/6.002x/2013_Spring/instructor_dashboard/api/profile_distribution?features=%5B%22year_of_birth%22%2C%22gender%22%5D
|
||||
|
||||
Example js query:
|
||||
$.get("http://localhost:8000/courses/MITx/6.002x/2013_Spring/instructor_dashboard/api/profile_distribution",
|
||||
{'features': JSON.stringify(['year_of_birth', 'gender'])},
|
||||
function(){console.log(arguments[0])})
|
||||
|
||||
TODO how should query parameter interpretation work?
|
||||
TODO respond to csv requests as well
|
||||
"""
|
||||
|
||||
try:
|
||||
features = json.loads(request.GET.get('features'))
|
||||
except Exception:
|
||||
features = [request.GET.get('features')]
|
||||
|
||||
feature_results = {}
|
||||
|
||||
for feature in features:
|
||||
try:
|
||||
feature_results[feature] = analytics.distributions.profile_distribution(course_id, feature)
|
||||
except Exception as e:
|
||||
feature_results[feature] = {'error': "can not find distribution for '%s'" % feature}
|
||||
raise e
|
||||
|
||||
response_payload = {
|
||||
'course_id': course_id,
|
||||
'queried_features': features,
|
||||
'available_features': analytics.distributions.AVAILABLE_PROFILE_FEATURES,
|
||||
'display_names': {
|
||||
'gender': 'Gender',
|
||||
'level_of_education': 'Level of Education',
|
||||
'year_of_birth': 'Year Of Birth',
|
||||
},
|
||||
'feature_results': feature_results,
|
||||
}
|
||||
response = HttpResponse(json.dumps(response_payload), content_type="application/json")
|
||||
return response
|
||||
110
lms/djangoapps/instructor/views/instructor_dashboard.py
Normal file
110
lms/djangoapps/instructor/views/instructor_dashboard.py
Normal file
@@ -0,0 +1,110 @@
|
||||
"""
|
||||
Instructor Dashboard Views
|
||||
|
||||
TODO add tracking
|
||||
"""
|
||||
|
||||
import csv
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import requests
|
||||
from django_future.csrf import ensure_csrf_cookie
|
||||
from django.views.decorators.cache import cache_control
|
||||
from mitxmako.shortcuts import render_to_response
|
||||
from django.core.urlresolvers import reverse
|
||||
from django.utils.html import escape
|
||||
|
||||
from django.conf import settings
|
||||
from courseware.access import has_access, get_access_group_name, course_beta_test_group_name
|
||||
from courseware.courses import get_course_with_access
|
||||
from django_comment_client.utils import has_forum_access
|
||||
from instructor.offline_gradecalc import student_grades, offline_grades_available
|
||||
from django_comment_common.models import Role, FORUM_ROLE_ADMINISTRATOR, FORUM_ROLE_MODERATOR, FORUM_ROLE_COMMUNITY_TA
|
||||
from xmodule.modulestore.django import modulestore
|
||||
from student.models import CourseEnrollment
|
||||
|
||||
|
||||
@ensure_csrf_cookie
|
||||
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
|
||||
def instructor_dashboard_2(request, course_id):
|
||||
"""Display the instructor dashboard for a course."""
|
||||
|
||||
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
|
||||
instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists
|
||||
forum_admin_access = has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR)
|
||||
|
||||
section_data = {
|
||||
'course_info': _section_course_info(request, course_id),
|
||||
'enrollment': _section_enrollment(course_id),
|
||||
'student_admin': _section_student_admin(course_id),
|
||||
'data_download': _section_data_download(course_id),
|
||||
'analytics': _section_analytics(course_id),
|
||||
}
|
||||
|
||||
context = {
|
||||
'course': course,
|
||||
'staff_access': True,
|
||||
'admin_access': request.user.is_staff,
|
||||
'instructor_access': instructor_access,
|
||||
'forum_admin_access': forum_admin_access,
|
||||
'djangopid': os.getpid(),
|
||||
'mitx_version': getattr(settings, 'MITX_VERSION_STRING', ''),
|
||||
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
|
||||
'section_data': section_data
|
||||
}
|
||||
|
||||
return render_to_response('courseware/instructor_dashboard_2/instructor_dashboard_2.html', context)
|
||||
|
||||
|
||||
def _section_course_info(request, course_id):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
|
||||
|
||||
section_data = {}
|
||||
section_data['course_id'] = course_id
|
||||
section_data['display_name'] = course.display_name
|
||||
section_data['enrollment_count'] = CourseEnrollment.objects.filter(course_id=course_id).count()
|
||||
section_data['has_started'] = course.has_started()
|
||||
section_data['has_ended'] = course.has_ended()
|
||||
section_data['grade_cutoffs'] = "[" + reduce(lambda memo, (letter, score): "{}: {}, ".format(letter, score) + memo , course.grade_cutoffs.items(), "")[:-2] + "]"
|
||||
section_data['offline_grades'] = offline_grades_available(course_id)
|
||||
|
||||
try:
|
||||
section_data['course_errors'] = [(escape(a), '') for (a,b) in modulestore().get_item_errors(course.location)]
|
||||
except Exception:
|
||||
section_data['course_errors'] = [('Error fetching errors', '')]
|
||||
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_enrollment(course_id):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
section_data = {}
|
||||
section_data['placeholder'] = "Enrollment content."
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_student_admin(course_id):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
section_data = {}
|
||||
section_data['placeholder'] = "Student Admin content."
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_data_download(course_id):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
section_data = {
|
||||
'grading_config_url': reverse('grading_config', kwargs={'course_id': course_id}),
|
||||
'enrolled_students_profiles_url': reverse('enrolled_students_profiles', kwargs={'course_id': course_id}),
|
||||
}
|
||||
return section_data
|
||||
|
||||
|
||||
def _section_analytics(course_id):
|
||||
""" Provide data for the corresponding dashboard section """
|
||||
section_data = {
|
||||
'profile_distributions_url': reverse('profile_distribution', kwargs={'course_id': course_id}),
|
||||
}
|
||||
return section_data
|
||||
120
lms/static/coffee/src/instructor_dashboard.coffee
Normal file
120
lms/static/coffee/src/instructor_dashboard.coffee
Normal file
@@ -0,0 +1,120 @@
|
||||
# Instructor Dashboard Tab Manager
|
||||
|
||||
log = -> console.log.apply console, arguments
|
||||
|
||||
CSS_INSTRUCTOR_CONTENT = 'instructor-dashboard-content-2'
|
||||
CSS_ACTIVE_SECTION = 'active-section'
|
||||
CSS_IDASH_SECTION = 'idash-section'
|
||||
CSS_IDASH_DEFAULT_SECTION = 'idash-default-section'
|
||||
CSS_INSTRUCTOR_NAV = 'instructor-nav'
|
||||
|
||||
HASH_LINK_PREFIX = '#view-'
|
||||
|
||||
|
||||
# once we're ready, check if this page has the instructor dashboard
|
||||
$ =>
|
||||
instructor_dashboard_content = $ ".#{CSS_INSTRUCTOR_CONTENT}"
|
||||
if instructor_dashboard_content.length != 0
|
||||
log "setting up instructor dashboard"
|
||||
setup_instructor_dashboard instructor_dashboard_content
|
||||
setup_instructor_dashboard_sections instructor_dashboard_content
|
||||
|
||||
|
||||
# enable links
|
||||
setup_instructor_dashboard = (idash_content) =>
|
||||
links = idash_content.find(".#{CSS_INSTRUCTOR_NAV}").find('a')
|
||||
# setup section header click handlers
|
||||
for link in ($ link for link in links)
|
||||
link.click (e) ->
|
||||
# deactivate (styling) all sections
|
||||
idash_content.find(".#{CSS_IDASH_SECTION}").removeClass CSS_ACTIVE_SECTION
|
||||
idash_content.find(".#{CSS_INSTRUCTOR_NAV}").children().removeClass CSS_ACTIVE_SECTION
|
||||
|
||||
# find paired section
|
||||
section_name = $(this).data 'section'
|
||||
section = idash_content.find "##{section_name}"
|
||||
|
||||
# activate (styling) active
|
||||
section.addClass CSS_ACTIVE_SECTION
|
||||
$(this).addClass CSS_ACTIVE_SECTION
|
||||
|
||||
# write deep link
|
||||
location.hash = "#{HASH_LINK_PREFIX}#{section_name}"
|
||||
|
||||
log "clicked #{section_name}"
|
||||
e.preventDefault()
|
||||
|
||||
# recover deep link from url
|
||||
# click default or go to section specified by hash
|
||||
if (new RegExp "^#{HASH_LINK_PREFIX}").test location.hash
|
||||
rmatch = (new RegExp "^#{HASH_LINK_PREFIX}(.*)").exec location.hash
|
||||
section_name = rmatch[1]
|
||||
link = links.filter "[data-section='#{section_name}']"
|
||||
link.click()
|
||||
else
|
||||
links.filter(".#{CSS_IDASH_DEFAULT_SECTION}").click()
|
||||
|
||||
|
||||
# call setup handlers for each section
|
||||
setup_instructor_dashboard_sections = (idash_content) ->
|
||||
log "setting up instructor dashboard sections"
|
||||
setup_section_data_download idash_content.find(".#{CSS_IDASH_SECTION}#data-download")
|
||||
setup_section_analytics idash_content.find(".#{CSS_IDASH_SECTION}#analytics")
|
||||
|
||||
|
||||
# setup the data download section
|
||||
setup_section_data_download = (section) ->
|
||||
list_studs_btn = section.find("input[name='list-profiles']'")
|
||||
list_studs_btn.click (e) ->
|
||||
log "fetching student list"
|
||||
url = $(this).data('endpoint')
|
||||
if $(this).data 'csv'
|
||||
url += '/csv'
|
||||
location.href = url
|
||||
else
|
||||
$.getJSON url, (data) ->
|
||||
section.find('.dumped-data-display').text JSON.stringify(data)
|
||||
|
||||
grade_config_btn = section.find("input[name='dump-gradeconf']'")
|
||||
grade_config_btn.click (e) ->
|
||||
log "fetching grading config"
|
||||
url = $(this).data('endpoint')
|
||||
$.getJSON url, (data) ->
|
||||
section.find('.dumped-data-display').html data['grading_config_summary']
|
||||
|
||||
|
||||
# setup the analytics section
|
||||
setup_section_analytics = (section) ->
|
||||
log "setting up instructor dashboard section - analytics"
|
||||
|
||||
distribution_select = section.find('select#distributions')
|
||||
# ask for available distributions
|
||||
$.getJSON distribution_select.data('endpoint'), features: JSON.stringify([]), (data) ->
|
||||
distribution_select.find('option').eq(0).text "-- Select distribution"
|
||||
|
||||
for feature in data.available_features
|
||||
opt = $ '<option/>',
|
||||
text: data.display_names[feature]
|
||||
data:
|
||||
feature: feature
|
||||
|
||||
distribution_select.append opt
|
||||
|
||||
distribution_select.change ->
|
||||
opt = $(this).children('option:selected')
|
||||
log "distribution selected: #{opt.data 'feature'}"
|
||||
feature = opt.data 'feature'
|
||||
$.getJSON distribution_select.data('endpoint'), features: JSON.stringify([feature]), (data) ->
|
||||
feature_res = data.feature_results[feature]
|
||||
# feature response format: {'error': 'optional error string', 'type': 'SOME_TYPE', 'data': [stuff]}
|
||||
display = section.find('.distribution-display').eq(0)
|
||||
if feature_res.error
|
||||
console.warn(feature_res.error)
|
||||
display.text 'Error fetching data'
|
||||
else
|
||||
if feature_res.type is 'EASY_CHOICE'
|
||||
display.text JSON.stringify(feature_res.data)
|
||||
log feature_res.data
|
||||
else
|
||||
console.warn("don't know how to show #{feature_res.type}")
|
||||
display.text 'Unavailable Metric'
|
||||
@@ -64,6 +64,7 @@
|
||||
|
||||
// instructor
|
||||
@import "course/instructor/instructor";
|
||||
@import "course/instructor/instructor_2";
|
||||
|
||||
// discussion
|
||||
@import "course/discussion/form-wmd-toolbar";
|
||||
|
||||
77
lms/static/sass/course/instructor/_instructor_2.scss
Normal file
77
lms/static/sass/course/instructor/_instructor_2.scss
Normal file
@@ -0,0 +1,77 @@
|
||||
.instructor-dashboard-wrapper-2 {
|
||||
@extend .table-wrapper;
|
||||
display: table;
|
||||
|
||||
section.instructor-dashboard-content-2 {
|
||||
@extend .content;
|
||||
padding: 40px;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
|
||||
h1 {
|
||||
@extend .top-header;
|
||||
}
|
||||
|
||||
.instructor_dash_glob_info {
|
||||
text-align: right;
|
||||
position: absolute;
|
||||
top: 46px;
|
||||
right: 50px;
|
||||
}
|
||||
|
||||
.instructor-nav {
|
||||
.active-section {
|
||||
color: #551A8B;
|
||||
}
|
||||
}
|
||||
|
||||
section.idash-section {
|
||||
// background-color: #0f0;
|
||||
display: none;
|
||||
|
||||
&.active-section {
|
||||
// background-color: #ff0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.basic-data {
|
||||
padding: 6px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.instructor-dashboard-wrapper-2 section.idash-section#course-info {
|
||||
.error-log {
|
||||
margin-top: 1em;
|
||||
|
||||
.course-error {
|
||||
margin-bottom: 1em;
|
||||
|
||||
code {
|
||||
&.course-error-first {
|
||||
color: #111;
|
||||
}
|
||||
|
||||
&.course-error-second {
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.instructor-dashboard-wrapper-2 section.idash-section#data-download {
|
||||
input {
|
||||
display: block;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.instructor-dashboard-wrapper-2 section.idash-section#analytics {
|
||||
.distribution-display {
|
||||
margin-top: 1em;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<div class="basic-data">
|
||||
Course Name:
|
||||
${ section_data['course_info']['display_name'] }
|
||||
</div>
|
||||
|
||||
<div class="basic-data">
|
||||
Course ID:
|
||||
${ section_data['course_info']['course_id'] }
|
||||
</div>
|
||||
|
||||
<div class="basic-data">
|
||||
Students Enrolled:
|
||||
${ section_data['course_info']['enrollment_count'] }
|
||||
</div>
|
||||
|
||||
<div class="basic-data">
|
||||
Started:
|
||||
${ section_data['course_info']['has_started'] }
|
||||
</div>
|
||||
|
||||
<div class="basic-data">
|
||||
Ended:
|
||||
${ section_data['course_info']['has_ended'] }
|
||||
</div>
|
||||
|
||||
<div class="basic-data">
|
||||
Grade Cutoffs:
|
||||
${ section_data['course_info']['grade_cutoffs'] }
|
||||
</div>
|
||||
|
||||
<div class="basic-data">
|
||||
Offline Grades Available:
|
||||
${ section_data['course_info']['offline_grades'] }
|
||||
</div>
|
||||
|
||||
<div class="error-log">
|
||||
<h2>Course Errors:</h2>
|
||||
%for error in section_data['course_info']['course_errors']:
|
||||
<div class="course-error">
|
||||
<code class=course-error-first> ${ error[0] } </code><br>
|
||||
<code class=course-error-second> ${ error[1] } </code>
|
||||
</div>
|
||||
%endfor
|
||||
</div>
|
||||
|
||||
## <div class="basic-data">
|
||||
## Section Dump<br>
|
||||
## ${ section_data['course_info'] }
|
||||
## </div>
|
||||
@@ -0,0 +1,79 @@
|
||||
<%inherit file="/main.html" />
|
||||
<%! from django.core.urlresolvers import reverse %>
|
||||
<%namespace name='static' file='/static_content.html'/>
|
||||
|
||||
<%block name="headextra">
|
||||
<%static:css group='course'/>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/underscore-min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/flot/jquery.flot.axislabels.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-1.1.1.min.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/vendor/jquery-jvectormap-1.1.1/jquery-jvectormap-world-mill-en.js')}"></script>
|
||||
<script type="text/javascript" src="${static.url('js/course_groups/cohorts.js')}"></script>
|
||||
|
||||
</%block>
|
||||
|
||||
<%include file="/courseware/course_navigation.html" args="active_page='instructor_2'" />
|
||||
|
||||
<style type="text/css"></style>
|
||||
|
||||
<script language="JavaScript" type="text/javascript"></script>
|
||||
|
||||
<section class="container">
|
||||
<div class="instructor-dashboard-wrapper-2">
|
||||
<section class="instructor-dashboard-content-2">
|
||||
|
||||
<h1>Instructor Dashboard</h1>
|
||||
<div class="instructor_dash_glob_info">
|
||||
<span id="djangopid">${djangopid}</span> |
|
||||
<span id="mitxver">${mitx_version}</span>
|
||||
</div>
|
||||
|
||||
## links which are tied to idash-sections below.
|
||||
## the links are acativated and handled in instructor_dashboard.coffee
|
||||
## when the javascript loads, it clicks on idash-default-section
|
||||
<h2 class="instructor-nav">[
|
||||
<a href="" data-section="course-info" class="idash-default-section"> Course Info </a> |
|
||||
<a href="" data-section="enrollment"> Enrollment </a> |
|
||||
<a href="" data-section="student-admin"> Student Admin </a> |
|
||||
<a href="" data-section="data-download"> Data Download </a> |
|
||||
<a href="" data-section="analytics"> Analytics </a>
|
||||
]</h2>
|
||||
|
||||
## each section corresponds to a section_data sub-dictionary provided by the view
|
||||
## to keep this short, sections can be pulled out into their own files
|
||||
|
||||
<section id="course-info" class="idash-section">
|
||||
<%include file="course_info.html"/>
|
||||
</section>
|
||||
|
||||
|
||||
<section id="enrollment" class="idash-section">
|
||||
${ section_data['enrollment']['placeholder'] }
|
||||
</section>
|
||||
|
||||
|
||||
<section id="student-admin" class="idash-section">
|
||||
${ section_data['student_admin']['placeholder'] }
|
||||
</section>
|
||||
|
||||
|
||||
<section id="data-download" class="idash-section">
|
||||
<input type="submit" name="list-profiles" value="List enrolled students with profile information" data-endpoint="${ section_data['data_download']['enrolled_students_profiles_url'] }" ></input>
|
||||
<input type="submit" name="list-profiles" value="[CSV]" data-csv="true" data-endpoint="${ section_data['data_download']['enrolled_students_profiles_url'] }" ></input>
|
||||
<input type="submit" name="list-grades" value="Student grades"></input>
|
||||
<input type="submit" name="list-answer-distributions" value="Answer distributions (x students got y points)"></input>
|
||||
<input type="submit" name="dump-gradeconf" value="Grading Configuration" data-endpoint="${ section_data['data_download']['grading_config_url'] }"></input>
|
||||
<div class="dumped-data-display"></div>
|
||||
</section>
|
||||
|
||||
<section id="analytics" class="idash-section">
|
||||
<select id="distributions" data-endpoint="${ section_data['analytics']['profile_distributions_url'] }">
|
||||
<option> Getting available distributions... </option>
|
||||
</select>
|
||||
<div class="distribution-display"></div>
|
||||
</section>
|
||||
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
12
lms/urls.py
12
lms/urls.py
@@ -251,6 +251,18 @@ if settings.COURSEWARE_ENABLED:
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor$',
|
||||
'instructor.views.legacy.instructor_dashboard', name="instructor_dashboard"),
|
||||
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard$',
|
||||
'instructor.views.instructor_dashboard.instructor_dashboard_2', name="instructor_dashboard_2"),
|
||||
|
||||
# api endpoints for instructor
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/grading_config$',
|
||||
'instructor.views.api.grading_config', name="grading_config"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/enrolled_students_profiles(?P<csv>/csv)?$',
|
||||
'instructor.views.api.enrolled_students_profiles', name="enrolled_students_profiles"),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/profile_distribution$',
|
||||
'instructor.views.api.profile_distribution', name="profile_distribution"),
|
||||
|
||||
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/gradebook$',
|
||||
'instructor.views.legacy.gradebook', name='gradebook'),
|
||||
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/grade_summary$',
|
||||
|
||||
Reference in New Issue
Block a user