Commit 02432e05 by Miles Steele

integrate slickgrid, add instructor.enrollment, add instructor.access, refactor & clean

parent 5d4a5fcd
......@@ -50,7 +50,7 @@ def profile_distribution(course_id, feature):
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 = profiles.values(feature).annotate(Count(feature)).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)
......
"""
Access control operations for use by instructor APIs.
Does not include any access control, be sure to check access before calling.
TODO sync instructor and staff flags
e.g. should these be possible?
{instructor: true, staff: false}
{instructor: true, staff: true}
"""
from django.contrib.auth.models import User, Group
from courseware.access import get_access_group_name
def allow_access(course, user, level):
"""
Allow user access to course modification.
level is one of ['instructor', 'staff']
"""
_change_access(course, user, level, 'allow')
def revoke_access(course, user, level):
"""
Revoke access from user to course modification.
level is one of ['instructor', 'staff']
"""
_change_access(course, user, level, 'revoke')
def _change_access(course, user, level, mode):
"""
Change access of user.
level is one of ['instructor', 'staff']
mode is one of ['allow', 'revoke']
"""
grpname = get_access_group_name(course, level)
group, _ = Group.objects.get_or_create(name=grpname)
if mode == 'allow':
user.groups.add(group)
elif mode == 'revoke':
user.groups.remove(group)
else:
raise ValueError("unrecognized mode '{}'".format(mode))
"""
Enrollment operations for use by instructor APIs.
Does not include any access control, be sure to check access before calling.
"""
import re
from django.contrib.auth.models import User
from student.models import CourseEnrollment, CourseEnrollmentAllowed
def enroll_emails(course_id, student_emails, auto_enroll=False):
"""
Enroll multiple students by email.
students is a list of student emails e.g. ["foo@bar.com", "bar@foo.com]
each of whom possibly does not exist in db.
status contains the relevant prior state and action performed on the user.
ce stands for CourseEnrollment
cea stands for CourseEnrollmentAllowed
! stands for the object not existing prior to the action
return a mapping from status to emails.
"""
auto_string = {False: 'allowed', True: 'willautoenroll'}[auto_enroll]
status_map = {
'user/ce/alreadyenrolled': [],
'user/!ce/enrolled': [],
'user/!ce/rejected': [],
'!user/cea/' + auto_string: [],
'!user/!cea/' + auto_string: [],
}
for student_email in student_emails:
# status: user
try:
user = User.objects.get(email=student_email)
# status: user/ce
try:
CourseEnrollment.objects.get(user=user, course_id=course_id)
status_map['user/ce/alreadyenrolled'].append(student_email)
# status: user/!ce
except CourseEnrollment.DoesNotExist:
# status: user/!ce/enrolled
try:
ce = CourseEnrollment(user=user, course_id=course_id)
ce.save()
status_map['user/!ce/enrolled'].append(student_email)
# status: user/!ce/rejected
except:
status_map['user/!ce/rejected'].append(student_email)
# status: !user
except User.DoesNotExist:
# status: !user/cea
try:
cea = CourseEnrollmentAllowed.objects.get(course_id=course_id, email=student_email)
cea.auto_enroll = auto_enroll
cea.save()
status_map['!user/cea/' + auto_string].append(student_email)
# status: !user/!cea
except CourseEnrollmentAllowed.DoesNotExist:
cea = CourseEnrollmentAllowed(course_id=course_id, email=student_email, auto_enroll=auto_enroll)
cea.save()
status_map['!user/!cea/' + auto_string].append(student_email)
return status_map
def unenroll_emails(course_id, student_emails):
"""
Unenroll multiple students by email.
students is a list of student emails e.g. ["foo@bar.com", "bar@foo.com]
each of whom possibly does not exist in db.
Fail quietly on student emails that do not match any users or allowed enrollments.
status contains the relevant prior state and action performed on the user.
ce stands for CourseEnrollment
cea stands for CourseEnrollmentAllowed
! stands for the object not existing prior to the action
return a mapping from status to emails.
"""
# NOTE these are not mutually exclusive
status_map = {
'cea/disallowed': [],
'ce/unenrolled': [],
'ce/rejected': [],
'!ce/notenrolled': [],
}
for student_email in student_emails:
# delete CourseEnrollmentAllowed
try:
cea = CourseEnrollmentAllowed.objects.get(course_id=course_id, email=student_email)
cea.delete()
status_map['cea/disallowed'].append(student_email)
except CourseEnrollmentAllowed.DoesNotExist:
pass
# delete CourseEnrollment
try:
ce = CourseEnrollment.objects.get(course_id=course_id, user__email=student_email)
try:
ce.delete()
status_map['ce/unenrolled'].append(student_email)
except Exception:
status_map['ce/rejected'].append(student_email)
except CourseEnrollment.DoesNotExist:
status_map['!ce/notenrolled'].append(student_email)
return status_map
def split_input_list(str_list):
"""
Separate out individual student email from the comma, or space separated string.
e.g.
in: "Lorem@ipsum.dolor, sit@amet.consectetur\nadipiscing@elit.Aenean\r convallis@at.lacus\r, ut@lacinia.Sed"
out: ['Lorem@ipsum.dolor', 'sit@amet.consectetur', 'adipiscing@elit.Aenean', 'convallis@at.lacus', 'ut@lacinia.Sed']
In:
students: string coming from the input text area
Return:
students: list of cleaned student emails
students_lc: list of lower case cleaned student emails
"""
new_list = re.split(r'[\n\r\s,]', str_list)
new_list = [str(s.strip()) for s in new_list]
new_list = [s for s in new_list if s != '']
return new_list
......@@ -4,31 +4,19 @@ Instructor Dashboard API views
Non-html views which the instructor dashboard requests.
TODO add tracking
TODO a lot of these GETs should be PUTs
"""
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.http import HttpResponse
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
from django.contrib.auth.models import User, Group
from instructor.enrollment import split_input_list, enroll_emails, unenroll_emails
from instructor.access import allow_access, revoke_access
import analytics.basic
import analytics.distributions
import analytics.csvs
......@@ -36,6 +24,61 @@ import analytics.csvs
@ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True)
def enroll_unenroll(request, course_id):
"""
Enroll or unenroll students by email.
"""
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
emails_to_enroll = split_input_list(request.GET.get('enroll', ''))
emails_to_unenroll = split_input_list(request.GET.get('unenroll', ''))
enrolled_result = enroll_emails(course_id, emails_to_enroll)
unenrolled_result = unenroll_emails(course_id, emails_to_unenroll)
response_payload = {
'enrolled': enrolled_result,
'unenrolled': unenrolled_result,
}
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 access_allow_revoke(request, course_id):
"""
Modify staff/instructor access. (instructor available only)
Query parameters:
email is the target users email
level is one of ['instructor', 'staff']
mode is one of ['allow', 'revoke']
"""
course = get_course_with_access(request.user, course_id, 'instructor', depth=None)
email = request.GET.get('email')
level = request.GET.get('level')
mode = request.GET.get('mode')
user = User.objects.get(email=email)
if mode == 'allow':
allow_access(course, user, level)
elif mode == 'revoke':
revoke_access(course, user, level)
else:
raise ValueError("unrecognized mode '{}'".format(mode))
response_payload = {
'done': 'yup',
}
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 grading_config(request, course_id):
"""
Respond with json which contains a html formatted grade summary.
......@@ -63,9 +106,10 @@ def enrolled_students_profiles(request, course_id, csv=False):
TODO accept requests for different attribute sets
"""
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
available_features = analytics.basic.AVAILABLE_STUDENT_FEATURES + analytics.basic.AVAILABLE_PROFILE_FEATURES
query_features = ['username', 'name', 'language', 'location', 'year_of_birth', 'gender',
query_features = ['username', 'name', 'email', 'language', 'location', 'year_of_birth', 'gender',
'level_of_education', 'mailing_address', 'goals']
student_data = analytics.basic.enrolled_students_profiles(course_id, query_features)
......@@ -75,7 +119,8 @@ def enrolled_students_profiles(request, course_id, csv=False):
'course_id': course_id,
'students': student_data,
'students_count': len(student_data),
'available_features': available_features
'queried_features': query_features,
'available_features': available_features,
}
response = HttpResponse(json.dumps(response_payload), content_type="application/json")
return response
......@@ -104,6 +149,7 @@ def profile_distribution(request, course_id):
TODO how should query parameter interpretation work?
TODO respond to csv requests as well
"""
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
try:
features = json.loads(request.GET.get('features'))
......
......@@ -15,10 +15,11 @@ 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 Http404
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 courseware.courses import get_course_by_id
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
......@@ -31,17 +32,21 @@ from student.models import CourseEnrollment
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)
course = get_course_by_id(course_id, depth=None)
instructor_access = has_access(request.user, course, 'instructor') # an instructor can manage staff lists
staff_access = has_access(request.user, course, 'staff')
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),
}
if not staff_access:
raise Http404
sections = [
_section_course_info(course_id),
_section_enrollment(course_id),
_section_student_admin(course_id),
_section_data_download(course_id),
_section_analytics(course_id),
]
context = {
'course': course,
......@@ -52,19 +57,34 @@ def instructor_dashboard_2(request, course_id):
'djangopid': os.getpid(),
'mitx_version': getattr(settings, 'MITX_VERSION_STRING', ''),
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}),
'section_data': section_data
'sections': sections
}
return render_to_response('courseware/instructor_dashboard_2/instructor_dashboard_2.html', context)
def _section_course_info(request, course_id):
"""
Section functions starting with _section return a dictionary of section data.
The dictionary must include at least {
'section_key': 'circus_expo'
'section_display_name': 'Circus Expo'
}
section_display_name will be used to generate link titles in the nav bar.
sek will be used as a css attribute, javascript tie-in, and template import filename.
"""
def _section_course_info(course_id):
""" Provide data for the corresponding dashboard section """
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
course = get_course_by_id(course_id, depth=None)
section_data = {}
section_data['section_key'] = 'course_info'
section_data['section_display_name'] = 'Course Info'
section_data['course_id'] = course_id
section_data['display_name'] = course.display_name
section_data['course_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()
......@@ -81,21 +101,29 @@ def _section_course_info(request, course_id):
def _section_enrollment(course_id):
""" Provide data for the corresponding dashboard section """
section_data = {}
section_data['placeholder'] = "Enrollment content."
section_data = {
'section_key': 'enrollment',
'section_display_name': 'Enrollment',
'enroll_button_url': reverse('enroll_unenroll', kwargs={'course_id': course_id}),
'unenroll_button_url': reverse('enroll_unenroll', kwargs={'course_id': course_id}),
}
return section_data
def _section_student_admin(course_id):
""" Provide data for the corresponding dashboard section """
section_data = {}
section_data['placeholder'] = "Student Admin content."
section_data = {
'section_key': 'student_admin',
'section_display_name': 'Student Admin',
}
return section_data
def _section_data_download(course_id):
""" Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'data_download',
'section_display_name': 'Data Download',
'grading_config_url': reverse('grading_config', kwargs={'course_id': course_id}),
'enrolled_students_profiles_url': reverse('enrolled_students_profiles', kwargs={'course_id': course_id}),
}
......@@ -105,6 +133,8 @@ def _section_data_download(course_id):
def _section_analytics(course_id):
""" Provide data for the corresponding dashboard section """
section_data = {
'section_key': 'analytics',
'section_display_name': 'Analytics',
'profile_distributions_url': reverse('profile_distribution', kwargs={'course_id': course_id}),
}
return section_data
......@@ -5,7 +5,6 @@ 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-'
......@@ -52,18 +51,117 @@ setup_instructor_dashboard = (idash_content) =>
link = links.filter "[data-section='#{section_name}']"
link.click()
else
links.filter(".#{CSS_IDASH_DEFAULT_SECTION}").click()
links.eq(0).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_enrollment idash_content.find(".#{CSS_IDASH_SECTION}#enrollment")
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_enrollment = (section) ->
log "setting up instructor dashboard section - enrollment"
emails_input = section.find("textarea[name='student-emails']'")
btn_enroll = section.find("input[name='enroll']'")
btn_unenroll = section.find("input[name='unenroll']'")
task_response = section.find(".task-response")
emails_input.click -> log 'click emails_input'
btn_enroll.click -> log 'click btn_enroll'
btn_unenroll.click -> log 'click btn_unenroll'
btn_enroll.click -> $.getJSON btn_enroll.data('endpoint'), enroll: emails_input.val() , (data) ->
log 'received response for enroll button', data
display_response(data)
btn_unenroll.click -> $.getJSON btn_unenroll.data('endpoint'), unenroll: emails_input.val() , (data) ->
log 'received response for unenroll button', data
display_response(data)
display_response = (data_from_server) ->
task_response.empty()
response_code_dict = _.extend {}, data_from_server.enrolled, data_from_server.unenrolled
# response_code_dict e.g. {'code': ['email1', 'email2'], ...}
message_ordering = [
'msg_error_enroll'
'msg_error_unenroll'
'msg_enrolled'
'msg_unenrolled'
'msg_willautoenroll'
'msg_allowed'
'msg_disallowed'
'msg_already_enrolled'
'msg_notenrolled'
]
msg_to_txt = {
msg_already_enrolled: "Already enrolled:"
msg_enrolled: "Enrolled:"
msg_error_enroll: "There was an error enrolling these students:"
msg_allowed: "These students will be allowed to enroll once they register:"
msg_willautoenroll: "These students will be enrolled once they register:"
msg_unenrolled: "Unenrolled:"
msg_error_unenroll: "There was an error unenrolling these students:"
msg_disallowed: "These students were removed from those who can enroll once they register:"
msg_notenrolled: "These students were not enrolled:"
}
msg_to_codes = {
msg_already_enrolled: ['user/ce/alreadyenrolled']
msg_enrolled: ['user/!ce/enrolled']
msg_error_enroll: ['user/!ce/rejected']
msg_allowed: ['!user/cea/allowed', '!user/!cea/allowed']
msg_willautoenroll: ['!user/cea/willautoenroll', '!user/!cea/willautoenroll']
msg_unenrolled: ['ce/unenrolled']
msg_error_unenroll: ['ce/rejected']
msg_disallowed: ['cea/disallowed']
msg_notenrolled: ['!ce/notenrolled']
}
for msg_symbol in message_ordering
# task_response.text JSON.stringify(data)
msg_txt = msg_to_txt[msg_symbol]
task_res_section = $ '<div/>', class: 'task-res-section'
task_res_section.append $ '<h3/>', text: msg_txt
email_list = $ '<ul/>'
task_res_section.append email_list
will_attach = false
for code in msg_to_codes[msg_symbol]
log 'logging code', code
emails = response_code_dict[code]
log 'emails', emails
if emails and emails.length
for email in emails
log 'logging email', email
email_list.append $ '<li/>', text: email
will_attach = true
if will_attach
task_response.append task_res_section
else
task_res_section.remove()
# setup the data download section
setup_section_data_download = (section) ->
log "setting up instructor dashboard section - data download"
display = section.find('.data-display')
display_text = display.find('.data-display-text')
display_table = display.find('.data-display-table')
reset_display = ->
display_text.empty()
display_table.empty()
list_studs_btn = section.find("input[name='list-profiles']'")
list_studs_btn.click (e) ->
log "fetching student list"
......@@ -72,43 +170,47 @@ setup_section_data_download = (section) ->
url += '/csv'
location.href = url
else
reset_display()
$.getJSON url, (data) ->
display = section.find('.dumped-data-display')
display.text JSON.stringify(data)
log data
# setup SlickGrid
options =
enableCellNavigation: true
enableColumnReorder: false
options = enableCellNavigation: true, enableColumnReorder: false
# columns = [{id: feature, name: feature} for feature in data.queried_features]
log options
# log columns
# new Slick.Grid(display, data.students, columns, options)
data = [{'label1': 'val1,1', 'label2': 'val2,1'}, {'label1': 'val1,2', 'label2': 'val2,2'}]
columns = [{id: 'label1', name: 'Label One', width: 80, minWidth: 80}, {id: 'label2', name: 'Label Two'}]
columns = ({id: feature, field: feature, name: feature} for feature in data.queried_features)
grid_data = data.students
log 'columns', columns
log 'data', data
grid = new Slick.Grid(display, data, columns, options)
table_placeholder = $ '<div/>', class: 'slickgrid'
display_table.append table_placeholder
grid = new Slick.Grid(table_placeholder, grid_data, columns, options)
grid.autosizeColumns()
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']
reset_display()
display_text.html data['grading_config_summary']
# setup the analytics section
setup_section_analytics = (section) ->
log "setting up instructor dashboard section - analytics"
display = section.find('.distribution-display')
display_text = display.find('.distribution-display-text')
display_graph = display.find('.distribution-display-graph')
display_table = display.find('.distribution-display-table')
reset_display = ->
display_text.empty()
display_graph.empty()
display_table.empty()
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"
......@@ -125,17 +227,55 @@ setup_section_analytics = (section) ->
opt = $(this).children('option:selected')
log "distribution selected: #{opt.data 'feature'}"
feature = opt.data 'feature'
reset_display()
$.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'
display_text.text 'Error fetching data'
else
if feature_res.type is 'EASY_CHOICE'
display.text JSON.stringify(feature_res.data)
# display_text.text JSON.stringify(feature_res.data)
log feature_res.data
# setup SlickGrid
options =
enableCellNavigation: true
enableColumnReorder: false
columns = [
id: feature
field: feature
name: feature
,
id: 'count'
field: 'count'
name: 'Count'
]
grid_data = _.map feature_res.data, (value, key) ->
datapoint = {}
datapoint[feature] = key
datapoint['count'] = value
datapoint
log grid_data
table_placeholder = $ '<div/>', class: 'slickgrid'
display_table.append table_placeholder
grid = new Slick.Grid(table_placeholder, grid_data, columns, options)
grid.autosizeColumns()
else if feature is 'year_of_birth'
graph_placeholder = $ '<div/>', class: 'year-of-birth'
display_graph.append graph_placeholder
graph_data = _.map feature_res.data, (value, key) -> [parseInt(key), value]
log graph_data
$.plot graph_placeholder, [
data: graph_data
]
else
console.warn("don't know how to show #{feature_res.type}")
display.text 'Unavailable Metric'
display_text.text 'Unavailable Metric\n' + JSON.stringify(feature_res)
......@@ -8,8 +8,14 @@
width: 100%;
position: relative;
.slick-header-column {
height: 100%;
}
h1 {
@extend .top-header;
border-bottom: 0;
padding-bottom: 0;
}
.instructor_dash_glob_info {
......@@ -20,9 +26,16 @@
}
.instructor-nav {
a {
margin-right: 1.2em;
}
.active-section {
color: #551A8B;
}
border-bottom: 1px solid #C8C8C8;
padding-bottom: 1em;
}
section.idash-section {
......@@ -42,7 +55,7 @@
}
.instructor-dashboard-wrapper-2 section.idash-section#course-info {
.instructor-dashboard-wrapper-2 section.idash-section#course_info {
.error-log {
margin-top: 1em;
......@@ -63,15 +76,77 @@
}
.instructor-dashboard-wrapper-2 section.idash-section#data-download {
.instructor-dashboard-wrapper-2 section.idash-section#enrollment {
div {
margin-top: 2em;
}
textarea {
height: 100px;
width: 500px;
}
.task-res-section {
h3 {
color: #646464;
}
ul {
padding: 0;
margin: 0;
margin-top: 0.5em;
line-height: 1.5em;
list-style-type: none;
li {
}
}
}
}
.instructor-dashboard-wrapper-2 section.idash-section#student_admin {
.h-row {
margin-bottom: 1em;
clear: both;
}
p input select {
float: left;
}
}
.instructor-dashboard-wrapper-2 section.idash-section#data_download {
input {
display: block;
// display: block;
margin-bottom: 1em;
}
.data-display {
.data-display-table {
.slickgrid {
height: 400px;
}
}
}
}
.instructor-dashboard-wrapper-2 section.idash-section#analytics {
.distribution-display {
margin-top: 1em;
margin-top: 1.2em;
.distribution-display-graph {
.year-of-birth {
width: 500px;
height: 200px;
}
}
.distribution-display-table {
.slickgrid {
height: 400px;
}
}
}
}
<%page args="section_data"/>
<h2>Distributions</h2>
<select id="distributions" data-endpoint="${ section_data['profile_distributions_url'] }">
<option> Getting available distributions... </option>
</select>
<div class="distribution-display">
<div class="distribution-display-text"></div>
<div class="distribution-display-graph"></div>
<div class="distribution-display-table"></div>
</div>
<%page args="section_data"/>
<h2>Course Information</h2>
<div class="basic-data">
Course Name:
${ section_data['course_info']['display_name'] }
${ section_data['course_display_name'] }
</div>
<div class="basic-data">
Course ID:
${ section_data['course_info']['course_id'] }
${ section_data['course_id'] }
</div>
<div class="basic-data">
Students Enrolled:
${ section_data['course_info']['enrollment_count'] }
${ section_data['enrollment_count'] }
</div>
<div class="basic-data">
Started:
${ section_data['course_info']['has_started'] }
${ section_data['has_started'] }
</div>
<div class="basic-data">
Ended:
${ section_data['course_info']['has_ended'] }
${ section_data['has_ended'] }
</div>
<div class="basic-data">
Grade Cutoffs:
${ section_data['course_info']['grade_cutoffs'] }
${ section_data['grade_cutoffs'] }
</div>
<div class="basic-data">
Offline Grades Available:
${ section_data['course_info']['offline_grades'] }
${ section_data['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>
%if len(section_data['course_errors']):
<h2>Course Errors:</h2>
%for error in section_data['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
%endif
</div>
<%page args="section_data"/>
<input type="button" name="list-profiles" value="List enrolled students with profile information" data-endpoint="${ section_data['enrolled_students_profiles_url'] }" >
<input type="button" name="list-profiles" value="[CSV]" data-csv="true" data-endpoint="${ section_data['enrolled_students_profiles_url'] }" >
<input type="button" name="list-grades" value="Student grades">
<input type="button" name="list-answer-distributions" value="Answer distributions (x students got y points)">
<input type="button" name="dump-gradeconf" value="Grading Configuration" data-endpoint="${ section_data['grading_config_url'] }">
<div class="data-display">
<div class="data-display-text"></div>
<div class="data-display-table"></div>
</div>
<%page args="section_data"/>
<div>
<h2>Batch Enrollment</h2>
<p>Enter student emails separated by new lines or commas.</p>
<textarea rows="6" cols="70" name="student-emails">Student Emails</textarea>
<input type="button" name="enroll" value="Enroll" data-endpoint="${ section_data['enroll_button_url'] }" >
<input type="button" name="unenroll" value="Unenroll" data-endpoint="${ section_data['unenroll_button_url'] }" >
<div class="task-response"></div>
</div>
......@@ -10,7 +10,12 @@
<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>
<script type="text/javascript" src="${static.url('js/vendor/jquery.event.drag-2.2.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/jquery.event.drop-2.2.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/slick.core.js')}"></script>
<script type="text/javascript" src="${static.url('js/vendor/slick.grid.js')}"></script>
<link rel="stylesheet" href="${static.url('css/vendor/slickgrid/smoothness/jquery-ui-1.8.16.custom.css')}">
<link rel="stylesheet" href="${static.url('css/vendor/slickgrid/slick.grid.css')}">
</%block>
<%include file="/courseware/course_navigation.html" args="active_page='instructor_2'" />
......@@ -24,55 +29,28 @@
<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>
## <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>
<h2 class="instructor-nav">
% for section_data in sections:
<a href="" data-section="${ section_data['section_key'] }">${ section_data['section_display_name'] }</a>
% endfor
</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>
% for section_data in sections:
<section id="${ section_data['section_key'] }" class="idash-section">
<%include file="${ section_data['section_key'] }.html" args="section_data=section_data" />
</section>
% endfor
</section>
</div>
......
<%page args="section_data"/>
<div class="h-row">
<p> Select student </p>
<input type="text" name="student-select" value="Jerry Smort">
</div>
##
<div class="h-row">
<p>grade</p>
<p>85 (B)</p>
</div>
##
<div class="h-row">
<a href="" class="progress-link">progress link</a>
<input type="button" name="unenroll" value="Unenroll">
</div>
##
<div class="h-row">
<select class="problems">
<option>Getting problems...</option>
</select>
<input type="button" name="reset-attempts" value="Reset Student Attempts">
</div>
......@@ -255,6 +255,10 @@ if settings.COURSEWARE_ENABLED:
'instructor.views.instructor_dashboard.instructor_dashboard_2', name="instructor_dashboard_2"),
# api endpoints for instructor
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/enroll_unenroll$',
'instructor.views.api.enroll_unenroll', name="enroll_unenroll"),
url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/access_allow_revoke$',
'instructor.views.api.access_allow_revoke', name="access_allow_revoke"),
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)?$',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment