membership.coffee 17.8 KB
Newer Older
1 2 3 4 5 6 7
###
Membership Section

imports from other modules.
wrap in (-> ... apply) to defer evaluation
such that the value can be defined later than this assignment (file load order).
###
8

9 10
plantTimeout = -> window.InstructorDashboard.util.plantTimeout.apply this, arguments
std_ajax_err = -> window.InstructorDashboard.util.std_ajax_err.apply this, arguments
11
emailStudents = false
12

13

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
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 add button
    @$('input[type="button"].add').click =>
      params.add_handler? @$('.add-field').val()

  # 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 = $ '<tr>'
    for item in row_array
      $td = $ '<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'
72
      labels: ["Username", "Email", "Revoke access"]
73
      add_placeholder: "Enter username or email"
74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
      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()

  # 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
101
      @show_errors gettext "Please enter a username or email."
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118

  # reload the list of members
  reload_list: ->
    # @clear_rows()
    @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()

      # 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
119
        $revoke_btn = $ '<div class="revoke"><i class="icon-remove-sign"></i> Revoke access</div>',
120
          class: 'revoke'
121
        $revoke_btn.click =>
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142
            @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]

  # 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]
143 144 145
      error: std_ajax_err => 
        `// Translators: A rolename appears this sentence.`
        cb? gettext("Error fetching list for role") + " '#{@rolename}'"
146 147 148 149 150

  # 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)
151
  modify_member_access: (unique_student_identifier, action, cb) ->
152 153 154 155
    $.ajax
      dataType: 'json'
      url: @modify_endpoint
      data:
156
        unique_student_identifier: unique_student_identifier
157 158 159
        rolename: @rolename
        action: action
      success: (data) => cb? null, data
160
      error: std_ajax_err => cb? gettext "Error changing user's permissions."
161 162


163 164 165
# Wrapper for the batch enrollment subsection.
# This object handles buttons, success and failure reporting,
# and server communication.
166 167
class BatchEnrollment
  constructor: (@$container) ->
168 169 170 171 172
    # gather elements
    @$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']'")
173
    @$checkbox_emailstudents = @$container.find("input[name='email-students']'")
174 175
    @$task_response          = @$container.find(".request-response")
    @$request_response_error = @$container.find(".request-response-error")
176

177
    # attach click handlers
178

179
    @$btn_enroll.click =>
180 181
      emailStudents = @$checkbox_emailstudents.is(':checked')

Miles Steele committed
182 183
      send_data =
        action: 'enroll'
184 185
        emails: @$emails_input.val()
        auto_enroll: @$checkbox_autoenroll.is(':checked')
186
        email_students: emailStudents
187 188 189

      $.ajax
        dataType: 'json'
190
        url: @$btn_enroll.data 'endpoint'
191
        data: send_data
192 193
        success: (data) => @display_response data
        error: std_ajax_err => @fail_with_error "Error enrolling/unenrolling students."
194

195
    @$btn_unenroll.click =>
196 197
      emailStudents = @$checkbox_emailstudents.is(':checked')

Miles Steele committed
198 199
      send_data =
        action: 'unenroll'
200 201
        emails: @$emails_input.val()
        auto_enroll: @$checkbox_autoenroll.is(':checked')
202
        email_students: emailStudents
203 204 205

      $.ajax
        dataType: 'json'
206
        url: @$btn_unenroll.data 'endpoint'
207
        data: send_data
208
        success: (data) => @display_response data
209
        error: std_ajax_err => @fail_with_error gettext "Error enrolling/unenrolling students."
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234


  fail_with_error: (msg) ->
    console.warn msg
    @$task_response.empty()
    @$request_response_error.empty()
    @$request_response_error.text msg

  display_response: (data_from_server) ->
    @$task_response.empty()
    @$request_response_error.empty()

    # these results arrays contain student_results
    # only populated arrays will be rendered
    #
    # students for which there was an error during the action
    errors = []
    # students who are now enrolled in the course
    enrolled = []
    # students who are now allowed to enroll in the course
    allowed = []
    # students who will be autoenrolled on registration
    autoenrolled = []
    # students who are now not enrolled in the course
    notenrolled = []
235 236
    # students who were not enrolled or allowed prior to unenroll action
    notunenrolled = []
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255

    # categorize student results into the above arrays.
    for student_results in data_from_server.results
      # for a successful action.
      # student_results is of the form {
      #   "email": "jd405@edx.org",
      #   "before": {
      #     "enrollment": true,
      #     "auto_enroll": false,
      #     "user": true,
      #     "allowed": false
      #   }
      #   "after": {
      #     "enrollment": true,
      #     "auto_enroll": false,
      #     "user": true,
      #     "allowed": false
      #   },
      # }
256
      #
257 258 259 260 261 262 263 264
      # for an action error.
      # student_results is of the form {
      #   'email': email,
      #   'error': True,
      # }

      if student_results.error
        errors.push student_results
265

266 267
      else if student_results.after.enrollment
        enrolled.push student_results
268

269 270 271
      else if student_results.after.allowed
        if student_results.after.auto_enroll
          autoenrolled.push student_results
272
        else
273
          allowed.push student_results
274 275

      # The instructor is trying to unenroll someone who is not enrolled or allowed to enroll; non-sensical action.
276
      else if data_from_server.action is 'unenroll' and not (student_results.before.enrollment) and not (student_results.before.allowed)
277 278
        notunenrolled.push student_results

279 280
      else if not student_results.after.enrollment
        notenrolled.push student_results
281

282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306
      else
        console.warn 'student results not reported to user'
        console.warn student_results

    # render populated result arrays
    render_list = (label, emails) =>
      task_res_section = $ '<div/>', class: 'request-res-section'
      task_res_section.append $ '<h3/>', text: label
      email_list = $ '<ul/>'
      task_res_section.append email_list

      for email in emails
        email_list.append $ '<li/>', text: email

      @$task_response.append task_res_section

    if errors.length
      errors_label = do ->
        if data_from_server.action is 'enroll'
          "There was an error enrolling:"
        else if data_from_server.action is 'unenroll'
          "There was an error unenrolling:"
        else
          console.warn "unknown action from server '#{data_from_server.action}'"
          "There was an error processing:"
307

308 309
      for student_results in errors
        render_list errors_label, (sr.email for sr in errors)
310

311 312
    if enrolled.length and emailStudents
      render_list gettext("Successfully enrolled and sent email to the following students:"), (sr.email for sr in enrolled)
313

314
    if enrolled.length and not emailStudents
315
      `// Translators: A list of students appears after this sentence.`
316 317 318 319
      render_list gettext("Successfully enrolled the following students:"), (sr.email for sr in enrolled)

    # Student hasn't registered so we allow them to enroll
    if allowed.length and emailStudents
320
      `// Translators: A list of students appears after this sentence.`
321 322 323 324 325
      render_list gettext("Successfully sent enrollment emails to the following students. They will be allowed to enroll once they register:"),
        (sr.email for sr in allowed)

    # Student hasn't registered so we allow them to enroll
    if allowed.length and not emailStudents
326
      `// Translators: A list of students appears after this sentence.`
327
      render_list gettext("These students will be allowed to enroll once they register:"),
328
        (sr.email for sr in allowed)
329

330 331
    # Student hasn't registered so we allow them to enroll with autoenroll
    if autoenrolled.length and emailStudents
332
      `// Translators: A list of students appears after this sentence.`
333
      render_list gettext("Successfully sent enrollment emails to the following students. They will be enrolled once they register:"),
334
        (sr.email for sr in autoenrolled)
335

336 337
    # Student hasn't registered so we allow them to enroll with autoenroll
    if autoenrolled.length and not emailStudents
338
      `// Translators: A list of students appears after this sentence.`
339 340 341 342
      render_list gettext("These students will be enrolled once they register:"),
        (sr.email for sr in autoenrolled)

    if notenrolled.length and emailStudents
343
      `// Translators: A list of students appears after this sentence.`
344 345 346 347
      render_list gettext("Emails successfully sent. The following students are no longer enrolled in the course:"),
        (sr.email for sr in notenrolled)

    if notenrolled.length and not emailStudents
348
      `// Translators: A list of students appears after this sentence.`
349
      render_list gettext("The following students are no longer enrolled in the course:"),
350
        (sr.email for sr in notenrolled)
351

352
    if notunenrolled.length
353
      `// Translators: A list of students appears after this sentence.`
354 355
      render_list gettext("These students were not affliliated with the course so could not be unenrolled:"),
        (sr.email for sr in notunenrolled)
356

357 358 359 360
# Wrapper for auth list subsection.
# manages a list of users who have special access.
# these could be instructors, staff, beta users, or forum roles.
# uses slickgrid to display list.
Miles Steele committed
361
class AuthList
362
  # rolename is one of ['instructor', 'staff'] for instructor_staff endpoints
Miles Steele committed
363 364
  # rolename is the name of Role for forums for the forum endpoints
  constructor: (@$container, @rolename) ->
365
    # gather elements
366 367 368 369 370
    @$display_table          = @$container.find('.auth-list-table')
    @$request_response_error = @$container.find('.request-response-error')
    @$add_section            = @$container.find('.auth-list-add')
    @$allow_field             = @$add_section.find("input[name='email']")
    @$allow_button            = @$add_section.find("input[name='allow']")
371

372
    # attach click handler
373
    @$allow_button.click =>
374
      @access_change @$allow_field.val(), 'allow', => @reload_auth_list()
375
      @$allow_field.val ''
376 377 378

    @reload_auth_list()

379
  # fetch and display list of users who match criteria
380
  reload_auth_list: ->
381
    # helper function to display server data in the list
382
    load_auth_list = (data) =>
383
      # clear existing data
384
      @$request_response_error.empty()
385 386
      @$display_table.empty()

387
      # setup slickgrid
388 389 390
      options =
        enableCellNavigation: true
        enableColumnReorder: false
Miles Steele committed
391 392
        # autoHeight: true
        forceFitColumns: true
393

394 395 396 397
      # this is a hack to put a button/link in a slick grid cell
      # if you change columns, then you must update
      # WHICH_CELL_IS_REVOKE to have the index
      # of the revoke column (left to right).
Miles Steele committed
398
      WHICH_CELL_IS_REVOKE = 3
399 400 401 402 403 404 405 406 407
      columns = [
        id: 'username'
        field: 'username'
        name: 'Username'
      ,
        id: 'email'
        field: 'email'
        name: 'Email'
      ,
Miles Steele committed
408 409 410 411 412 413 414 415
        id: 'first_name'
        field: 'first_name'
        name: 'First Name'
      ,
      #   id: 'last_name'
      #   field: 'last_name'
      #   name: 'Last Name'
      # ,
416 417 418 419 420 421 422
        id: 'revoke'
        field: 'revoke'
        name: 'Revoke'
        formatter: (row, cell, value, columnDef, dataContext) ->
          "<span class='revoke-link'>Revoke Access</span>"
      ]

Miles Steele committed
423
      table_data = data[@rolename]
424 425 426 427 428

      $table_placeholder = $ '<div/>', class: 'slickgrid'
      @$display_table.append $table_placeholder
      grid = new Slick.Grid($table_placeholder, table_data, columns, options)

429
      # click handler part of the revoke button/link hack.
430 431
      grid.onClick.subscribe (e, args) =>
        item = args.grid.getDataItem(args.row)
Miles Steele committed
432
        if args.cell is WHICH_CELL_IS_REVOKE
433
          @access_change item.email, 'revoke', => @reload_auth_list()
434

435 436
    # fetch data from the endpoint
    # the endpoint comes from data-endpoint of the table
437 438 439 440 441 442 443
    $.ajax
      dataType: 'json'
      url: @$display_table.data 'endpoint'
      data: rolename: @rolename
      success: load_auth_list
      error: std_ajax_err => @$request_response_error.text "Error fetching list for '#{@rolename}'"

444

445 446 447
  # slickgrid's layout collapses when rendered
  # in an invisible div. use this method to reload
  # the AuthList widget
Miles Steele committed
448 449 450 451
  refresh: ->
    @$display_table.empty()
    @reload_auth_list()

452 453
  # update the access of a user.
  # (add or remove them from the list)
454 455
  # action should be one of ['allow', 'revoke']
  access_change: (email, action, cb) ->
456 457 458 459 460
    $.ajax
      dataType: 'json'
      url: @$add_section.data 'endpoint'
      data:
        email: email
461
        rolename: @rolename
462
        action: action
463
      success: (data) -> cb?(data)
464
      error: std_ajax_err => @$request_response_error.text gettext "Error changing user's permissions."
465 466


467
# Membership Section
468
class Membership
469
  # enable subsections.
470
  constructor: (@$section) ->
471 472 473
    # attach self to html
    # so that instructor_dashboard.coffee can find this object
    # to call event handlers like 'onClickTitle'
474 475
    @$section.data 'wrapper', @

476 477
    # isolate # initialize BatchEnrollment subsection
    plantTimeout 0, => new BatchEnrollment @$section.find '.batch-enrollment'
Miles Steele committed
478

479
    # gather elements
480 481 482
    @$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'
483

484 485
    # initialize & store AuthList subsections
    # one for each .auth-list-container in the section.
486
    @auth_lists = _.map (@$auth_list_containers), (auth_list_container) =>
Miles Steele committed
487
      rolename = $(auth_list_container).data 'rolename'
488
      new AuthListWidget $(auth_list_container), rolename, @$auth_list_errors
Miles Steele committed
489

Miles Steele committed
490 491 492 493 494 495 496
    # populate selector
    @$list_selector.empty()
    for auth_list in @auth_lists
      @$list_selector.append $ '<option/>',
        text: auth_list.$container.data 'display-name'
        data:
          auth_list: auth_list
497 498
    if @auth_lists.length is 0
      @$list_selector.hide()
499

Miles Steele committed
500 501
    @$list_selector.change =>
      $opt = @$list_selector.children('option:selected')
502
      return unless $opt.length > 0
Miles Steele committed
503 504 505 506
      for auth_list in @auth_lists
        auth_list.$container.removeClass 'active'
      auth_list = $opt.data('auth_list')
      auth_list.$container.addClass 'active'
507
      auth_list.re_view()
Miles Steele committed
508

509
    # one-time first selection of top list.
Miles Steele committed
510
    @$list_selector.change()
Miles Steele committed
511

512
  # handler for when the section title is clicked.
Miles Steele committed
513
  onClickTitle: ->
Miles Steele committed
514

515

516 517
# export for use
# create parent namespaces if they do not already exist.
518 519 520 521
_.defaults window, InstructorDashboard: {}
_.defaults window.InstructorDashboard, sections: {}
_.defaults window.InstructorDashboard.sections,
  Membership: Membership