integrate slickgrid, add instructor.enrollment, add instructor.access, refactor & clean
This commit is contained in:
@@ -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)
|
||||
|
||||
49
lms/djangoapps/instructor/access.py
Normal file
49
lms/djangoapps/instructor/access.py
Normal file
@@ -0,0 +1,49 @@
|
||||
"""
|
||||
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))
|
||||
138
lms/djangoapps/instructor/enrollment.py
Normal file
138
lms/djangoapps/instructor/enrollment.py
Normal file
@@ -0,0 +1,138 @@
|
||||
"""
|
||||
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,36 +4,79 @@ 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
|
||||
|
||||
|
||||
@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):
|
||||
@@ -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]
|
||||
columns = ({id: feature, field: 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
|
||||
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 {
|
||||
input {
|
||||
display: block;
|
||||
margin-bottom: 1em;
|
||||
.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#analytics {
|
||||
.distribution-display {
|
||||
margin-top: 1em;
|
||||
|
||||
.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;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.data-display {
|
||||
.data-display-table {
|
||||
.slickgrid {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.instructor-dashboard-wrapper-2 section.idash-section#analytics {
|
||||
.distribution-display {
|
||||
margin-top: 1.2em;
|
||||
|
||||
.distribution-display-graph {
|
||||
.year-of-birth {
|
||||
width: 500px;
|
||||
height: 200px;
|
||||
}
|
||||
}
|
||||
|
||||
.distribution-display-table {
|
||||
.slickgrid {
|
||||
height: 400px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<%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>
|
||||
@@ -1,49 +1,50 @@
|
||||
<%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
|
||||
%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>
|
||||
|
||||
## <div class="basic-data">
|
||||
## Section Dump<br>
|
||||
## ${ section_data['course_info'] }
|
||||
## </div>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<%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>
|
||||
@@ -0,0 +1,10 @@
|
||||
<%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>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<%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)?$',
|
||||
|
||||
Reference in New Issue
Block a user