Repository: jupyter/nbgrader (language: python)

Issue:
Ability to show/toggle all student names in formgrader
Follow up on:
https://github.com/jupyter/nbgrader/issues/575 
... which led to:
https://github.com/jupyter/nbgrader/pull/617

I would highly appreciate the possibility to **toggle the visibility of all student names--with one click**. We do grading anonymously and I think it's a good default.

However, when students want to discuss individual issues **after they've received their grades/feedback**, I currently have to toggle up to 200 submission names to find the formgrader version of their submission. 
Thanks for any suggestions! 




Discussion / hints:
This should be possible, though it requires going through a different route:

* Click "Manage Students" on the left navigation bar
* Click on name of the relevant student
* Click on the name of the relevant assignment
* Click on the name of the relevant notebook
* You should now be viewing their submitted notebook in the formgrader

Let me know if that doesn't answer your question!
Oh, that is great! Thank you very much for your answer. 

I still would advocate for a general toggle/option for anonymization. At our university, the student submissions we receive are already anonymized through the "Moodle" system. 

The "double-anonymization" makes things slower in cases where students submit, for example, supplementary .pdf files / figures or when somehow there is an issue with a notebook.

In any case, thanks again for the great work!


Sure, I think it's reasonable to have the anonymization be a config option.
For the record: Jérémie Neveu from Paris Sud mentioned to me that the "toggle visibility of all student names with one click" would be helpful in his workflow.

Fix patch:
diff --git a/nbgrader/server_extensions/formgrader/static/js/gradebook_notebook_submissions.js b/nbgrader/server_extensions/formgrader/static/js/gradebook_notebook_submissions.js
index a8ad1d0c7..9e10c7b17 100644
--- a/nbgrader/server_extensions/formgrader/static/js/gradebook_notebook_submissions.js
+++ b/nbgrader/server_extensions/formgrader/static/js/gradebook_notebook_submissions.js
@@ -37,11 +37,13 @@ var SubmittedNotebookUI = Backbone.View.extend({
     showName: function () {
         this.$reveal.parent().find(".name-shown").show();
         this.$reveal.parent().find(".name-hidden").hide();
+        updateToggleNamesButtonLabel();
     },
 
     hideName: function () {
         this.$reveal.parent().find(".name-hidden").show();
         this.$reveal.parent().find(".name-shown").hide();
+        updateToggleNamesButtonLabel();
     },
 
     render: function () {
@@ -187,6 +189,33 @@ var loadSubmittedNotebooks = function () {
     });
 };
 
+// at least one hidden name -> label="Show All Names"
+// no hidden name -> label="Hide All Names"
+let updateToggleNamesButtonLabel = function() {
+    const button = document.getElementById("toggle_names");
+    const icons = document.getElementsByClassName("glyphicon name-hidden");
+    const icon_array = [...icons];
+    if (icon_array.some(el => $(el).is(':visible'))){
+        button.innerHTML = "Show All Names";
+    } else{
+        button.innerHTML = "Hide All Names";
+    }
+};
+
+// button to toggle all student names
+let toggleAllNames = function () {
+    let icons;
+    const button = document.getElementById("toggle_names");
+    const names_hidden = (button.innerHTML === "Show All Names");
+    if (names_hidden){
+        icons = document.getElementsByClassName("glyphicon name-hidden");
+    } else {
+        icons = document.getElementsByClassName("glyphicon name-shown");
+    }
+    let icon_array = [...icons];
+    icon_array.forEach(x => x.click());
+};
+
 var models = undefined;
 var views = [];
 $(window).on('load', function () {
diff --git a/nbgrader/server_extensions/formgrader/templates/gradebook_notebook_submissions.tpl b/nbgrader/server_extensions/formgrader/templates/gradebook_notebook_submissions.tpl
index caa8b640c..d3a155a72 100644
--- a/nbgrader/server_extensions/formgrader/templates/gradebook_notebook_submissions.tpl
+++ b/nbgrader/server_extensions/formgrader/templates/gradebook_notebook_submissions.tpl
@@ -17,6 +17,11 @@ var notebook_id = "{{ notebook_id }}";
 </ol>
 {%- endblock -%}
 
+<!-- Button to toggle all student names -->
+{%- block messages -%}
+<button id="toggle_names" onclick="toggleAllNames()">Show All Names</button>
+{%- endblock -%}
+
 {%- block table_header -%}
 <tr>
   <th></th>


Test patch:
diff --git a/nbgrader/tests/labextension_ui-tests/tests/test_formgrader.spec.ts b/nbgrader/tests/labextension_ui-tests/tests/test_formgrader.spec.ts
index 489fa0481..8c12951ee 100644
--- a/nbgrader/tests/labextension_ui-tests/tests/test_formgrader.spec.ts
+++ b/nbgrader/tests/labextension_ui-tests/tests/test_formgrader.spec.ts
@@ -510,6 +510,55 @@ test('Gradebook3 show hide names', async ({
 
 });
 
+/*
+  Toggle name visibility button
+ */
+test('Gradebook toggle names button', async ({
+  page,
+  baseURL,
+  tmpPath
+}) => {
+
+  test.skip(is_windows, 'This test does not work on Windows');
+
+  if (baseURL === undefined) throw new Error("BaseURL is undefined.");
+
+  // create environment
+  await create_env(page, tmpPath, exchange_dir, cache_dir, is_windows);
+  await add_courses(page, baseURL, tmpPath);
+  await open_formgrader(page);
+
+  // get formgrader iframe
+  const iframe = page.mainFrame().childFrames()[0];
+
+  // Change iframe URL to gradebook Problem Set 1
+  await iframe.goto(`${baseURL}/formgrader/gradebook/Problem Set 1/Problem 1`);
+  await check_formgrader_breadcrumbs(iframe, ["Manual Grading", "Problem Set 1", "Problem 1"]);
+
+  const button = iframe.locator("[id='toggle_names']").first()
+  const hidden = iframe.locator("td .glyphicon.name-hidden").first();
+  const shown = iframe.locator("td .glyphicon.name-shown").first();
+
+  // At the start, all names are hidden
+  await expect(button).toHaveText("Show All Names", {useInnerText: true});
+  await expect(hidden).toBeVisible();
+  await expect(shown).toBeHidden();
+
+  // Clicking should make names shown
+  await button.click();
+  await expect(button).toHaveText("Hide All Names", {useInnerText: true});
+  await expect(hidden).toBeHidden();
+  await expect(shown).toBeVisible();
+
+  // If there is at least one hidden, button should default to showing all names
+  await shown.click();
+  await expect(button).toHaveText("Show All Names", {useInnerText: true});
+  await button.click();
+  await expect(hidden).toBeHidden();
+  await expect(shown).toBeVisible();
+
+});
+
 /*
  * Load students and test students links
  */
diff --git a/nbgrader/tests/nbextensions/test_formgrader.py b/nbgrader/tests/nbextensions/test_formgrader.py
index 04230f6c0..e31bc7f0e 100644
--- a/nbgrader/tests/nbextensions/test_formgrader.py
+++ b/nbgrader/tests/nbextensions/test_formgrader.py
@@ -278,6 +278,38 @@ def test_gradebook3_show_hide_names(browser, port, gradebook):
     assert hidden.is_displayed()
 
 
+@pytest.mark.nbextensions
+def test_gradebook_toggle_names_button(browser, port, gradebook):
+    problem = gradebook.find_assignment("Problem Set 1").notebooks[0]
+    utils._load_gradebook_page(browser, port, "gradebook/Problem Set 1/{}".format(problem.name))
+    submissions = problem.submissions
+    submissions.sort(key=lambda x: x.id)
+
+    button = browser.find_element(By.ID, "toggle_names")
+    row1 = browser.find_element(By.CSS_SELECTOR, "tbody tr")
+    col1 = row1.find_element(By.CSS_SELECTOR, "td")
+    hidden = col1.find_element(By.CSS_SELECTOR, ".glyphicon.name-hidden")
+    shown = col1.find_element(By.CSS_SELECTOR, ".glyphicon.name-shown")
+
+    # At the start, all names are hidden
+    assert button.text == "Show All Names"
+    assert hidden.is_displayed()
+    assert not shown.is_displayed()
+
+    # Clicking should make names shown
+    button.click()
+    assert button.text == "Hide All Names"
+    assert not hidden.is_displayed()
+    assert shown.is_displayed()
+
+    # If there is at least one hidden, button should default to showing all names
+    shown.click()
+    assert button.text == "Show All Names"
+    button.click()
+    assert not hidden.is_displayed()
+    assert shown.is_displayed()
+
+
 @pytest.mark.nbextensions
 def test_load_student1(browser, port, gradebook):
     # load the student view