diff --git a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee index 3e75358d52..91b31cf221 100644 --- a/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee +++ b/lms/static/coffee/src/instructor_dashboard/instructor_dashboard.coffee @@ -64,7 +64,7 @@ setup_instructor_dashboard = (idash_content) => section.addClass CSS_ACTIVE_SECTION # tracking - # analytics.pageview "instructor_#{section_name}" + analytics.pageview "instructor_section:#{section_name}" # deep linking # write to url diff --git a/lms/static/coffee/src/instructor_dashboard/membership.coffee b/lms/static/coffee/src/instructor_dashboard/membership.coffee index 48e2531932..733480e268 100644 --- a/lms/static/coffee/src/instructor_dashboard/membership.coffee +++ b/lms/static/coffee/src/instructor_dashboard/membership.coffee @@ -7,6 +7,175 @@ plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, argum std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments +class MemberListWidget + # create a MemberListWidget `$container` is a jquery object to embody. + # `params` holds template parameters. `params` should look like the defaults below. + constructor: (@$container, params={}) -> + params = _.defaults params, + title: "Member List" + info: """ + Use this list to manage members. + """ + labels: ["field1", "field2", "field3"] + add_placeholder: "Enter name" + add_btn_label: "Add Member" + add_handler: (input) -> + + template_html = $("#member-list-widget-template").html() + @$container.html Mustache.render template_html, params + + # bind info toggle + @$('.info-badge').click => @toggle_info() + + # bind add button + @$('input[type="button"].add').click => + params.add_handler? @$('.add-field').val() + + show_info: -> + @$('.info').show() + @$('.member-list').hide() + + show_list: -> + @$('.info').hide() + @$('.member-list').show() + + toggle_info: -> + @$('.info').toggle() + @$('.member-list').toggle() + + # clear the input text field + clear_input: -> @$('.add-field').val '' + + # clear all table rows + clear_rows: -> @$('table tbody').empty() + + # takes a table row as an array items are inserted as text, unless detected + # as a jquery objects in which case they are inserted directly. if an + # element is a jquery object + add_row: (row_array) -> + $tbody = @$('table tbody') + $tr = $ '' + for item in row_array + $td = $ '' + if item instanceof jQuery + $td.append item + else + $td.text item + $tr.append $td + $tbody.append $tr + + # local selector + $: (selector) -> + if @debug? + s = @$container.find selector + if s?.length != 1 + console.warn "local selector '#{selector}' found (#{s.length}) results" + s + else + @$container.find selector + + +class AuthListWidget extends MemberListWidget + constructor: ($container, @rolename, @$error_section) -> + super $container, + title: $container.data 'display-name' + info: $container.data 'info-text' + labels: ["username", "email", "revoke access"] + add_placeholder: "Enter email" + add_btn_label: $container.data 'add-button-label' + add_handler: (input) => @add_handler input + + @debug = true + @list_endpoint = $container.data 'list-endpoint' + @modify_endpoint = $container.data 'modify-endpoint' + unless @rolename? + throw "AuthListWidget missing @rolename" + + @reload_list() + + # action to do when is reintroduced into user's view + re_view: -> + @clear_errors() + @clear_input() + @reload_list() + @$('.info').hide() + @$('.member-list').show() + + # handle clicks on the add button + add_handler: (input) -> + if input? and input isnt '' + @modify_member_access input, 'allow', (error) => + # abort on error + return @show_errors error unless error is null + @clear_errors() + @clear_input() + @reload_list() + else + @show_errors "Enter an email." + + # reload the list of members + reload_list: -> + # @clear_rows() + # @show_info() + @get_member_list (error, member_list) => + # abort on error + return @show_errors error unless error is null + + # only show the list of there are members + @clear_rows() + @show_info() + # @show_info() + + # use _.each instead of 'for' so that member + # is bound in the button callback. + _.each member_list, (member) => + # if there are members, show the list + + # create revoke button and insert it into the row + $revoke_btn = $ '
', + class: 'revoke' + click: => + @modify_member_access member.email, 'revoke', (error) => + # abort on error + return @show_errors error unless error is null + @clear_errors() + @reload_list() + @add_row [member.username, member.email, $revoke_btn] + # make sure the list is shown because there are members. + @show_list() + + # clear error display + clear_errors: -> @$error_section?.text '' + + # set error display + show_errors: (msg) -> @$error_section?.text msg + + # send ajax request to list members + # `cb` is called with cb(error, member_list) + get_member_list: (cb) -> + $.ajax + dataType: 'json' + url: @list_endpoint + data: rolename: @rolename + success: (data) => cb? null, data[@rolename] + error: std_ajax_err => cb? "Error fetching list for role '#{@rolename}'" + + # send ajax request to modify access + # (add or remove them from the list) + # `action` can be 'allow' or 'revoke' + # `cb` is called with cb(error, data) + modify_member_access: (email, action, cb) -> + $.ajax + dataType: 'json' + url: @modify_endpoint + data: + email: email + rolename: @rolename + action: action + success: (data) => cb? null, data + error: std_ajax_err => cb? "Error changing user's permissions." + + # Wrapper for the batch enrollment subsection. # This object handles buttons, success and failure reporting, # and server communication. @@ -277,13 +446,15 @@ class Membership plantTimeout 0, => new BatchEnrollment @$section.find '.batch-enrollment' # gather elements - @$list_selector = @$section.find('select#member-lists-selector') + @$list_selector = @$section.find 'select#member-lists-selector' + @$auth_list_containers = @$section.find '.auth-list-container' + @$auth_list_errors = @$section.find '.member-lists-management .request-response-error' # initialize & store AuthList subsections # one for each .auth-list-container in the section. - @auth_lists = _.map (@$section.find '.auth-list-container'), (auth_list_container) -> + @auth_lists = _.map (@$auth_list_containers), (auth_list_container) => rolename = $(auth_list_container).data 'rolename' - new AuthList $(auth_list_container), rolename + new AuthListWidget $(auth_list_container), rolename, @$auth_list_errors # populate selector @$list_selector.empty() @@ -299,17 +470,14 @@ class Membership 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' + auth_list.re_view() # one-time first selection of top list. @$list_selector.change() - # handler for when the section title is clicked. onClickTitle: -> - for auth_list in @auth_lists - auth_list.refresh() # export for use diff --git a/lms/static/images/info-icon-dark.png b/lms/static/images/info-icon-dark.png new file mode 100644 index 0000000000..4f22a5dac9 Binary files /dev/null and b/lms/static/images/info-icon-dark.png differ diff --git a/lms/static/sass/course/instructor/_instructor_2.scss b/lms/static/sass/course/instructor/_instructor_2.scss index 3540ad8e21..58ac22c7b9 100644 --- a/lms/static/sass/course/instructor/_instructor_2.scss +++ b/lms/static/sass/course/instructor/_instructor_2.scss @@ -18,92 +18,91 @@ right: 15px; font-size: 11pt; } +} - section.instructor-dashboard-content-2 { - @extend .content; - // position: relative; - padding: 40px; - width: 100%; +section.instructor-dashboard-content-2 { + @extend .content; + // position: relative; + padding: 40px; + width: 100%; - // .has-event-handler-for-click { - // border: 1px solid blue; - // } + // .has-event-handler-for-click { + // border: 1px solid blue; + // } - .request-response-error { - margin-top: 1em; - margin-bottom: 1em; - color: $error-red; + .request-response-error { + margin-top: 1em; + margin-bottom: 1em; + color: $error-red; + } + + .slickgrid { + margin-left: 1px; + color:#333333; + font-size:11px; + font-family: verdana,arial,sans-serif; + + .slick-header-column { + // height: 100% } - .slickgrid { - margin-left: 1px; - color:#333333; - font-size:11px; - font-family: verdana,arial,sans-serif; - - .slick-header-column { - // height: 100% - } - - .slick-cell { - border: 1px dotted silver; - border-collapse: collapse; - white-space: normal; - word-wrap: break-word; - } - } - - h1 { - @extend .top-header; - padding-bottom: 0; - border-bottom: 0; - } - - input[type="button"] { - @include idashbutton(#eee); - - &.molly-guard { - // @include idashbutton($danger-red); - // @include idashbutton($black); - // border: 2px solid $danger-red; - } - } - - .instructor_dash_glob_info { - position: absolute; - top: 46px; - right: 50px; - text-align: right; - } - - .instructor-nav { - padding-bottom: 1em; - - border-bottom: 1px solid #C8C8C8; - a { - margin-right: 1.2em; - } - - .active-section { - color: #551A8B; - } - } - - section.idash-section { - display: none; - // background-color: #0f0; - - &.active-section { - display: block; - // background-color: #ff0; - } - - .basic-data { - padding: 6px; - } + .slick-cell { + border: 1px dotted silver; + border-collapse: collapse; + white-space: normal; + word-wrap: break-word; + } + } + + h1 { + @extend .top-header; + padding-bottom: 0; + border-bottom: 0; + } + + input[type="button"] { + @include idashbutton(#eee); + + &.molly-guard { + // @include idashbutton($danger-red); + // @include idashbutton($black); + // border: 2px solid $danger-red; + } + } + + .instructor_dash_glob_info { + position: absolute; + top: 46px; + right: 50px; + text-align: right; + } + + .instructor-nav { + padding-bottom: 1em; + + border-bottom: 1px solid #C8C8C8; + a { + margin-right: 1.2em; + } + + .active-section { + color: #551A8B; + } + } + + section.idash-section { + display: none; + // background-color: #0f0; + + &.active-section { + display: block; + // background-color: #ff0; + } + + .basic-data { + padding: 6px; } } - // @extend .table-wrapper; } @@ -250,14 +249,20 @@ display: block; } - .auth-list-table { - .slickgrid { - height: 250px; - } - } + .revoke { + width: 10px; + height: 10px; + background: url('../images/moderator-delete-icon.png') left center no-repeat; + opacity: 0.7; + &:hover { opacity: 0.8; } + &:active { opacity: 0.9; } - .auth-list-add { - margin-top: 0.5em; + // @include idashbutton($danger-red); + // line-height: 0.6em; + // margin-top: 5px; + // padding: 6px 9px; + // font-size: 9pt; + // border-radius: 10px; } } } @@ -329,3 +334,115 @@ } } } + + +.member-list-widget { + $width: 20 * $baseline; + $height: 25 * $baseline; + $header-height: 3 * $baseline; + $bottom-bar-height: 3 * $baseline; + $content-height: $height - $header-height - $bottom-bar-height; + $border-radius: 3px; + + width: $width; + height: $height; + + .header { + @include box-sizing(border-box); + @include border-top-radius($border-radius); + + position: relative; + padding: $baseline; + width: $width; + height: $header-height; + + background-color: #efefef; + border: 1px solid $light-gray; + } + + .title { + font-size: 16pt; + } + + .label { + color: $lighter-base-font-color; + font-size: $body-font-size * 4/5; + } + + .info-badge { + // float: right; + position: absolute; + top: $baseline / 2; + right: $baseline / 2; + width: 17px; + height: 17px; + background: url('../images/info-icon-dark.png') left center no-repeat; + opacity: 0.35; + &:hover { opacity: 0.45; } + &:active { opacity: 0.5; } + } + + .info { + display: none; + + @include box-sizing(border-box); + max-height: $content-height; + padding: $baseline; + + border: 1px solid $light-gray; + border-top: none; + color: $lighter-base-font-color; + line-height: 1.3em; + } + + .member-list { + @include box-sizing(border-box); + overflow: auto; + padding-top: 0; + width: $width; + max-height: $content-height; + + table { + width: 100%; + } + + tr { + border-bottom: 1px solid $light-gray; + } + + td { + table-layout: fixed; + vertical-align: middle; + word-wrap: break-word; + padding-left: 15px; + border-left: 1px solid $light-gray; + border-right: 1px solid $light-gray; + + font-size: 3/4 *$body-font-size; + } + } + + .bottom-bar { + @include box-sizing(border-box); + @include border-bottom-radius($border-radius); + + position: relative; + width: $width; + height: $bottom-bar-height; + padding: $baseline / 2; + + // border-top: none; + margin-top: -1px; + border: 1px solid $light-gray; + background-color: #efefef; + box-shadow: inset #bbb 0px 1px 1px 0px; + } + + // .add-field + + input[type="button"].add { + @include idashbutton($blue); + position: absolute; + right: $baseline; + } +} diff --git a/lms/templates/courseware/instructor_dashboard_2/instructor_dashboard_2.html b/lms/templates/courseware/instructor_dashboard_2/instructor_dashboard_2.html index 116f81b0e5..e670a30b3e 100644 --- a/lms/templates/courseware/instructor_dashboard_2/instructor_dashboard_2.html +++ b/lms/templates/courseware/instructor_dashboard_2/instructor_dashboard_2.html @@ -5,6 +5,7 @@ <%block name="headextra"> <%static:css group='course'/> + diff --git a/lms/templates/courseware/instructor_dashboard_2/membership.html b/lms/templates/courseware/instructor_dashboard_2/membership.html index d50e71f9fb..c226f74215 100644 --- a/lms/templates/courseware/instructor_dashboard_2/membership.html +++ b/lms/templates/courseware/instructor_dashboard_2/membership.html @@ -1,5 +1,31 @@ <%page args="section_data"/> + +

Batch Enrollment

Enter student emails separated by new lines or commas.

@@ -26,66 +52,75 @@ +
+ %if section_data['access']['instructor']: -
-
-
- - -
-
-
+
%if section_data['access']['instructor']: -
-
-
- - -
-
-
+
%endif -
-
-
- - -
-
-
+
%endif %if section_data['access']['instructor']: -
-
-
- - -
-
-
+
%endif %if section_data['access']['instructor'] or section_data['access']['forum_admin']: -
-
-
- - -
-
-
+
-
-
-
- - -
-
-
+
%endif