add instructor dashboard beta (partial) (view, template, coffeescript, api endpoints)

This commit is contained in:
Miles Steele
2013-06-04 10:12:10 -04:00
parent 4238cb569f
commit daeabb06bf
16 changed files with 802 additions and 0 deletions

View File

View 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('<','&lt;')
return msg

View 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,
}

View 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

View 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

View 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

View 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

View 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'

View File

@@ -64,6 +64,7 @@
// instructor
@import "course/instructor/instructor";
@import "course/instructor/instructor_2";
// discussion
@import "course/discussion/form-wmd-toolbar";

View 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;
}
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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$',