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): ...@@ -50,7 +50,7 @@ def profile_distribution(course_id, feature):
feature_results['type'] = 'EASY_CHOICE' feature_results['type'] = 'EASY_CHOICE'
elif feature in OPEN_CHOICE_FEATURES: elif feature in OPEN_CHOICE_FEATURES:
profiles = UserProfile.objects.filter(user__courseenrollment__course_id=course_id) 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}, ...] # 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 = 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 ...@@ -4,31 +4,19 @@ Instructor Dashboard API views
Non-html views which the instructor dashboard requests. Non-html views which the instructor dashboard requests.
TODO add tracking TODO add tracking
TODO a lot of these GETs should be PUTs
""" """
import csv
import json import json
import logging
import os
import re
import requests
from django_future.csrf import ensure_csrf_cookie from django_future.csrf import ensure_csrf_cookie
from django.views.decorators.cache import cache_control from django.views.decorators.cache import cache_control
from mitxmako.shortcuts import render_to_response from django.http import HttpResponse
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 courseware.courses import get_course_with_access
from django_comment_client.utils import has_forum_access from django.contrib.auth.models import User, Group
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 instructor.enrollment import split_input_list, enroll_emails, unenroll_emails
from instructor.access import allow_access, revoke_access
import analytics.basic import analytics.basic
import analytics.distributions import analytics.distributions
import analytics.csvs import analytics.csvs
...@@ -36,6 +24,61 @@ import analytics.csvs ...@@ -36,6 +24,61 @@ import analytics.csvs
@ensure_csrf_cookie @ensure_csrf_cookie
@cache_control(no_cache=True, no_store=True, must_revalidate=True) @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): def grading_config(request, course_id):
""" """
Respond with json which contains a html formatted grade summary. Respond with json which contains a html formatted grade summary.
...@@ -63,9 +106,10 @@ def enrolled_students_profiles(request, course_id, csv=False): ...@@ -63,9 +106,10 @@ def enrolled_students_profiles(request, course_id, csv=False):
TODO accept requests for different attribute sets 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 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'] 'level_of_education', 'mailing_address', 'goals']
student_data = analytics.basic.enrolled_students_profiles(course_id, query_features) student_data = analytics.basic.enrolled_students_profiles(course_id, query_features)
...@@ -75,7 +119,8 @@ def enrolled_students_profiles(request, course_id, csv=False): ...@@ -75,7 +119,8 @@ def enrolled_students_profiles(request, course_id, csv=False):
'course_id': course_id, 'course_id': course_id,
'students': student_data, 'students': student_data,
'students_count': len(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") response = HttpResponse(json.dumps(response_payload), content_type="application/json")
return response return response
...@@ -104,6 +149,7 @@ def profile_distribution(request, course_id): ...@@ -104,6 +149,7 @@ def profile_distribution(request, course_id):
TODO how should query parameter interpretation work? TODO how should query parameter interpretation work?
TODO respond to csv requests as well TODO respond to csv requests as well
""" """
course = get_course_with_access(request.user, course_id, 'staff', depth=None)
try: try:
features = json.loads(request.GET.get('features')) features = json.loads(request.GET.get('features'))
......
...@@ -15,10 +15,11 @@ from django.views.decorators.cache import cache_control ...@@ -15,10 +15,11 @@ from django.views.decorators.cache import cache_control
from mitxmako.shortcuts import render_to_response from mitxmako.shortcuts import render_to_response
from django.core.urlresolvers import reverse from django.core.urlresolvers import reverse
from django.utils.html import escape from django.utils.html import escape
from django.http import Http404
from django.conf import settings from django.conf import settings
from courseware.access import has_access, get_access_group_name, course_beta_test_group_name 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 django_comment_client.utils import has_forum_access
from instructor.offline_gradecalc import student_grades, offline_grades_available 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 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 ...@@ -31,17 +32,21 @@ from student.models import CourseEnrollment
def instructor_dashboard_2(request, course_id): def instructor_dashboard_2(request, course_id):
"""Display the instructor dashboard for a course.""" """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 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) forum_admin_access = has_forum_access(request.user, course_id, FORUM_ROLE_ADMINISTRATOR)
section_data = { if not staff_access:
'course_info': _section_course_info(request, course_id), raise Http404
'enrollment': _section_enrollment(course_id),
'student_admin': _section_student_admin(course_id), sections = [
'data_download': _section_data_download(course_id), _section_course_info(course_id),
'analytics': _section_analytics(course_id), _section_enrollment(course_id),
} _section_student_admin(course_id),
_section_data_download(course_id),
_section_analytics(course_id),
]
context = { context = {
'course': course, 'course': course,
...@@ -52,19 +57,34 @@ def instructor_dashboard_2(request, course_id): ...@@ -52,19 +57,34 @@ def instructor_dashboard_2(request, course_id):
'djangopid': os.getpid(), 'djangopid': os.getpid(),
'mitx_version': getattr(settings, 'MITX_VERSION_STRING', ''), 'mitx_version': getattr(settings, 'MITX_VERSION_STRING', ''),
'cohorts_ajax_url': reverse('cohorts', kwargs={'course_id': course_id}), '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) 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 """ """ 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_data['section_key'] = 'course_info'
section_data['section_display_name'] = 'Course Info'
section_data['course_id'] = course_id 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['enrollment_count'] = CourseEnrollment.objects.filter(course_id=course_id).count()
section_data['has_started'] = course.has_started() section_data['has_started'] = course.has_started()
section_data['has_ended'] = course.has_ended() section_data['has_ended'] = course.has_ended()
...@@ -81,21 +101,29 @@ def _section_course_info(request, course_id): ...@@ -81,21 +101,29 @@ def _section_course_info(request, course_id):
def _section_enrollment(course_id): def _section_enrollment(course_id):
""" Provide data for the corresponding dashboard section """ """ Provide data for the corresponding dashboard section """
section_data = {} section_data = {
section_data['placeholder'] = "Enrollment content." '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 return section_data
def _section_student_admin(course_id): def _section_student_admin(course_id):
""" Provide data for the corresponding dashboard section """ """ Provide data for the corresponding dashboard section """
section_data = {} section_data = {
section_data['placeholder'] = "Student Admin content." 'section_key': 'student_admin',
'section_display_name': 'Student Admin',
}
return section_data return section_data
def _section_data_download(course_id): def _section_data_download(course_id):
""" Provide data for the corresponding dashboard section """ """ Provide data for the corresponding dashboard section """
section_data = { section_data = {
'section_key': 'data_download',
'section_display_name': 'Data Download',
'grading_config_url': reverse('grading_config', kwargs={'course_id': course_id}), 'grading_config_url': reverse('grading_config', kwargs={'course_id': course_id}),
'enrolled_students_profiles_url': reverse('enrolled_students_profiles', 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): ...@@ -105,6 +133,8 @@ def _section_data_download(course_id):
def _section_analytics(course_id): def _section_analytics(course_id):
""" Provide data for the corresponding dashboard section """ """ Provide data for the corresponding dashboard section """
section_data = { section_data = {
'section_key': 'analytics',
'section_display_name': 'Analytics',
'profile_distributions_url': reverse('profile_distribution', kwargs={'course_id': course_id}), 'profile_distributions_url': reverse('profile_distribution', kwargs={'course_id': course_id}),
} }
return section_data return section_data
...@@ -5,7 +5,6 @@ log = -> console.log.apply console, arguments ...@@ -5,7 +5,6 @@ log = -> console.log.apply console, arguments
CSS_INSTRUCTOR_CONTENT = 'instructor-dashboard-content-2' CSS_INSTRUCTOR_CONTENT = 'instructor-dashboard-content-2'
CSS_ACTIVE_SECTION = 'active-section' CSS_ACTIVE_SECTION = 'active-section'
CSS_IDASH_SECTION = 'idash-section' CSS_IDASH_SECTION = 'idash-section'
CSS_IDASH_DEFAULT_SECTION = 'idash-default-section'
CSS_INSTRUCTOR_NAV = 'instructor-nav' CSS_INSTRUCTOR_NAV = 'instructor-nav'
HASH_LINK_PREFIX = '#view-' HASH_LINK_PREFIX = '#view-'
...@@ -52,18 +51,117 @@ setup_instructor_dashboard = (idash_content) => ...@@ -52,18 +51,117 @@ setup_instructor_dashboard = (idash_content) =>
link = links.filter "[data-section='#{section_name}']" link = links.filter "[data-section='#{section_name}']"
link.click() link.click()
else else
links.filter(".#{CSS_IDASH_DEFAULT_SECTION}").click() links.eq(0).click()
# call setup handlers for each section # call setup handlers for each section
setup_instructor_dashboard_sections = (idash_content) -> setup_instructor_dashboard_sections = (idash_content) ->
log "setting up instructor dashboard sections" 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_section_analytics idash_content.find(".#{CSS_IDASH_SECTION}#analytics")
# setup the data download section # 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) -> 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 = section.find("input[name='list-profiles']'")
list_studs_btn.click (e) -> list_studs_btn.click (e) ->
log "fetching student list" log "fetching student list"
...@@ -72,43 +170,47 @@ setup_section_data_download = (section) -> ...@@ -72,43 +170,47 @@ setup_section_data_download = (section) ->
url += '/csv' url += '/csv'
location.href = url location.href = url
else else
reset_display()
$.getJSON url, (data) -> $.getJSON url, (data) ->
display = section.find('.dumped-data-display')
display.text JSON.stringify(data)
log data
# setup SlickGrid # setup SlickGrid
options =
enableCellNavigation: true
enableColumnReorder: false
options = enableCellNavigation: true, enableColumnReorder: false columns = ({id: feature, field: feature, name: feature} for feature in data.queried_features)
# columns = [{id: feature, name: feature} for feature in data.queried_features] grid_data = data.students
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'}]
log 'columns', columns table_placeholder = $ '<div/>', class: 'slickgrid'
log 'data', data display_table.append table_placeholder
grid = new Slick.Grid(table_placeholder, grid_data, columns, options)
grid = new Slick.Grid(display, data, columns, options)
grid.autosizeColumns() grid.autosizeColumns()
grade_config_btn = section.find("input[name='dump-gradeconf']'") grade_config_btn = section.find("input[name='dump-gradeconf']'")
grade_config_btn.click (e) -> grade_config_btn.click (e) ->
log "fetching grading config" log "fetching grading config"
url = $(this).data('endpoint') url = $(this).data('endpoint')
$.getJSON url, (data) -> $.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 the analytics section
setup_section_analytics = (section) -> setup_section_analytics = (section) ->
log "setting up instructor dashboard section - analytics" 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') distribution_select = section.find('select#distributions')
# ask for available distributions # ask for available distributions
$.getJSON distribution_select.data('endpoint'), features: JSON.stringify([]), (data) -> $.getJSON distribution_select.data('endpoint'), features: JSON.stringify([]), (data) ->
distribution_select.find('option').eq(0).text "-- Select distribution" distribution_select.find('option').eq(0).text "-- Select distribution"
...@@ -125,17 +227,55 @@ setup_section_analytics = (section) -> ...@@ -125,17 +227,55 @@ setup_section_analytics = (section) ->
opt = $(this).children('option:selected') opt = $(this).children('option:selected')
log "distribution selected: #{opt.data 'feature'}" log "distribution selected: #{opt.data 'feature'}"
feature = opt.data 'feature' feature = opt.data 'feature'
reset_display()
$.getJSON distribution_select.data('endpoint'), features: JSON.stringify([feature]), (data) -> $.getJSON distribution_select.data('endpoint'), features: JSON.stringify([feature]), (data) ->
feature_res = data.feature_results[feature] feature_res = data.feature_results[feature]
# feature response format: {'error': 'optional error string', 'type': 'SOME_TYPE', 'data': [stuff]} # feature response format: {'error': 'optional error string', 'type': 'SOME_TYPE', 'data': [stuff]}
display = section.find('.distribution-display').eq(0)
if feature_res.error if feature_res.error
console.warn(feature_res.error) console.warn(feature_res.error)
display.text 'Error fetching data' display_text.text 'Error fetching data'
else else
if feature_res.type is 'EASY_CHOICE' 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 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 else
console.warn("don't know how to show #{feature_res.type}") 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 @@ ...@@ -8,8 +8,14 @@
width: 100%; width: 100%;
position: relative; position: relative;
.slick-header-column {
height: 100%;
}
h1 { h1 {
@extend .top-header; @extend .top-header;
border-bottom: 0;
padding-bottom: 0;
} }
.instructor_dash_glob_info { .instructor_dash_glob_info {
...@@ -20,9 +26,16 @@ ...@@ -20,9 +26,16 @@
} }
.instructor-nav { .instructor-nav {
a {
margin-right: 1.2em;
}
.active-section { .active-section {
color: #551A8B; color: #551A8B;
} }
border-bottom: 1px solid #C8C8C8;
padding-bottom: 1em;
} }
section.idash-section { section.idash-section {
...@@ -42,7 +55,7 @@ ...@@ -42,7 +55,7 @@
} }
.instructor-dashboard-wrapper-2 section.idash-section#course-info { .instructor-dashboard-wrapper-2 section.idash-section#course_info {
.error-log { .error-log {
margin-top: 1em; margin-top: 1em;
...@@ -63,15 +76,77 @@ ...@@ -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 { input {
display: block; // display: block;
margin-bottom: 1em; margin-bottom: 1em;
} }
.data-display {
.data-display-table {
.slickgrid {
height: 400px;
}
}
}
} }
.instructor-dashboard-wrapper-2 section.idash-section#analytics { .instructor-dashboard-wrapper-2 section.idash-section#analytics {
.distribution-display { .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"> <div class="basic-data">
Course Name: Course Name:
${ section_data['course_info']['display_name'] } ${ section_data['course_display_name'] }
</div> </div>
<div class="basic-data"> <div class="basic-data">
Course ID: Course ID:
${ section_data['course_info']['course_id'] } ${ section_data['course_id'] }
</div> </div>
<div class="basic-data"> <div class="basic-data">
Students Enrolled: Students Enrolled:
${ section_data['course_info']['enrollment_count'] } ${ section_data['enrollment_count'] }
</div> </div>
<div class="basic-data"> <div class="basic-data">
Started: Started:
${ section_data['course_info']['has_started'] } ${ section_data['has_started'] }
</div> </div>
<div class="basic-data"> <div class="basic-data">
Ended: Ended:
${ section_data['course_info']['has_ended'] } ${ section_data['has_ended'] }
</div> </div>
<div class="basic-data"> <div class="basic-data">
Grade Cutoffs: Grade Cutoffs:
${ section_data['course_info']['grade_cutoffs'] } ${ section_data['grade_cutoffs'] }
</div> </div>
<div class="basic-data"> <div class="basic-data">
Offline Grades Available: Offline Grades Available:
${ section_data['course_info']['offline_grades'] } ${ section_data['offline_grades'] }
</div> </div>
<div class="error-log"> <div class="error-log">
<h2>Course Errors:</h2> %if len(section_data['course_errors']):
%for error in section_data['course_info']['course_errors']: <h2>Course Errors:</h2>
<div class="course-error"> %for error in section_data['course_errors']:
<code class=course-error-first> ${ error[0] } </code><br> <div class="course-error">
<code class=course-error-second> ${ error[1] } </code> <code class=course-error-first> ${ error[0] } </code><br>
</div> <code class=course-error-second> ${ error[1] } </code>
%endfor </div>
</div> %endfor
%endif
## <div class="basic-data"> </div>
## Section Dump<br>
## ${ section_data['course_info'] }
## </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 @@ ...@@ -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-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/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/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> </%block>
<%include file="/courseware/course_navigation.html" args="active_page='instructor_2'" /> <%include file="/courseware/course_navigation.html" args="active_page='instructor_2'" />
...@@ -24,55 +29,28 @@ ...@@ -24,55 +29,28 @@
<section class="instructor-dashboard-content-2"> <section class="instructor-dashboard-content-2">
<h1>Instructor Dashboard</h1> <h1>Instructor Dashboard</h1>
<div class="instructor_dash_glob_info"> ## <div class="instructor_dash_glob_info">
<span id="djangopid">${djangopid}</span> | ## <span id="djangopid">${djangopid}</span> |
<span id="mitxver">${mitx_version}</span> ## <span id="mitxver">${mitx_version}</span>
</div> ## </div>
## links which are tied to idash-sections below. ## links which are tied to idash-sections below.
## the links are acativated and handled in instructor_dashboard.coffee ## the links are acativated and handled in instructor_dashboard.coffee
## when the javascript loads, it clicks on idash-default-section ## when the javascript loads, it clicks on idash-default-section
<h2 class="instructor-nav">[ <h2 class="instructor-nav">
<a href="" data-section="course-info" class="idash-default-section"> Course Info </a> | % for section_data in sections:
<a href="" data-section="enrollment"> Enrollment </a> | <a href="" data-section="${ section_data['section_key'] }">${ section_data['section_display_name'] }</a>
<a href="" data-section="student-admin"> Student Admin </a> | % endfor
<a href="" data-section="data-download"> Data Download </a> | </h2>
<a href="" data-section="analytics"> Analytics </a>
]</h2>
## each section corresponds to a section_data sub-dictionary provided by the view ## 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 ## to keep this short, sections can be pulled out into their own files
<section id="course-info" class="idash-section"> % for section_data in sections:
<%include file="course_info.html"/> <section id="${ section_data['section_key'] }" class="idash-section">
</section> <%include file="${ section_data['section_key'] }.html" args="section_data=section_data" />
</section>
% endfor
<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> </section>
</div> </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: ...@@ -255,6 +255,10 @@ if settings.COURSEWARE_ENABLED:
'instructor.views.instructor_dashboard.instructor_dashboard_2', name="instructor_dashboard_2"), 'instructor.views.instructor_dashboard.instructor_dashboard_2', name="instructor_dashboard_2"),
# api endpoints for instructor # 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$', url(r'^courses/(?P<course_id>[^/]+/[^/]+/[^/]+)/instructor_dashboard/api/grading_config$',
'instructor.views.api.grading_config', name="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)?$', 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