<commit_before>// JavaScript event-notification callbacks run asynchronously behind the
// scenes.  Integration tests that verify this page need to wait for the
// callbacks to complete before checking whether the callbacks have completed,
// or else we have a race condition and a flaky test.  This variable is
// incremented by background operations upon completion so that tests can
// block until the relevant callbacks have finished.
//
var gcbAdminOperationCount = 0;

$(function() {

  var AddCoursePanel = function(title, xsrfToken, email, templateCourse) {
    this._xsrfToken = xsrfToken;
    this._documentBody = $(document.body);
    this._lightbox = new window.gcb.Lightbox();
    this._form = $(
        '<div class="add-course-panel">' +
        '  <h2 class="title"></h2>' +
        '  <div class="form-row">' +
        '    <label>Title</label>' +
        '    <input type="text" name="title"' +
        '        placeholder="e.g. New Course">' +
        '  </div>' +
        '  <div class="form-row">' +
        '    <label>URL Component</label>' +
        '    <input type="text" name="name"' +
        '        placeholder="e.g. new_course">' +
        '  </div>' +
        '  <div class="form-row">' +
        '    <label>Admin</label>' +
        '    <input type="text" name="admin_email"' +
        '        placeholder="e.g. <EMAIL>">' +
        '  </div>' +
        '  <div class="controls">' +
        '    <button class="gcb-button save-button">OK</button>' +
        '    <button class="gcb-button cancel-button">Cancel</button>' +
        '  </div>' +
        '  <div class="spinner hidden">' +
        '    <div class="background"></div>' +
        '    <span class="icon spinner md md-settings md-spin"></span>' +
        '  </div>' +
        '</div>');
    this._form.find('.title').text(title);
    this._nameInput = this._form.find('[name="name"]');
    this._titleInput = this._form.find('[name="title"]');
    this._adminEmailInput = this._form.find('[name="admin_email"]');
    this._form.find('.save-button').click(this._save.bind(this));
    this._form.find('.cancel-button').click(this._close.bind(this));
    this._spinner = this._form.find('.spinner');

    this._adminEmailInput.val(email);
    this._templateCourse = templateCourse;
  };
  AddCoursePanel.prototype.open = function() {
    this._lightbox
      .bindTo(this._documentBody)
      .setContent(this._form)
      .show();
  };
  AddCoursePanel.prototype._save = function() {
    this._showSpinner();
    var payload = {
      name: this._nameInput.val(),
      title: this._titleInput.val(),
      admin_email: this._adminEmailInput.val(),
      template_course: this._templateCourse
    };
    var request = {
      xsrf_token: this._xsrfToken,
      payload: JSON.stringify(payload)
    };
    $.ajax('/rest/courses/item', {
      method: 'PUT',
      data: {request: JSON.stringify(request)},
      dataType: 'text',
      error: this._saveError.bind(this),
      success: this._saveSuccess.bind(this),
      complete: this._saveComplete.bind(this)
    });
  };
  AddCoursePanel.prototype._showSpinner = function() {
    this._spinner.removeClass('hidden');
  };
  AddCoursePanel.prototype._hideSpinner = function() {
    this._spinner.addClass('hidden');
  };
  AddCoursePanel.prototype._close = function() {
    this._lightbox.close();
  };
  AddCoursePanel.prototype._saveError = function() {
    cbShowMsg('Something went wrong. Please try again.');
  };
  AddCoursePanel.prototype._saveSuccess = function(data) {
    data = window.gcb.parseJsonResponse(data);
    console.log(data);
    if (data.status != 200) {
      var message = data.message || 'Something went wrong. Please try again.';
      cbShowMsg(message);
      return;
    }
    window.location.reload();
  };
  AddCoursePanel.prototype._saveComplete = function() {
    this._hideSpinner();
  };

  function addCourse() {
    var xsrfToken = $('#add_course').data('xsrfToken');
    var email = $('#add_course').data('email');
    new AddCoursePanel('Add Course...', xsrfToken, email, null).open();
  }
  function addSampleCourse() {
    var xsrfToken = $('#add_course').data('xsrfToken');
    var email = $('#add_course').data('email');
    new AddCoursePanel('Add Sample Course...', xsrfToken, email, 'sample')
        .open();
  }

  function setMultiCourseActionAllowed(isAvailable) {
    if (isAvailable) {
      $('#multi_course_actions').removeClass('inactive-container').
          find(".multi-course-actions").removeClass('inactive');
    } else {
      $('#multi_course_actions').addClass('inactive-container').
          find(".multi-course-actions").addClass('inactive');
    }
  }

  function _allColHdrsSel() {
    return 'thead > tr > th';
  }
  function _someColHdrsSel(extra) {
    return _allColHdrsSel() + extra;
  }
  function _colHdrSelById(id) {
    return _someColHdrsSel('#' + id);
  }
  function _fixedHdrTableSel() {
    return '.gcb-list .table-container > table#courses_list.fixed.thead';
  }
  function _coursesListTableSel() {
    return '.gcb-list table#courses_list:not(.fixed)';
  }
  function _coursesListColHdrsSel(extra) {
    return _coursesListTableSel() + ' > ' + _someColHdrsSel(extra);
  }
  function _coursesListColHdrSelById(id) {
    return _coursesListTableSel() + ' > ' + _colHdrSelById(id);
  }
  function _fixedAndScrolled($someTh) {
    // _fixedAndScrolled(), given some <th> in the <thead> of either:
    //   * the non-scrolling 'table.fixed.thead', or
    //   * the actual <table> containing the <tbody> rows to be sorted,
    // returns an object containing these jQuery objects:
    //   $table: the "real" <table> containing the <tbody> rows that scroll
    //     (those to be sorted, course checkboxes checked, etc.).
    //   $th: <th> of interest in the <thead> of $table (indicating the column
    //     to sort by, the "all courses" checkbox, etc.).
    //   $theadTable: the '.fixed.thead' <table> with only column headers.
    //   $theadTh: the <th> in $theadTable corresponding to $th.
    //
    // Since CSS classes on <th> elements actually store the "sorted by"
    // clicked and "hinted at" hover states, both the original and "cloned"
    // <th> elements representing sortable columens need to contain the same
    // state at all times. Similarly, the original #all_courses_select checkbox
    // and its clone should both contain the same checked state.
    //
    // This function is necessary because sortCourseRows() is invoked when
    // the <th> of some sortable column in the courses list table is clicked.
    // However, this <th> may be in the 'table.fixed.thead' clone or it
    // might be in the "real" <table> that contains the <tbody> to be sorted.
    // _fixedAndScrolled() determines which of the two was actually
    // clicked and returns both.
    //
    // hintNextSort() and unhintSort() suffer the same dilemma when they are
    // hovered over, as does the #all_courses_select checkbox when it is
    // clicked.
    var idx = $someTh.index();
    var hdrsSel = _allColHdrsSel();
    var parts = {};

    var $someTable = $someTh.closest('table');

    if ($someTable.hasClass('thead')) {
      // Supplied <th> is inside the '.table-container > table.fixed.thead'.
      parts.$table = $($someTable.siblings('.table-scroller')
          .find('table:not(.fixed)').get()[0]);
      parts.$th = $(parts.$table.find(hdrsSel).get()[idx]);
      parts.$theadTable = $someTable;
      parts.$theadTh = $someTh;
    } else {
      // Supplied <th> is inside the "real" '.table-scroller > table'.
      parts.$table = $someTable;
      parts.$th = $someTh;
      parts.$theadTable = $($someTable.closest('.table-container')
          .find('table.thead').get()[0]);
      parts.$theadTh = $(parts.$theadTable.find(hdrsSel).get()[idx]);
    }
    return parts;
  }
  function _allCourseSelectSel() {
    if (typeof(gcbAllCourseSelectSel) == 'undefined') {
      gcbAllCourseSelectSel = _someColHdrsSel(
          '.gcb-list-select-course #all_courses_select');
    }
    return gcbAllCourseSelectSel;
  }
  function _fixedHdrAllCourseSelectSel() {
    if (typeof(gcbFixedHdrAllCourseSelectSel) == 'undefined') {
      gcbFixedHdrAllCourseSelectSel =_fixedHdrTableSel() +
          ' > ' + _allCourseSelectSel();
    }
    return gcbFixedHdrAllCourseSelectSel;
  }
  function _coursesListAllCourseSelectSel() {
    if (typeof(gcbCoursesListAllCourseSelectSel) == 'undefined') {
      gcbCoursesListAllCourseSelectSel = _coursesListTableSel() +
          ' > ' + _allCourseSelectSel();
    }
    return gcbCoursesListAllCourseSelectSel;
  }
  function _scrolledCourseCheckboxesSel() {
    if (typeof(gcbCoursesListScrolledCheckboxesSel) == 'undefined') {
      gcbCoursesListScrolledCheckboxesSel = _coursesListTableSel() +
          ' > tbody > tr > td.gcb-list-select-course .gcb-course-checkbox';
    }
    return gcbCoursesListScrolledCheckboxesSel;
  }
  function selectAll() {
    // This gets called _after_ checkbox is checked, so 'indeterminate' has
    // been cleared for *one* of the #all_courses_select checkboxes (either
    // the "real" checkbox in '.table-scroller > table' or its "clone" in
    // table.fixed.thead. Rather than try to determine which one was clicked,
    // just clear 'indeterminate' on both.
    $(_coursesListAllCourseSelectSel()).prop('indeterminate', false);
    $(_fixedHdrAllCourseSelectSel()).prop('indeterminate', false);

    // Now examine the rest of the course checkboxes to determine how to
    // update those. If any is unchecked, check all of them; if all are
    // checked, uncheck them all instead.
    var newState = false;

    // NOTE: The .gcb-course-checkbox checkboxes do not suffer the same
    // "cloned" issues as the #all_courses_select checkbox, since the <tbody>
    // containing them was deleted from all of the table.fixed clones.
    $(_scrolledCourseCheckboxesSel()).each(function(_, checkbox){
      newState |= !checkbox.checked;
    });
    $(_scrolledCourseCheckboxesSel()).prop('checked', newState);

    // Now update both the "real" checkbox in '.table-scroller > table' and
    // its "clone" in table.fixed.thead. Only 'checked' is updated.
    // 'indeterminate' is left cleared, since all of the course checkboxes
    // now have an identical state.
    $(_coursesListAllCourseSelectSel()).prop('checked', newState);
    $(_fixedHdrAllCourseSelectSel()).prop('checked', newState);

    setMultiCourseActionAllowed(newState);
    gcbAdminOperationCount++;
  }
  function selectCourse() {
    // Having clicked the selection checkbox for a single course, set the state
    // of the all-courses selection checkbox accordingly.  All-on -> on;
    // all-off -> off; mixed -> indeterminate.
    var anyChecked = false;
    var anyUnchecked = false;

    // (see selectAll() about .gcb-course-checkbox checkboxes not being cloned)
    $(_scrolledCourseCheckboxesSel()).each(function(_, checkbox){
      anyChecked |= checkbox.checked;
      anyUnchecked |= !checkbox.checked;
    });

    // (see selectAll() about #all_courses_select checkbox clones)
    if (anyChecked && anyUnchecked) {
      $(_coursesListAllCourseSelectSel()).prop('indeterminate', true);
      $(_fixedHdrAllCourseSelectSel()).prop('indeterminate', true);
    } else {
      $(_coursesListAllCourseSelectSel())
          .prop('indeterminate', false)
          .prop('checked', anyChecked);
      $(_fixedHdrAllCourseSelectSel())
          .prop('indeterminate', false)
          .prop('checked', anyChecked);
    }
    setMultiCourseActionAllowed(anyChecked);
    gcbAdminOperationCount++;
  }

  // ---------------------------------------------------------------------------
  // Abstract base popup panel that supports modifications to multiple courses.
  //
  // Derived classes should implement the following functions.  These take
  // no parameters other than the implied 'this'.
  //
  // - _getTitle(): Returns a string for titling the popup window.
  // - _getXsrfToken(): Returns a string containing the xsrf token authorizing
  //       changes to course settings.
  // - _getFormFields(): Returns HTML elements that are added to
  //       the popup window.  Typically contains form fields for settings.
  // - _getCurrentValue(): Returns a text string giving the current value
  //       for the setting.  This is used to populate the current-value
  //       column in the popup form.
  // - _validate(): Return true/false.  On false, you should highlight
  //       incorrect fields and/or set a butterbar message to indicate
  //       problems.  If this returns false, the Save button does not
  //       save the current settings.
  // - _getSaveUrl(): Return the URL component (e.g., "/rest/assessment")
  //       without the course slug prefix that will accept an HTTP PUT.
  //       This handler *must* return a payload dict with an item named
  //       "key" and a value giving the slug identifying the course.
  // - _getSavePayload(): Return a dict/object containing key/value pairs
  //       of data that is sent in the PUT request.
  // - _settingSaved(payload): Called back when a PUT for a given course
  //       returns successfully.  Derived classes may wish to use this
  //       opportunity to update the contents of the page to reflect the new
  //       setting.  The payload parameter is the payload component of the
  //       JSON response returned from the REST handler.
  //
  var EditMultiCoursePanel = function() {
    // Constructors must take no parameters and have no side effects, since
    // declaring class inheritance is done by setting the derived class'
    // .prototype member to a constructed (new'd) instance of the base class.
    // We separate the declaration of the inheritance structure into the
    // constructor-function, and object initialization is deferred to init().
    //
    // Derived classes may wish to call init() as part of their constructor,
    // but doing so implicitly declares that class to be a leaf - it will not
    // be usable as a base class.
  };

  EditMultiCoursePanel.prototype.init = function() {
    this._xsrfToken = this._getXsrfToken();
    this._documentBody = $(document.body);
    this._lightbox = new window.gcb.Lightbox();
    this._numSuccessResponses = 0;
    this._numFailureResponses = 0;
    this._courses = $(_scrolledCourseCheckboxesSel() + ':checked').map(
        function(index, item){
          return {
            namespace: $(item).data('course-namespace'),
            title: $(item).data('course-title'),
            slug: $(item).data('course-slug')
          };
        });

    this._form = $(
        '<div class="add-course-panel multi-course-panel" ' +
        '     id="multi-course-edit-panel">' +
        '  <h2 class="title"></h2>' +
        '  <div class="edit-multi-course-list">' +
        '    <table>' +
        '      <thead>' +
        '        <tr id="multi_edit_header_row">' +
        '          <th class="edit-multi-course-coursename">Course Name</th>' +
        '          <th class="edit-multi-course-value">Current Value</th>' +
        '          <th class="edit-multi-course-status">Setting Saved?</th>' +
        '        </tr>' +
        '      </thead>' +
        '      <tbody id="course_list">' +
        '      </tbody>' +
        '    </table>' +
        '  </div>' +
        '  <div class="controls">' +
        '    <button id="multi-course-save" ' +
        '            class="gcb-button save-button">Save</button>' +
        '    <button id="multi-course-close" ' +
        '            class="gcb-button cancel-button">Close</button>' +
        '  </div>' +
        '  <div id="multi-course-spinner" class="spinner hidden">' +
        '    <div class="background"></div>' +
        '    <span class="icon spinner md md-settings md-spin"></span>' +
        '  </div>' +
        '</div>');
    var titleElement = this._form.find('.title');
    titleElement.text(this._getTitle());
    titleElement.after(this._getFormFields());
    var courseList = this._form.find('#course_list');
    var that = this;
    $(this._courses).each(function(index, course) {
      var currentValue = that._getCurrentValue(course.namespace);
      courseList.append(
          $('<tr><td class="edit-multi-course-coursename">' +
            course.title +
            '</td><td id="current_value_' + course.namespace + '">' +
            currentValue +
            '</td><td id="course_status_' + course.namespace + '">' +
            ' - ' +
            '</td></tr>'));
    });
    this._form.find('#multi-course-save').click(this._save.bind(this));
    this._form.find('#multi-course-close').click(this._close.bind(this));
    this._spinner = this._form.find('.spinner');
  };
  EditMultiCoursePanel.prototype.open = function() {
    this._lightbox
      .bindTo(this._documentBody)
      .setContent(this._form)
      .show();
  };
  EditMultiCoursePanel.prototype._validate = function() {
    return true;
  };
  EditMultiCoursePanel.prototype._getCurrentValue = function(courseNamespace) {
    return '';  // Override in concrete classes.
  };
  EditMultiCoursePanel.prototype._save = function() {
    if (!this._validate()) {
      return;
    }
    if (!confirm('You are about to change settings in ' +
        this._courses.length + ' courses.  Continue?')) {
      return;
    }
    this._showSpinner();
    var errorHandler = this._saveError.bind(this);
    var successHandler = this._saveSuccess.bind(this);
    var completeHandler = this._saveComplete.bind(this);
    var xsrfToken = this._xsrfToken;
    var saveUrl = this._getSaveUrl();
    var payload = JSON.stringify(this._getSavePayload());
    this._numSuccessResponses = 0;
    this._numFailureResponses = 0;

    $(this._courses).each(function(index, course){
      var statusField = $('#course_status_' + course.namespace);
      statusField.text(' - ');

      var request = {
        key: course.namespace,
        xsrf_token: xsrfToken,
        payload: payload,
      };
      var url = '/' + course.slug + saveUrl;
      url = url.replace('//', '/');

      $.ajax(url, {
        method: 'PUT',
        data: {request: JSON.stringify(request)},
        dataType: 'text',
        error: errorHandler,
        success: successHandler,
        complete: completeHandler
      });
    });
  };
  EditMultiCoursePanel.prototype._showSpinner = function() {
    this._spinner.removeClass('hidden');
  };
  EditMultiCoursePanel.prototype._hideSpinner = function() {
    this._spinner.addClass('hidden');
  };
  EditMultiCoursePanel.prototype._close = function() {
    this._lightbox.close();
  };
  EditMultiCoursePanel.prototype._saveError = function() {
    cbShowMsg('Something went wrong. Please try again.');
    this._spinner.addClass('hidden')
  };
  EditMultiCoursePanel.prototype._saveSuccess = function(data) {
    var data = window.gcb.parseJsonResponse(data);
    var payload = window.gcb.parseJsonResponse(data.payload);

    var courseNamespace = payload.key;
    var statusField = $('#course_status_' + courseNamespace);
    var message;
    if (data.status != 200) {
      message = data.message || 'Unknown error.';
      this._numFailureResponses++;
    } else {
      this._settingSaved(payload);
      message = 'Saved.';
      this._numSuccessResponses++;
    }
    statusField.text(message);
    statusField.get(0).scrollIntoView();
  };

  EditMultiCoursePanel.prototype._saveComplete = function() {
    var numResponses = this._numSuccessResponses + this._numFailureResponses;
    if (numResponses >= this._courses.length) {
      var message = (
          'Updated settings in ' + this._numSuccessResponses +' course');
      if (this._numSuccessResponses != 1) {
        message += 's';
      }
      if (this._numFailureResponses > 0) {
        message += ' and had ' + this._numFailureResponses + ' error';
        if (this._numFailureResponses != 1) {
          message += 's';
        }
      }
      message += '.';
      $('#multi_edit_header_row').get(0).scrollIntoView();
      cbShowMsgAutoHide(message);
      this._hideSpinner();
    }
  };

  // ---------------------------------------------------------------------------
  // For editing availability on multiple courses.
  //
  // Used for immediate change of course availability, and also as a base class
  // for changing course start/end date + availability settings.
  //
  EditMultiCourseAvailabilityPanel = function () {
    EditMultiCoursePanel.call(this);
  };
  EditMultiCourseAvailabilityPanel.prototype = new EditMultiCoursePanel();
  EditMultiCourseAvailabilityPanel.prototype._getTitle = function() {
    return 'Set Course Availability';
  };
  EditMultiCourseAvailabilityPanel.prototype._getXsrfToken = function() {
    return $('#edit_multi_course_availability').data('xsrfToken');
  };
  EditMultiCourseAvailabilityPanel.prototype._getFormFields = function() {
    var ret = $(
        '<div class="form-row">' +
        '  <label>Availability</label>' +
        '  <select ' +
        '      id="multi-course-select-availability" ' +
        '      name="availability">' +
        '  </select>' +
        '</div>'
        );
    var availabilitySelect = ret.find('#multi-course-select-availability');
    availabilitySelect.append(
        $('<option value="">--- change availability to ---</option>'));
    this._availabilitySelect = availabilitySelect;
    var options = $('#edit_multi_course_availability').data('options');
    $(options).each(function(index, opt) {
      availabilitySelect.append(
          $('<option value=' + opt.value + '>' + opt.title + '</option>'));
    });
    availabilitySelect.change(function(event) {
      availabilitySelect.removeClass('input-invalid');
    });
    return ret
  };
  EditMultiCourseAvailabilityPanel.prototype._validate = function() {
    var isValid = (this._availabilitySelect.val() != '');
    if (isValid) {
      this._availabilitySelect.removeClass('input-invalid');
    } else {
      this._availabilitySelect.addClass('input-invalid');
    }
    return isValid;
  };
  EditMultiCourseAvailabilityPanel.prototype._getCurrentValue = function(
      courseNamespace) {
    debugger;
    return $('#availability_' + courseNamespace).text().trim();
  };
  EditMultiCourseAvailabilityPanel.prototype._getSaveUrl = function() {
    return '/rest/multi_availability';
  };
  EditMultiCourseAvailabilityPanel.prototype._getSavePayload = function() {
    var availability = this._availabilitySelect.val();
    return {
      trigger_action: 'merge',
      course_availability: availability
    };
  };
  EditMultiCourseAvailabilityPanel.prototype._settingSaved = function(payload) {
    var courseNamespace = payload.key;
    var availability = this._availabilitySelect[0].selectedOptions[0].text;
    $('#availability_' + courseNamespace).text(availability);
    $('#current_value_' + courseNamespace).text(availability);
  };<commit_msg>Remove 'debugger' statement<commit_after>// JavaScript event-notification callbacks run asynchronously behind the
// scenes.  Integration tests that verify this page need to wait for the
// callbacks to complete before checking whether the callbacks have completed,
// or else we have a race condition and a flaky test.  This variable is
// incremented by background operations upon completion so that tests can
// block until the relevant callbacks have finished.
//
var gcbAdminOperationCount = 0;

$(function() {

  var AddCoursePanel = function(title, xsrfToken, email, templateCourse) {
    this._xsrfToken = xsrfToken;
    this._documentBody = $(document.body);
    this._lightbox = new window.gcb.Lightbox();
    this._form = $(
        '<div class="add-course-panel">' +
        '  <h2 class="title"></h2>' +
        '  <div class="form-row">' +
        '    <label>Title</label>' +
        '    <input type="text" name="title"' +
        '        placeholder="e.g. New Course">' +
        '  </div>' +
        '  <div class="form-row">' +
        '    <label>URL Component</label>' +
        '    <input type="text" name="name"' +
        '        placeholder="e.g. new_course">' +
        '  </div>' +
        '  <div class="form-row">' +
        '    <label>Admin</label>' +
        '    <input type="text" name="admin_email"' +
        '        placeholder="e.g. <EMAIL>">' +
        '  </div>' +
        '  <div class="controls">' +
        '    <button class="gcb-button save-button">OK</button>' +
        '    <button class="gcb-button cancel-button">Cancel</button>' +
        '  </div>' +
        '  <div class="spinner hidden">' +
        '    <div class="background"></div>' +
        '    <span class="icon spinner md md-settings md-spin"></span>' +
        '  </div>' +
        '</div>');
    this._form.find('.title').text(title);
    this._nameInput = this._form.find('[name="name"]');
    this._titleInput = this._form.find('[name="title"]');
    this._adminEmailInput = this._form.find('[name="admin_email"]');
    this._form.find('.save-button').click(this._save.bind(this));
    this._form.find('.cancel-button').click(this._close.bind(this));
    this._spinner = this._form.find('.spinner');

    this._adminEmailInput.val(email);
    this._templateCourse = templateCourse;
  };
  AddCoursePanel.prototype.open = function() {
    this._lightbox
      .bindTo(this._documentBody)
      .setContent(this._form)
      .show();
  };
  AddCoursePanel.prototype._save = function() {
    this._showSpinner();
    var payload = {
      name: this._nameInput.val(),
      title: this._titleInput.val(),
      admin_email: this._adminEmailInput.val(),
      template_course: this._templateCourse
    };
    var request = {
      xsrf_token: this._xsrfToken,
      payload: JSON.stringify(payload)
    };
    $.ajax('/rest/courses/item', {
      method: 'PUT',
      data: {request: JSON.stringify(request)},
      dataType: 'text',
      error: this._saveError.bind(this),
      success: this._saveSuccess.bind(this),
      complete: this._saveComplete.bind(this)
    });
  };
  AddCoursePanel.prototype._showSpinner = function() {
    this._spinner.removeClass('hidden');
  };
  AddCoursePanel.prototype._hideSpinner = function() {
    this._spinner.addClass('hidden');
  };
  AddCoursePanel.prototype._close = function() {
    this._lightbox.close();
  };
  AddCoursePanel.prototype._saveError = function() {
    cbShowMsg('Something went wrong. Please try again.');
  };
  AddCoursePanel.prototype._saveSuccess = function(data) {
    data = window.gcb.parseJsonResponse(data);
    console.log(data);
    if (data.status != 200) {
      var message = data.message || 'Something went wrong. Please try again.';
      cbShowMsg(message);
      return;
    }
    window.location.reload();
  };
  AddCoursePanel.prototype._saveComplete = function() {
    this._hideSpinner();
  };

  function addCourse() {
    var xsrfToken = $('#add_course').data('xsrfToken');
    var email = $('#add_course').data('email');
    new AddCoursePanel('Add Course...', xsrfToken, email, null).open();
  }
  function addSampleCourse() {
    var xsrfToken = $('#add_course').data('xsrfToken');
    var email = $('#add_course').data('email');
    new AddCoursePanel('Add Sample Course...', xsrfToken, email, 'sample')
        .open();
  }

  function setMultiCourseActionAllowed(isAvailable) {
    if (isAvailable) {
      $('#multi_course_actions').removeClass('inactive-container').
          find(".multi-course-actions").removeClass('inactive');
    } else {
      $('#multi_course_actions').addClass('inactive-container').
          find(".multi-course-actions").addClass('inactive');
    }
  }

  function _allColHdrsSel() {
    return 'thead > tr > th';
  }
  function _someColHdrsSel(extra) {
    return _allColHdrsSel() + extra;
  }
  function _colHdrSelById(id) {
    return _someColHdrsSel('#' + id);
  }
  function _fixedHdrTableSel() {
    return '.gcb-list .table-container > table#courses_list.fixed.thead';
  }
  function _coursesListTableSel() {
    return '.gcb-list table#courses_list:not(.fixed)';
  }
  function _coursesListColHdrsSel(extra) {
    return _coursesListTableSel() + ' > ' + _someColHdrsSel(extra);
  }
  function _coursesListColHdrSelById(id) {
    return _coursesListTableSel() + ' > ' + _colHdrSelById(id);
  }
  function _fixedAndScrolled($someTh) {
    // _fixedAndScrolled(), given some <th> in the <thead> of either:
    //   * the non-scrolling 'table.fixed.thead', or
    //   * the actual <table> containing the <tbody> rows to be sorted,
    // returns an object containing these jQuery objects:
    //   $table: the "real" <table> containing the <tbody> rows that scroll
    //     (those to be sorted, course checkboxes checked, etc.).
    //   $th: <th> of interest in the <thead> of $table (indicating the column
    //     to sort by, the "all courses" checkbox, etc.).
    //   $theadTable: the '.fixed.thead' <table> with only column headers.
    //   $theadTh: the <th> in $theadTable corresponding to $th.
    //
    // Since CSS classes on <th> elements actually store the "sorted by"
    // clicked and "hinted at" hover states, both the original and "cloned"
    // <th> elements representing sortable columens need to contain the same
    // state at all times. Similarly, the original #all_courses_select checkbox
    // and its clone should both contain the same checked state.
    //
    // This function is necessary because sortCourseRows() is invoked when
    // the <th> of some sortable column in the courses list table is clicked.
    // However, this <th> may be in the 'table.fixed.thead' clone or it
    // might be in the "real" <table> that contains the <tbody> to be sorted.
    // _fixedAndScrolled() determines which of the two was actually
    // clicked and returns both.
    //
    // hintNextSort() and unhintSort() suffer the same dilemma when they are
    // hovered over, as does the #all_courses_select checkbox when it is
    // clicked.
    var idx = $someTh.index();
    var hdrsSel = _allColHdrsSel();
    var parts = {};

    var $someTable = $someTh.closest('table');

    if ($someTable.hasClass('thead')) {
      // Supplied <th> is inside the '.table-container > table.fixed.thead'.
      parts.$table = $($someTable.siblings('.table-scroller')
          .find('table:not(.fixed)').get()[0]);
      parts.$th = $(parts.$table.find(hdrsSel).get()[idx]);
      parts.$theadTable = $someTable;
      parts.$theadTh = $someTh;
    } else {
      // Supplied <th> is inside the "real" '.table-scroller > table'.
      parts.$table = $someTable;
      parts.$th = $someTh;
      parts.$theadTable = $($someTable.closest('.table-container')
          .find('table.thead').get()[0]);
      parts.$theadTh = $(parts.$theadTable.find(hdrsSel).get()[idx]);
    }
    return parts;
  }
  function _allCourseSelectSel() {
    if (typeof(gcbAllCourseSelectSel) == 'undefined') {
      gcbAllCourseSelectSel = _someColHdrsSel(
          '.gcb-list-select-course #all_courses_select');
    }
    return gcbAllCourseSelectSel;
  }
  function _fixedHdrAllCourseSelectSel() {
    if (typeof(gcbFixedHdrAllCourseSelectSel) == 'undefined') {
      gcbFixedHdrAllCourseSelectSel =_fixedHdrTableSel() +
          ' > ' + _allCourseSelectSel();
    }
    return gcbFixedHdrAllCourseSelectSel;
  }
  function _coursesListAllCourseSelectSel() {
    if (typeof(gcbCoursesListAllCourseSelectSel) == 'undefined') {
      gcbCoursesListAllCourseSelectSel = _coursesListTableSel() +
          ' > ' + _allCourseSelectSel();
    }
    return gcbCoursesListAllCourseSelectSel;
  }
  function _scrolledCourseCheckboxesSel() {
    if (typeof(gcbCoursesListScrolledCheckboxesSel) == 'undefined') {
      gcbCoursesListScrolledCheckboxesSel = _coursesListTableSel() +
          ' > tbody > tr > td.gcb-list-select-course .gcb-course-checkbox';
    }
    return gcbCoursesListScrolledCheckboxesSel;
  }
  function selectAll() {
    // This gets called _after_ checkbox is checked, so 'indeterminate' has
    // been cleared for *one* of the #all_courses_select checkboxes (either
    // the "real" checkbox in '.table-scroller > table' or its "clone" in
    // table.fixed.thead. Rather than try to determine which one was clicked,
    // just clear 'indeterminate' on both.
    $(_coursesListAllCourseSelectSel()).prop('indeterminate', false);
    $(_fixedHdrAllCourseSelectSel()).prop('indeterminate', false);

    // Now examine the rest of the course checkboxes to determine how to
    // update those. If any is unchecked, check all of them; if all are
    // checked, uncheck them all instead.
    var newState = false;

    // NOTE: The .gcb-course-checkbox checkboxes do not suffer the same
    // "cloned" issues as the #all_courses_select checkbox, since the <tbody>
    // containing them was deleted from all of the table.fixed clones.
    $(_scrolledCourseCheckboxesSel()).each(function(_, checkbox){
      newState |= !checkbox.checked;
    });
    $(_scrolledCourseCheckboxesSel()).prop('checked', newState);

    // Now update both the "real" checkbox in '.table-scroller > table' and
    // its "clone" in table.fixed.thead. Only 'checked' is updated.
    // 'indeterminate' is left cleared, since all of the course checkboxes
    // now have an identical state.
    $(_coursesListAllCourseSelectSel()).prop('checked', newState);
    $(_fixedHdrAllCourseSelectSel()).prop('checked', newState);

    setMultiCourseActionAllowed(newState);
    gcbAdminOperationCount++;
  }
  function selectCourse() {
    // Having clicked the selection checkbox for a single course, set the state
    // of the all-courses selection checkbox accordingly.  All-on -> on;
    // all-off -> off; mixed -> indeterminate.
    var anyChecked = false;
    var anyUnchecked = false;

    // (see selectAll() about .gcb-course-checkbox checkboxes not being cloned)
    $(_scrolledCourseCheckboxesSel()).each(function(_, checkbox){
      anyChecked |= checkbox.checked;
      anyUnchecked |= !checkbox.checked;
    });

    // (see selectAll() about #all_courses_select checkbox clones)
    if (anyChecked && anyUnchecked) {
      $(_coursesListAllCourseSelectSel()).prop('indeterminate', true);
      $(_fixedHdrAllCourseSelectSel()).prop('indeterminate', true);
    } else {
      $(_coursesListAllCourseSelectSel())
          .prop('indeterminate', false)
          .prop('checked', anyChecked);
      $(_fixedHdrAllCourseSelectSel())
          .prop('indeterminate', false)
          .prop('checked', anyChecked);
    }
    setMultiCourseActionAllowed(anyChecked);
    gcbAdminOperationCount++;
  }

  // ---------------------------------------------------------------------------
  // Abstract base popup panel that supports modifications to multiple courses.
  //
  // Derived classes should implement the following functions.  These take
  // no parameters other than the implied 'this'.
  //
  // - _getTitle(): Returns a string for titling the popup window.
  // - _getXsrfToken(): Returns a string containing the xsrf token authorizing
  //       changes to course settings.
  // - _getFormFields(): Returns HTML elements that are added to
  //       the popup window.  Typically contains form fields for settings.
  // - _getCurrentValue(): Returns a text string giving the current value
  //       for the setting.  This is used to populate the current-value
  //       column in the popup form.
  // - _validate(): Return true/false.  On false, you should highlight
  //       incorrect fields and/or set a butterbar message to indicate
  //       problems.  If this returns false, the Save button does not
  //       save the current settings.
  // - _getSaveUrl(): Return the URL component (e.g., "/rest/assessment")
  //       without the course slug prefix that will accept an HTTP PUT.
  //       This handler *must* return a payload dict with an item named
  //       "key" and a value giving the slug identifying the course.
  // - _getSavePayload(): Return a dict/object containing key/value pairs
  //       of data that is sent in the PUT request.
  // - _settingSaved(payload): Called back when a PUT for a given course
  //       returns successfully.  Derived classes may wish to use this
  //       opportunity to update the contents of the page to reflect the new
  //       setting.  The payload parameter is the payload component of the
  //       JSON response returned from the REST handler.
  //
  var EditMultiCoursePanel = function() {
    // Constructors must take no parameters and have no side effects, since
    // declaring class inheritance is done by setting the derived class'
    // .prototype member to a constructed (new'd) instance of the base class.
    // We separate the declaration of the inheritance structure into the
    // constructor-function, and object initialization is deferred to init().
    //
    // Derived classes may wish to call init() as part of their constructor,
    // but doing so implicitly declares that class to be a leaf - it will not
    // be usable as a base class.
  };

  EditMultiCoursePanel.prototype.init = function() {
    this._xsrfToken = this._getXsrfToken();
    this._documentBody = $(document.body);
    this._lightbox = new window.gcb.Lightbox();
    this._numSuccessResponses = 0;
    this._numFailureResponses = 0;
    this._courses = $(_scrolledCourseCheckboxesSel() + ':checked').map(
        function(index, item){
          return {
            namespace: $(item).data('course-namespace'),
            title: $(item).data('course-title'),
            slug: $(item).data('course-slug')
          };
        });

    this._form = $(
        '<div class="add-course-panel multi-course-panel" ' +
        '     id="multi-course-edit-panel">' +
        '  <h2 class="title"></h2>' +
        '  <div class="edit-multi-course-list">' +
        '    <table>' +
        '      <thead>' +
        '        <tr id="multi_edit_header_row">' +
        '          <th class="edit-multi-course-coursename">Course Name</th>' +
        '          <th class="edit-multi-course-value">Current Value</th>' +
        '          <th class="edit-multi-course-status">Setting Saved?</th>' +
        '        </tr>' +
        '      </thead>' +
        '      <tbody id="course_list">' +
        '      </tbody>' +
        '    </table>' +
        '  </div>' +
        '  <div class="controls">' +
        '    <button id="multi-course-save" ' +
        '            class="gcb-button save-button">Save</button>' +
        '    <button id="multi-course-close" ' +
        '            class="gcb-button cancel-button">Close</button>' +
        '  </div>' +
        '  <div id="multi-course-spinner" class="spinner hidden">' +
        '    <div class="background"></div>' +
        '    <span class="icon spinner md md-settings md-spin"></span>' +
        '  </div>' +
        '</div>');
    var titleElement = this._form.find('.title');
    titleElement.text(this._getTitle());
    titleElement.after(this._getFormFields());
    var courseList = this._form.find('#course_list');
    var that = this;
    $(this._courses).each(function(index, course) {
      var currentValue = that._getCurrentValue(course.namespace);
      courseList.append(
          $('<tr><td class="edit-multi-course-coursename">' +
            course.title +
            '</td><td id="current_value_' + course.namespace + '">' +
            currentValue +
            '</td><td id="course_status_' + course.namespace + '">' +
            ' - ' +
            '</td></tr>'));
    });
    this._form.find('#multi-course-save').click(this._save.bind(this));
    this._form.find('#multi-course-close').click(this._close.bind(this));
    this._spinner = this._form.find('.spinner');
  };
  EditMultiCoursePanel.prototype.open = function() {
    this._lightbox
      .bindTo(this._documentBody)
      .setContent(this._form)
      .show();
  };
  EditMultiCoursePanel.prototype._validate = function() {
    return true;
  };
  EditMultiCoursePanel.prototype._getCurrentValue = function(courseNamespace) {
    return '';  // Override in concrete classes.
  };
  EditMultiCoursePanel.prototype._save = function() {
    if (!this._validate()) {
      return;
    }
    if (!confirm('You are about to change settings in ' +
        this._courses.length + ' courses.  Continue?')) {
      return;
    }
    this._showSpinner();
    var errorHandler = this._saveError.bind(this);
    var successHandler = this._saveSuccess.bind(this);
    var completeHandler = this._saveComplete.bind(this);
    var xsrfToken = this._xsrfToken;
    var saveUrl = this._getSaveUrl();
    var payload = JSON.stringify(this._getSavePayload());
    this._numSuccessResponses = 0;
    this._numFailureResponses = 0;

    $(this._courses).each(function(index, course){
      var statusField = $('#course_status_' + course.namespace);
      statusField.text(' - ');

      var request = {
        key: course.namespace,
        xsrf_token: xsrfToken,
        payload: payload,
      };
      var url = '/' + course.slug + saveUrl;
      url = url.replace('//', '/');

      $.ajax(url, {
        method: 'PUT',
        data: {request: JSON.stringify(request)},
        dataType: 'text',
        error: errorHandler,
        success: successHandler,
        complete: completeHandler
      });
    });
  };
  EditMultiCoursePanel.prototype._showSpinner = function() {
    this._spinner.removeClass('hidden');
  };
  EditMultiCoursePanel.prototype._hideSpinner = function() {
    this._spinner.addClass('hidden');
  };
  EditMultiCoursePanel.prototype._close = function() {
    this._lightbox.close();
  };
  EditMultiCoursePanel.prototype._saveError = function() {
    cbShowMsg('Something went wrong. Please try again.');
    this._spinner.addClass('hidden')
  };
  EditMultiCoursePanel.prototype._saveSuccess = function(data) {
    var data = window.gcb.parseJsonResponse(data);
    var payload = window.gcb.parseJsonResponse(data.payload);

    var courseNamespace = payload.key;
    var statusField = $('#course_status_' + courseNamespace);
    var message;
    if (data.status != 200) {
      message = data.message || 'Unknown error.';
      this._numFailureResponses++;
    } else {
      this._settingSaved(payload);
      message = 'Saved.';
      this._numSuccessResponses++;
    }
    statusField.text(message);
    statusField.get(0).scrollIntoView();
  };

  EditMultiCoursePanel.prototype._saveComplete = function() {
    var numResponses = this._numSuccessResponses + this._numFailureResponses;
    if (numResponses >= this._courses.length) {
      var message = (
          'Updated settings in ' + this._numSuccessResponses +' course');
      if (this._numSuccessResponses != 1) {
        message += 's';
      }
      if (this._numFailureResponses > 0) {
        message += ' and had ' + this._numFailureResponses + ' error';
        if (this._numFailureResponses != 1) {
          message += 's';
        }
      }
      message += '.';
      $('#multi_edit_header_row').get(0).scrollIntoView();
      cbShowMsgAutoHide(message);
      this._hideSpinner();
    }
  };

  // ---------------------------------------------------------------------------
  // For editing availability on multiple courses.
  //
  // Used for immediate change of course availability, and also as a base class
  // for changing course start/end date + availability settings.
  //
  EditMultiCourseAvailabilityPanel = function () {
    EditMultiCoursePanel.call(this);
  };
  EditMultiCourseAvailabilityPanel.prototype = new EditMultiCoursePanel();
  EditMultiCourseAvailabilityPanel.prototype._getTitle = function() {
    return 'Set Course Availability';
  };
  EditMultiCourseAvailabilityPanel.prototype._getXsrfToken = function() {
    return $('#edit_multi_course_availability').data('xsrfToken');
  };
  EditMultiCourseAvailabilityPanel.prototype._getFormFields = function() {
    var ret = $(
        '<div class="form-row">' +
        '  <label>Availability</label>' +
        '  <select ' +
        '      id="multi-course-select-availability" ' +
        '      name="availability">' +
        '  </select>' +
        '</div>'
        );
    var availabilitySelect = ret.find('#multi-course-select-availability');
    availabilitySelect.append(
        $('<option value="">--- change availability to ---</option>'));
    this._availabilitySelect = availabilitySelect;
    var options = $('#edit_multi_course_availability').data('options');
    $(options).each(function(index, opt) {
      availabilitySelect.append(
          $('<option value=' + opt.value + '>' + opt.title + '</option>'));
    });
    availabilitySelect.change(function(event) {
      availabilitySelect.removeClass('input-invalid');
    });
    return ret
  };
  EditMultiCourseAvailabilityPanel.prototype._validate = function() {
    var isValid = (this._availabilitySelect.val() != '');
    if (isValid) {
      this._availabilitySelect.removeClass('input-invalid');
    } else {
      this._availabilitySelect.addClass('input-invalid');
    }
    return isValid;
  };
  EditMultiCourseAvailabilityPanel.prototype._getCurrentValue = function(
      courseNamespace) {
    return $('#availability_' + courseNamespace).text().trim();
  };
  EditMultiCourseAvailabilityPanel.prototype._getSaveUrl = function() {
    return '/rest/multi_availability';
  };
  EditMultiCourseAvailabilityPanel.prototype._getSavePayload = function() {
    var availability = this._availabilitySelect.val();
    return {
      trigger_action: 'merge',
      course_availability: availability
    };
  };
  EditMultiCourseAvailabilityPanel.prototype._settingSaved = function(payload) {
    var courseNamespace = payload.key;
    var availability = this._availabilitySelect[0].selectedOptions[0].text;
    $('#availability_' + courseNamespace).text(availability);
    $('#current_value_' + courseNamespace).text(availability);
  };
{'fraction_non_alphanumeric': 0.14315307457479284, 'fraction_numerical': 0.0010466637592673354, 'mean_word_length': 2.820476507830723, 'pattern_counts': {'":': 0, '<': 205, '<?xml version=': 0, '>': 243, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5621999', 'n_tokens_mistral': 13637, 'n_tokens_neox': 13166, 'n_words': 3131}