diff --git a/lms/djangoapps/instructor/access.py b/lms/djangoapps/instructor/access.py index 4e5bbf4ce2..704dfc4178 100644 --- a/lms/djangoapps/instructor/access.py +++ b/lms/djangoapps/instructor/access.py @@ -18,7 +18,16 @@ from django_comment_common.models import (Role, def list_with_level(course, level): - grpname = get_access_group_name(course, level) + """ + List users who have 'level' access. + + level is in ['instructor', 'staff', 'beta'] + """ + if level in ['beta']: + grpname = course_beta_test_group_name(course.location) + else: + grpname = get_access_group_name(course, level) + try: return Group.objects.get(name=grpname).user_set.all() except Group.DoesNotExist: @@ -52,7 +61,7 @@ def _change_access(course, user, level, mode): """ if level in ['beta']: - grpname = course_beta_test_group_name(course) + grpname = course_beta_test_group_name(course.location) else: grpname = get_access_group_name(course, level) group, _ = Group.objects.get_or_create(name=grpname) diff --git a/lms/djangoapps/instructor/views/api.py b/lms/djangoapps/instructor/views/api.py index 2f7904f92f..249291a655 100644 --- a/lms/djangoapps/instructor/views/api.py +++ b/lms/djangoapps/instructor/views/api.py @@ -31,22 +31,33 @@ import analytics.csvs @ensure_csrf_cookie @cache_control(no_cache=True, no_store=True, must_revalidate=True) -def enroll_unenroll(request, course_id): +def students_update_enrollment_email(request, course_id): """ Enroll or unenroll students by email. Requires staff access. + + Query Parameters: + - action in ['enroll', 'unenroll'] + - emails is string containing a list of emails separated by anything split_input_list can handle. + - auto_enroll is a boolean (defaults to false) """ 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', '')) + action = request.GET.get('action', '') + emails = split_input_list(request.GET.get('emails', '')) + auto_enroll = request.GET.get('auto_enroll', '') in ['true', 'Talse', True] - enrolled_result = enroll_emails(course_id, emails_to_enroll) - unenrolled_result = unenroll_emails(course_id, emails_to_unenroll) + if action == 'enroll': + results = enroll_emails(course_id, emails, auto_enroll=auto_enroll) + elif action == 'unenroll': + results = unenroll_emails(course_id, emails) + else: + raise ValueError("unrecognized action '{}'".format(action)) response_payload = { - 'enrolled': enrolled_result, - 'unenrolled': unenrolled_result, + 'action': action, + 'results': results, + 'auto_enroll': auto_enroll, } response = HttpResponse(json.dumps(response_payload), content_type="application/json") return response diff --git a/lms/djangoapps/instructor/views/instructor_dashboard.py b/lms/djangoapps/instructor/views/instructor_dashboard.py index 475d41e8f4..51a985dbb5 100644 --- a/lms/djangoapps/instructor/views/instructor_dashboard.py +++ b/lms/djangoapps/instructor/views/instructor_dashboard.py @@ -40,9 +40,15 @@ def instructor_dashboard_2(request, course_id): if not staff_access: raise Http404 + access = { + 'instructor': instructor_access, + 'staff': staff_access, + 'forum_admin': forum_admin_access, + } + sections = [ _section_course_info(course_id), - _section_membership(course_id), + _section_membership(course_id, access), _section_student_admin(course_id), _section_data_download(course_id), _section_analytics(course_id), @@ -100,13 +106,16 @@ def _section_course_info(course_id): return section_data -def _section_membership(course_id): +def _section_membership(course_id, access): """ Provide data for the corresponding dashboard section """ section_data = { 'section_key': 'membership', 'section_display_name': 'Membership', - 'enroll_button_url': reverse('enroll_unenroll', kwargs={'course_id': course_id}), - 'unenroll_button_url': reverse('enroll_unenroll', kwargs={'course_id': course_id}), + + 'access': access, + + 'enroll_button_url': reverse('students_update_enrollment_email', kwargs={'course_id': course_id}), + 'unenroll_button_url': reverse('students_update_enrollment_email', kwargs={'course_id': course_id}), 'list_course_role_members_url': reverse('list_course_role_members', kwargs={'course_id': course_id}), 'access_allow_revoke_url': reverse('access_allow_revoke', kwargs={'course_id': course_id}), 'list_forum_members_url': reverse('list_forum_members', kwargs={'course_id': course_id}), @@ -121,7 +130,7 @@ def _section_student_admin(course_id): 'section_key': 'student_admin', 'section_display_name': 'Student Admin', 'get_student_progress_url': reverse('get_student_progress_url', kwargs={'course_id': course_id}), - 'unenroll_button_url': reverse('enroll_unenroll', kwargs={'course_id': course_id}), + 'unenroll_button_url': reverse('students_update_enrollment_email', kwargs={'course_id': course_id}), 'reset_student_attempts_url': reverse('reset_student_attempts', kwargs={'course_id': course_id}), } return section_data diff --git a/lms/static/coffee/src/instructor_dashboard/analytics.coffee b/lms/static/coffee/src/instructor_dashboard/analytics.coffee index 0a9b9b0287..4de7a412b1 100644 --- a/lms/static/coffee/src/instructor_dashboard/analytics.coffee +++ b/lms/static/coffee/src/instructor_dashboard/analytics.coffee @@ -23,7 +23,7 @@ class Analytics populate_selector: (cb) -> @get_profile_distributions [], (data) => - @$distribution_select.find('option').eq(0).text "-- Select distribution" + @$distribution_select.find('option').eq(0).text "-- Select Distribution --" for feature in data.available_features opt = $ '', @@ -107,7 +107,8 @@ class Analytics # exports -_.defaults window, InstructorDashboard: {} -_.defaults window.InstructorDashboard, sections: {} -_.defaults window.InstructorDashboard.sections, - Analytics: Analytics +if _? + _.defaults window, InstructorDashboard: {} + _.defaults window.InstructorDashboard, sections: {} + _.defaults window.InstructorDashboard.sections, + Analytics: Analytics diff --git a/lms/static/coffee/src/instructor_dashboard/course_info.coffee b/lms/static/coffee/src/instructor_dashboard/course_info.coffee new file mode 100644 index 0000000000..02dd32908e --- /dev/null +++ b/lms/static/coffee/src/instructor_dashboard/course_info.coffee @@ -0,0 +1,28 @@ +log = -> console.log.apply console, arguments +plantTimeout = (ms, cb) -> setTimeout cb, ms + +class CourseInfo + constructor: (@$section) -> + log "setting up instructor dashboard section - course info" + @$section.data 'wrapper', @ + + @$course_errors_wrapper = @$section.find '.course-errors-wrapper' + + if @$course_errors_wrapper.length + @$course_error_toggle = @$course_errors_wrapper.find('h2').eq(0) + @$course_error_visibility_wrapper = @$course_errors_wrapper.find '.course-errors-visibility-wrapper' + @$course_errors = @$course_errors_wrapper.find('.course-error') + + @$course_error_toggle.text @$course_error_toggle.text() + " (#{@$course_errors.length})" + + @$course_error_toggle.click (e) => + e.preventDefault() + @$course_error_visibility_wrapper.toggle() + + +# exports +if _? + _.defaults window, InstructorDashboard: {} + _.defaults window.InstructorDashboard, sections: {} + _.defaults window.InstructorDashboard.sections, + CourseInfo: CourseInfo diff --git a/lms/static/coffee/src/instructor_dashboard/data_download.coffee b/lms/static/coffee/src/instructor_dashboard/data_download.coffee index a321b00cf4..ace1fa57ac 100644 --- a/lms/static/coffee/src/instructor_dashboard/data_download.coffee +++ b/lms/static/coffee/src/instructor_dashboard/data_download.coffee @@ -48,7 +48,8 @@ class DataDownload # exports -_.defaults window, InstructorDashboard: {} -_.defaults window.InstructorDashboard, sections: {} -_.defaults window.InstructorDashboard.sections, - DataDownload: DataDownload +if _? + _.defaults window, InstructorDashboard: {} + _.defaults window.InstructorDashboard, sections: {} + _.defaults window.InstructorDashboard.sections, + DataDownload: DataDownload diff --git a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee index fb5bd3f4bb..ac3ae8a770 100644 --- a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee +++ b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee @@ -26,6 +26,7 @@ setup_instructor_dashboard = (idash_content) => # setup section header click handlers for link in ($ link for link in links) link.click (e) -> + e.preventDefault() # deactivate (styling) all sections idash_content.find(".#{CSS_IDASH_SECTION}").removeClass CSS_ACTIVE_SECTION idash_content.find(".#{CSS_INSTRUCTOR_NAV}").children().removeClass CSS_ACTIVE_SECTION @@ -34,8 +35,6 @@ setup_instructor_dashboard = (idash_content) => section_name = $(this).data 'section' section = idash_content.find "##{section_name}" - section.data('wrapper')?.onClickTitle?() - # activate (styling) active section.addClass CSS_ACTIVE_SECTION $(this).addClass CSS_ACTIVE_SECTION @@ -43,8 +42,7 @@ setup_instructor_dashboard = (idash_content) => # write deep link location.hash = "#{HASH_LINK_PREFIX}#{section_name}" - log "clicked #{section_name}" - e.preventDefault() + plantTimeout 0, -> section.data('wrapper')?.onClickTitle?() # recover deep link from url # click default or go to section specified by hash @@ -62,6 +60,7 @@ setup_instructor_dashboard_sections = (idash_content) -> log "setting up instructor dashboard sections" # fault isolation # an error thrown in one section will not block other sections from exectuing + plantTimeout 0, -> new window.InstructorDashboard.sections.CourseInfo idash_content.find ".#{CSS_IDASH_SECTION}#course_info" plantTimeout 0, -> new window.InstructorDashboard.sections.DataDownload idash_content.find ".#{CSS_IDASH_SECTION}#data_download" plantTimeout 0, -> new window.InstructorDashboard.sections.Membership idash_content.find ".#{CSS_IDASH_SECTION}#membership" plantTimeout 0, -> new window.InstructorDashboard.sections.StudentAdmin idash_content.find ".#{CSS_IDASH_SECTION}#student_admin" diff --git a/lms/static/coffee/src/instructor_dashboard/membership.coffee b/lms/static/coffee/src/instructor_dashboard/membership.coffee index 9240ee75d7..f85b1adaea 100644 --- a/lms/static/coffee/src/instructor_dashboard/membership.coffee +++ b/lms/static/coffee/src/instructor_dashboard/membership.coffee @@ -9,6 +9,8 @@ class BatchEnrollment $emails_input = @$container.find("textarea[name='student-emails']'") $btn_enroll = @$container.find("input[name='enroll']'") $btn_unenroll = @$container.find("input[name='unenroll']'") + $checkbox_autoenroll = @$container.find("input[name='auto-enroll']'") + window.autoenroll = $checkbox_autoenroll $task_response = @$container.find(".task-response") $emails_input.click -> log 'click $emails_input' @@ -16,20 +18,27 @@ class BatchEnrollment $btn_unenroll.click -> log 'click $btn_unenroll' $btn_enroll.click -> - $.getJSON $btn_enroll.data('endpoint'), enroll: $emails_input.val() , (data) -> + send_data = + action: 'enroll' + emails: $emails_input.val() + auto_enroll: $checkbox_autoenroll.is(':checked') + $.getJSON $btn_enroll.data('endpoint'), send_data, (data) -> log 'received response for enroll button', data display_response(data) $btn_unenroll.click -> - log 'VAL', $emails_input.val() - $.getJSON $btn_unenroll.data('endpoint'), unenroll: $emails_input.val() , (data) -> - # log 'received response for unenroll button', data - # display_response(data) + send_data = + action: 'unenroll' + emails: $emails_input.val() + auto_enroll: $checkbox_autoenroll.is(':checked') + $.getJSON $btn_unenroll.data('endpoint'), send_data, (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 = _.extend {}, data_from_server.results # response_code_dict e.g. {'code': ['email1', 'email2'], ...} message_ordering = [ 'msg_error_enroll' @@ -77,13 +86,10 @@ class BatchEnrollment 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 $ '
', text: email will_attach = true @@ -114,7 +120,6 @@ class AuthList reload_auth_list: => list_endpoint = @$display_table.data 'endpoint' $.getJSON list_endpoint, {rolename: @rolename}, (data) => - log data @$display_table.empty() @@ -139,11 +144,9 @@ class AuthList ] table_data = data[@rolename] - log 'table_data', table_data $table_placeholder = $ '', class: 'slickgrid' @$display_table.append $table_placeholder - log '@$display_table', $table_placeholder grid = new Slick.Grid($table_placeholder, table_data, columns, options) grid.autosizeColumns() @@ -152,11 +155,16 @@ class AuthList if args.cell is 2 @access_change(item.email, @rolename, 'revoke', @reload_auth_list) + # slickgrid collapses when rendered in an invisible div + # use this method to reload the widget + refresh: -> + @$display_table.empty() + @reload_auth_list() + access_change: (email, rolename, mode, cb) -> access_change_endpoint = @$add_section.data 'endpoint' $.getJSON access_change_endpoint, {email: email, rolename: @rolename, mode: mode}, (data) -> - log data - cb?() + cb?(data) class Membership @@ -164,36 +172,41 @@ class Membership log "setting up instructor dashboard section - membership" @$section.data 'wrapper', @ - # isolate sections from each other's errors. - plantTimeout 0, => @batchenrollment = new BatchEnrollment @$section.find '.batch-enrollment' - plantTimeout 0, => @stafflist = new AuthList (@$section.find '.auth-list-container.auth-list-staff'), 'staff' - plantTimeout 0, => @instructorlist = new AuthList (@$section.find '.auth-list-container.auth-list-instructor'), 'instructor' + @$list_selector = @$section.find('select#member-lists-selector') + + plantTimeout 0, => @batchenrollment = new BatchEnrollment @$section.find '.batch-enrollment' + + @auth_lists = _.map (@$section.find '.auth-list-container'), (auth_list_container) -> + rolename = $(auth_list_container).data 'rolename' + new AuthList $(auth_list_container), rolename + + # populate selector + @$list_selector.empty() + for auth_list in @auth_lists + @$list_selector.append $ '', + text: auth_list.$container.data 'display-name' + data: + auth_list: auth_list + + @$list_selector.change => + $opt = @$list_selector.children('option:selected') + for auth_list in @auth_lists + auth_list.$container.removeClass 'active' + auth_list = $opt.data('auth_list') + auth_list.refresh() + auth_list.$container.addClass 'active' + + @$list_selector.change() - # TODO names like 'Administrator' should come from server through template. - plantTimeout 0, => @forum_admin_list = new AuthList (@$section.find '.auth-list-container.auth-list-forum-admin'), 'Administrator' - plantTimeout 0, => @forum_mod_list = new AuthList (@$section.find '.auth-list-container.auth-list-forum-moderator'), 'Moderator' - plantTimeout 0, => @forum_comta_list = new AuthList (@$section.find '.auth-list-container.auth-list-forum-community-ta'), 'Community TA' onClickTitle: -> - @stafflist.$display_table.empty() - @stafflist.reload_auth_list() - - @instructorlist.$display_table.empty() - @instructorlist.reload_auth_list() - - @forum_admin_list.$display_table.empty() - @forum_admin_list.reload_auth_list() - - @forum_mod_list.$display_table.empty() - @forum_mod_list.reload_auth_list() - - @forum_comta_list.$display_table.empty() - @forum_comta_list.reload_auth_list() - + for auth_list in @auth_lists + auth_list.refresh() # exports -_.defaults window, InstructorDashboard: {} -_.defaults window.InstructorDashboard, sections: {} -_.defaults window.InstructorDashboard.sections, - Membership: Membership +if _? + _.defaults window, InstructorDashboard: {} + _.defaults window.InstructorDashboard, sections: {} + _.defaults window.InstructorDashboard.sections, + Membership: Membership diff --git a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee index dbc95ca200..5810a31832 100644 --- a/lms/static/coffee/src/instructor_dashboard/student_admin.coffee +++ b/lms/static/coffee/src/instructor_dashboard/student_admin.coffee @@ -24,8 +24,12 @@ class StudentAdmin console.warn 'error getting student progress url for ' + email @$unenroll_btn.click => - $.getJSON @$unenroll_btn.data('endpoint'), unenroll: @$student_email_field.val(), (data) -> - log 'data' + send_data = + action: 'unenroll' + emails: @$student_email_field.val() + auto_enroll: false + $.getJSON @$unenroll_btn.data('endpoint'), send_data, (data) -> + log data @$reset_attempts_btn.click => email = @$student_email_field.val() @@ -77,7 +81,8 @@ class StudentAdmin # exports -_.defaults window, InstructorDashboard: {} -_.defaults window.InstructorDashboard, sections: {} -_.defaults window.InstructorDashboard.sections, - StudentAdmin: StudentAdmin +if _? + _.defaults window, InstructorDashboard: {} + _.defaults window.InstructorDashboard, sections: {} + _.defaults window.InstructorDashboard.sections, + StudentAdmin: StudentAdmin diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index ba0f11336a..034aa2d2e3 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -63,19 +63,28 @@ .instructor-dashboard-wrapper-2 section.idash-section#course_info { - .error-log { - margin-top: 1em; + .course-errors-wrapper { + margin-top: 2em; - .course-error { - margin-bottom: 1em; + h2 { + color: #D60000; + } - code { - &.course-error-first { - color: #111; - } + .course-errors-visibility-wrapper { + display: none; - &.course-error-second { - color: black; + .course-error { + margin-bottom: 1em; + margin-left: 0.5em; + + code { + &.course-error-first { + color: #111; + } + + &.course-error-second { + color: #111; + } } } } @@ -86,17 +95,26 @@ .instructor-dashboard-wrapper-2 section.idash-section#membership { .vert-left { float: left; - width: 45%; + width: 47%; } .vert-right { float: right; - width: 45%; + width: 47%; + } + + select { + margin-bottom: 1em; } .auth-list-container { + display: none; margin-bottom: 1.5em; + &.active { + display: block; + } + .auth-list-table { .slickgrid { height: 250px; @@ -110,6 +128,9 @@ .batch-enrollment { textarea { + margin-top: 0.2em; + margin-bottom: 1em; + height: 100px; width: 500px; } @@ -146,6 +167,7 @@ input { // display: block; margin-bottom: 1em; + line-height: 1.3em; } .data-display { diff --git a/lms/templates/courseware/instructor_dashboard_2/course_info.html b/lms/templates/courseware/instructor_dashboard_2/course_info.html index 9dcfa9a390..a2053c304a 100644 --- a/lms/templates/courseware/instructor_dashboard_2/course_info.html +++ b/lms/templates/courseware/instructor_dashboard_2/course_info.html @@ -37,14 +37,16 @@ ${ section_data['offline_grades'] } - ${ error[0] } ${ error[1] }
- ${ error[0] } ${ error[1] }
+