about summary refs log tree commit diff
path: root/uploader/templates/phenotypes/add-phenotypes-raw-files.html
diff options
context:
space:
mode:
Diffstat (limited to 'uploader/templates/phenotypes/add-phenotypes-raw-files.html')
-rw-r--r--uploader/templates/phenotypes/add-phenotypes-raw-files.html847
1 files changed, 847 insertions, 0 deletions
diff --git a/uploader/templates/phenotypes/add-phenotypes-raw-files.html b/uploader/templates/phenotypes/add-phenotypes-raw-files.html
new file mode 100644
index 0000000..67b56e3
--- /dev/null
+++ b/uploader/templates/phenotypes/add-phenotypes-raw-files.html
@@ -0,0 +1,847 @@
+{%extends "phenotypes/add-phenotypes-base.html"%}
+{%from "flash_messages.html" import flash_all_messages%}
+{%from "macro-table-pagination.html" import table_pagination%}
+{%from "phenotypes/macro-display-pheno-dataset-card.html" import display_pheno_dataset_card%}
+{%from "phenotypes/macro-display-preview-table.html" import display_preview_table%}
+{%from "phenotypes/macro-display-resumable-elements.html" import display_resumable_elements%}
+
+{%block title%}Phenotypes{%endblock%}
+
+{%block pagetitle%}Phenotypes{%endblock%}
+
+{%block lvl4_breadcrumbs%}
+<li {%if activelink=="add-phenotypes"%}
+    class="breadcrumb-item active"
+    {%else%}
+    class="breadcrumb-item"
+    {%endif%}>
+  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
+           species_id=species.SpeciesId,
+           population_id=population.Id,
+           dataset_id=dataset.Id)}}">Add Phenotypes</a>
+</li>
+{%endblock%}
+
+{%block frm_add_phenotypes_documentation%}
+<p>This page will allow you to upload all the separate files that make up your
+  phenotypes. Here, you will have to upload each separate file individually. If
+  you want instead to upload all your files as a single ZIP file,
+  <a href="{{url_for('species.populations.phenotypes.add_phenotypes',
+           species_id=species.SpeciesId,
+           population_id=population.Id,
+           dataset_id=dataset.Id,
+           use_bundle=true)}}"
+     title="">click here</a>.</p>
+{%endblock%}
+
+{%block frm_add_phenotypes_elements%}
+<fieldset id="fldset-file-metadata">
+  <legend>File(s) Metadata</legend>
+  <div class="form-group">
+    <label for="txt-file-separator" class="form-label">File Separator</label>
+    <div class="input-group">
+      <input id="txt-file-separator"
+             name="file-separator"
+             type="text"
+             value="&#9;"
+             class="form-control"
+             maxlength="1" />
+      <span class="input-group-btn">
+        <button id="btn-reset-file-separator" class="btn btn-info">Reset Default</button>
+      </span>
+    </div>
+    <span class="form-text text-muted">
+      Provide the character that separates the fields in your file(s). It should
+      be the same character for all files (if more than one is provided).<br />
+      A tab character will be assumed if you leave this field blank. See
+      <a href="#docs-file-separator"
+         title="Documentation for file-separator characters">
+        documentation for more information</a>.
+    </span>
+  </div>
+
+  <div class="form-group">
+    <label for="txt-file-comment-character" class="form-label">File Comment-Character</label>
+    <div class="input-group">
+      <input id="txt-file-comment-character"
+             name="file-comment-character"
+             type="text"
+             value="#"
+             class="form-control"
+             maxlength="1" />
+      <span class="input-group-btn">
+        <button id="btn-reset-file-comment-character" class="btn btn-info">
+          Reset Default</button>
+      </span>
+    </div>
+    <span class="form-text text-muted">
+      This specifies that lines that begin with the character provided will be
+      considered comment lines and ignored in their entirety. See
+      <a href="#docs-file-comment-character"
+         title="Documentation for comment characters">
+        documentation for more information</a>.
+    </span>
+  </div>
+
+  <div class="form-group">
+    <label for="txt-file-na" class="form-label">File "No-Value" Indicators</label>
+    <div class="input-group">
+      <input id="txt-file-na"
+             name="file-na"
+             type="text"
+             value="- NA N/A"
+             class="form-control" />
+      <span class="input-group-btn">
+        <button id="btn-reset-file-na" class="btn btn-info">Reset Default</button>
+      </span>
+    </div>
+    <span class="form-text text-muted">
+      This specifies strings in your file indicate that there is no value for a
+      particular cell (a cell is where a column and row intersect). Provide a
+      space-separated list of strings if you have more than one way of
+      indicating no values. See
+      <a href="#docs-file-na" title="Documentation for no-value fields">
+        documentation for more information</a>.</span>
+  </div>
+</fieldset>
+
+<fieldset id="fldset-files">
+  <legend>Data File(s)</legend>
+
+  <fieldset id="fldset-descriptions-file">
+    <div class="form-group">
+      <div class="form-check">
+        <input id="chk-phenotype-descriptions-transposed"
+               name="phenotype-descriptions-transposed"
+               type="checkbox"
+               class="form-check-input"
+               style="border: solid #8EABF0" />
+        <label for="chk-phenotype-descriptions-transposed"
+               class="form-check-label">
+          Description file transposed?</label>
+      </div>
+
+      <div class="non-resumable-elements">
+        <label for="finput-phenotype-descriptions" class="form-label">
+          Phenotype Descriptions</label>
+        <input id="finput-phenotype-descriptions"
+               name="phenotype-descriptions"
+               class="form-control"
+               type="file"
+               data-preview-table="tbl-preview-pheno-desc"
+               required="required"  />
+        <span class="form-text text-muted">
+          Provide a file that contains only the phenotype descriptions,
+          <a href="#docs-file-phenotype-description"
+             title="Documentation of the phenotype data file format.">
+            the documentation for the expected format of the file</a>.</span>
+      </div>
+      {{display_resumable_elements(
+      "resumable-phenotype-descriptions",
+      "phenotype descriptions",
+      '<p>Drag and drop the CSV file that contains the descriptions of your
+        phenotypes here.</p>
+
+      <p>The CSV file should be a matrix of
+        <strong>phenotypes × descriptions</strong> i.e. The first column
+        contains the phenotype names/identifiers whereas the first row is a list
+        of metadata fields like, "description", "units", etc.</p>
+
+      <p>If the format is transposed (i.e.
+        <strong>descriptions × phenotypes</strong>) select the checkbox above.
+      </p>
+
+      <p>Please see the
+        <a href="#docs-file-phenotype-description"
+           title="Documentation of the phenotype data file format.">
+          "Phenotypes Descriptions" documentation</a> section below for more
+        information on the expected format of the file provided here.</p>')}}
+      {{display_preview_table(
+      "tbl-preview-pheno-desc", "phenotype descriptions")}}
+    </div>
+  </fieldset>
+
+
+  <fieldset id="fldset-data-file">
+    <div class="form-group">
+      <div class="form-check">
+        <input id="chk-phenotype-data-transposed"
+               name="phenotype-data-transposed"
+               type="checkbox"
+               class="form-check-input"
+               style="border: solid #8EABF0" />
+        <label for="chk-phenotype-data-transposed" class="form-check-label">
+          Data file transposed?</label>
+      </div>
+
+      <div class="non-resumable-elements">
+        <label for="finput-phenotype-data" class="form-label">Phenotype Data</label>
+        <input id="finput-phenotype-data"
+               name="phenotype-data"
+               class="form-control"
+               type="file"
+               data-preview-table="tbl-preview-pheno-data"
+               required="required"  />
+        <span class="form-text text-muted">
+          Provide a file that contains only the phenotype data. See
+          <a href="#docs-file-phenotype-data"
+             title="Documentation of the phenotype data file format.">
+            the documentation for the expected format of the file</a>.</span>
+      </div>
+
+      {{display_resumable_elements(
+      "resumable-phenotype-data",
+      "phenotype data",
+      '<p>Drag and drop a CSV file that contains the phenotypes numerical data
+        here. You can click the "Browse" button (below and to the right) to
+        select the file from your computer.</p>
+
+      <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>,
+        i.e. The first column contains the samples identifiers while the first
+        row is the list of phenotypes identifiers occurring in the phenotypes
+        descriptions file.</p>
+
+      <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>)
+        select the checkbox above.</p>
+      <p>Please see the
+        <a href="#docs-file-phenotype-data"
+           title="Documentation of the phenotype data file format.">
+          "Phenotypes Data" documentation</a> section below for more information
+        on the expected format for the file provided here.</p>')}}
+      {{display_preview_table("tbl-preview-pheno-data", "phenotype data")}}
+    </div>
+  </fieldset>
+
+  
+  {%if population.Family in families_with_se_and_n%}
+  <fieldset id="fldset-se-file">
+    <div class="form-group">
+      <div class="form-check">
+        <input id="chk-phenotype-se-transposed"
+               name="phenotype-se-transposed"
+               type="checkbox"
+               class="form-check-input"
+               style="border: solid #8EABF0" />
+        <label for="chk-phenotype-se-transposed" class="form-check-label">
+          Standard-Errors file transposed?</label>
+      </div>
+      <div class="group non-resumable-elements">
+        <label for="finput-phenotype-se" class="form-label">Phenotype: Standard Errors</label>
+        <input id="finput-phenotype-se"
+               name="phenotype-se"
+               class="form-control"
+               type="file"
+               data-preview-table="tbl-preview-pheno-se"
+               required="required"  />
+        <span class="form-text text-muted">
+          Provide a file that contains only the standard errors for the phenotypes,
+          computed from the data above.</span>
+      </div>
+
+      {{display_resumable_elements(
+      "resumable-phenotype-se",
+      "standard errors",
+      '<p>Drag and drop a CSV file that contains the phenotypes standard-errors
+        data here. You can click the "Browse" button (below and to the right) to
+        select the file from your computer.</p>
+
+      <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>,
+        i.e. The first column contains the samples identifiers while the first
+        row is the list of phenotypes identifiers occurring in the phenotypes
+        descriptions file.</p>
+
+      <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>)
+        select the checkbox above.</p>
+
+      <p>Please see the
+        <a href="#docs-file-phenotype-se"
+           title="Documentation of the phenotype data file format.">
+          "Phenotypes Data" documentation</a> section below for more information
+        on the expected format of the file provided here.</p>')}}
+
+      {{display_preview_table("tbl-preview-pheno-se", "standard errors")}}
+    </div>
+  </fieldset>
+
+
+  <fieldset id="fldset-n-file">
+    <div class="form-group">
+      <div class="form-check">
+        <input id="chk-phenotype-n-transposed"
+               name="phenotype-n-transposed"
+               type="checkbox"
+               class="form-check-input"
+               style="border: solid #8EABF0" />
+        <label for="chk-phenotype-n-transposed" class="form-check-label">
+          Counts file transposed?</label>
+      </div>
+      <div class="non-resumable-elements">
+        <label for="finput-phenotype-n" class="form-label">Phenotype: Number of Samples/Individuals</label>
+        <input id="finput-phenotype-n"
+               name="phenotype-n"
+               class="form-control"
+               type="file"
+               data-preview-table="tbl-preview-pheno-n"
+               required="required"  />
+        <span class="form-text text-muted">
+          Provide a file that contains only the number of samples/individuals used in
+          the computation of the standard errors above.</span>
+      </div>
+
+      {{display_resumable_elements(
+      "resumable-phenotype-n",
+      "number of samples/individuals",
+      '<p>Drag and drop a CSV file that contains the samples\' phenotypes counts
+        data here. You can click the "Browse" button (below and to the right) to
+        select the file from your computer.</p>
+
+      <p>The CSV should be a matrix of <strong>samples × phenotypes</strong>,
+        i.e. The first column contains the samples identifiers while the first
+        row is the list of phenotypes identifiers occurring in the phenotypes
+        descriptions file.</p>
+
+      <p>If the format is transposed (i.e <strong>phenotypes × samples</strong>)
+        select the checkbox above.</p>
+
+      <p>Please see the
+        <a href="#docs-file-phenotype-se"
+           title="Documentation of the phenotype data file format.">
+          "Phenotypes Data" documentation</a> section below for more information
+        on the expected format of the file provided here.</p>')}}
+
+      {{display_preview_table("tbl-preview-pheno-n", "number of samples/individuals")}}
+    </div>
+  </fieldset>
+</fieldset>
+{%endif%}
+{%endblock%}
+
+
+{%block page_documentation%}
+<div class="row">
+  <h2 class="heading" id="docs-help">Help</h2>
+  <h3 class="subheading">Common Features</h3>
+  <p>The following are the common expectations for <strong>ALL</strong> the
+    files provided in the form above:
+    <ul>
+      <li>The file <strong>MUST</strong> be character-separated values (CSV)
+        text file</li>
+      <li>The first row in the file <strong>MUST</strong> be a heading row, and
+        will be composed of the list identifiers for all of
+        samples/individuals/cases involved in your study.</li>
+      <li>The first column of data in the file <strong>MUST</strong> be the
+        identifiers for all of the phenotypes you wish to upload.</li>
+    </ul>
+  </p>
+
+  <p>If you do not specify the separator character, then we will assume a
+    <strong>TAB</strong> character was used as your separator.</p>
+
+  <p>We also assume you might include comments lines in your files. In that
+    case, if you do not specify what character denotes that a line in your files
+    is a comment line, we will assume the <strong>#</strong> character.<br />
+    A comment <strong>MUST ALWAYS</strong> begin at the start of the line marked
+    with the comment character specified.</p>
+
+  <h3 class="subheading" id="docs-file-metadata">File Metadata</h3>
+  <p>We request some details about your files to help us parse and process the
+    files correctly. The details we collect are:</p>
+  <dl>
+    <dt id="docs-file-separator">File separator</dt>
+    <dd>The files you provide should be character-separated value (CSV) files.
+      We need to know what character you used to separate the values in your
+      file. Some common ones are the Tab character, the comma, etc.<br />
+      Providing that information makes it possible for the system to parse and
+      process your files correctly.<br>
+      <strong>NOTE:</strong> All the files you upload MUST use the same
+      separator.</dd>
+
+    <dt id="docs-file-comment-character">Comment character</dt>
+    <dd>We support use of comment lines in your files. We only support one type
+      of comment style, the <em>line comment</em>.<br />
+      This mean the comment begins at the start of the line, and the end of that
+      line indicates the end of that comment. If you have a really long comment,
+      then you need to break it across multiple lines, marking each line a
+      comment line.<br />
+      The "comment character" is the character at the start of the line that
+      indicates that the line is a line comment.</dd>
+
+    <dt id="docs-file-na">No-Value indicator(s)</dt>
+    <dd>Data in the real world is messy, and in some cases, entirely absent. You
+      need to indicate, in your files, that a particular field did not have a
+      value, and once you do that, you then need to let the system know how you
+      mark such fields. Common ways of indicating "empty values" are, leaving
+      the field blank, using a character such as '-', or using strings like
+      "NA", "N/A", "NULL", etc.<br />
+      Providing this information will help with parsing and processing such
+      no-value fields the correct way.</dd>
+  </dl>
+
+  <h3 class="subheading" id="docs-file-phenotype-description">
+    file: Phenotypes Descriptions</h3>
+  <p>The data in this file is a matrix of <em>phenotypes × metadata-fields</em>.
+    Please note we use the term "metadata-fields" above loosely, due to lack of
+    a good word for this.</p>
+  <p>The file <strong>MUST</strong> have columns in this order:
+    <dl>
+      <dt>Phenotype Identifiers</dt>
+      <dd>These are the names/identifiers for your phenotypes. These
+        names/identifiers are the same ones you will have in all the other files you are
+        uploading.</dd>
+
+      <dt>Descriptions</dt>
+      <dd>Each phenotype will need a description. Good description are necessary
+        to inform other people of what the data is about. Good description are
+        hard to construct, so we provide
+        <a href="https://info.genenetwork.org/faq.php#q-22"
+           title="How to write phenotype descriptions">
+          advice on describing your phenotypes.</a></dd>
+
+      <dt>Units</dt>
+      <dd>Each phenotype will need units for the measurements taken. If there are
+        none, then indicate the field is a no-value field.</dd>
+  </dl></p>
+  <p>You can add more columns after those three if you want to, but these 3
+    <strong>MUST</strong> be present.</p>
+  <p>The file would, for example, look like the following:</p>
+  <code>id,description,units,…<br />
+    pheno10001|Central nervous system, behavior, cognition; …|mg|…<br />
+    pheno10002|Aging, metabolism, central nervous system: …|mg|…<br />
+    ⋮<br /></code>
+
+  <p><strong>Note 01</strong>: The first usable row is the heading row.</p>
+  <p><strong>Note 02: </strong>This example demonstrates a subtle issue that
+    could make your CSV file invalid &mdash; the choice of your field separator
+    character.<br >
+    In the example above, we use the pipe character (<code>|</code>) as our
+    field separator. This is because, if we follow the advice on how to write
+    good descriptions, then we cannot use the comma as our separator &ndash; if
+    we did, then our CSV file would be invalid because the system would have no
+    way to tell the difference between the comma as a field separator, and the
+    comma as a way to separate the "general category and ontology terms".</p>
+
+  <h3 class="subheading">file: Phenotype Data, Standard Errors and/or Sample Counts</h3>
+  <span id="docs-file-phenotype-data"></span>
+  <span id="docs-file-phenotype-se"></span>
+  <span id="docs-file-phenotype-n"></span>
+  <p>The data is a matrix of <em>samples(or individuals) × phenotypes</em>, e.g.</p>
+  <code>
+    # num-cases: 2549
+    # num-phenos: 13
+    id,pheno10001,pheno10002,pheno10003,pheno10004,53.099998,…<br />
+    IND001,61.400002,49,62.5,55.099998,…<br />
+    IND002,54.099998,50.099998,53.299999,55.099998,…<br />
+    IND003,483,403,501,403,…<br />
+    IND004,49.799999,45.5,62.900002,NA,…<br />
+    ⋮<br /></code>
+
+  <p>where <code>IND001,IND002,IND003,IND004,…</code> are the
+    samples/individuals/cases in your study, and
+    <code>pheno10001,pheno10002,pheno10004,pheno10004,…</code> are the
+    identifiers for your phenotypes.</p>
+  <p>The lines beginning with the "<em>#</em>" symbol (i.e.
+    <code># num-cases: 2549</code> and <code># num-phenos: 13</code> are comment
+    lines and will be ignored</p>
+  <p>In this example, the comma (,) is used as the file separator.</p>
+</div>
+
+{%endblock%}
+
+{%block sidebarcontents%}
+{{display_pheno_dataset_card(species, population, dataset)}}
+{%endblock%}
+
+
+{%block more_javascript%}
+<script src="{{url_for('base.node_modules',
+             filename='resumablejs/resumable.js')}}"></script>
+<script type="text/javascript" src="/static/js/files.js"></script>
+
+<script type="text/javascript">
+  $("#btn-reset-file-separator").on("click", (event) => {
+      event.preventDefault();
+      $("#txt-file-separator").val("\t");
+      $("#txt-file-separator").trigger("change");
+  });
+  $("#btn-reset-file-comment-character").on("click", (event) => {
+      event.preventDefault();
+      $("#txt-file-comment-character").val("#");
+      $("#txt-file-comment-character").trigger("change");
+  });
+  $("#btn-reset-file-na").on("click", (event) => {
+      event.preventDefault();
+      $("#txt-file-na").val("- NA N/A");
+      $("#txt-file-na").trigger("change");
+  });
+
+  var update_preview = (table, filedata, formdata, numrows) => {
+      table.find("thead tr").remove()
+      table.find(".data-row").remove();
+      var linenum = 0;
+      var tableheader = table.find("thead");
+      var tablebody = table.find("tbody");
+      var numheadings = 0;
+      var navalues = formdata
+          .na_strings
+          .split(" ")
+          .map((v) => {return v.trim();})
+          .filter((v) => {return Boolean(v);});
+      filedata.forEach((line) => {
+          if(line.startsWith(formdata.comment_char) || linenum >= numrows) {
+              return false;
+          }
+          var row = $("<tr></tr>");
+          line.split(formdata.separator)
+              .map((field) => {
+                  var value = field.trim();
+                  if(navalues.includes(value)) {
+                      return "⋘NUL⋙";
+                  }
+                  return value;
+              })
+              .filter((field) => {
+                  return (field !== "" && field != undefined && field != null);
+              })
+              .forEach((field) => {
+                  if(linenum == 0) {
+                      numheadings += 1;
+                      var tablefield = $("<th></th>");
+                      tablefield.text(field);
+                      row.append(tablefield);
+                  } else {
+                      add_class(row, "data-row");
+                      var tablefield = $("<td></td>");
+                      tablefield.text(field);
+                      row.append(tablefield);
+                  }
+              });
+
+          if(linenum == 0) {
+              tableheader.append(row);
+          } else {
+              tablebody.append(row);
+          }
+          linenum += 1;
+      });
+
+      if(table.find("tbody tr.data-row").length > 0) {
+          add_class(table.find(".data-row-template"), "visually-hidden");
+      } else {
+          remove_class(table.find(".data-row-template"), "visually-hidden");
+      }
+  };
+
+  var makePreviewUpdater = (preview_table) => {
+      return (data) => {
+          update_preview(
+              preview_table,
+              data,
+              filesMetadata(),
+              PREVIEW_ROWS);
+      };
+  };
+
+  var preview_tables_to_elements_map = {
+      "#tbl-preview-pheno-desc": "#finput-phenotype-descriptions",
+      "#tbl-preview-pheno-data": "#finput-phenotype-data",
+      "#tbl-preview-pheno-se": "#finput-phenotype-se",
+      "#tbl-preview-pheno-n": "#finput-phenotype-n"
+  };
+
+  var filesMetadata = () => {
+      return {
+          "separator": $("#txt-file-separator").val(),
+          "comment_char": $(
+              "#txt-file-comment-character").val(),
+          "na_strings": $("#txt-file-na").val()
+      }
+  };
+
+  var PREVIEW_ROWS = 5;
+
+  var handler_update_previews = (event) => {
+      Object.entries(preview_tables_to_elements_map).forEach((mapentry) => {
+          var preview_table = $(mapentry[0]);
+          var file_input = $(mapentry[1]);
+          if(file_input[0].files.length > 0) {
+              readFirstNLines(
+                  file_input[0].files[0],
+                  10,
+                  [makePreviewUpdater(preview_table)]);
+          }
+      });
+
+      if(typeof(resumables) !== "undefined") {
+          resumables.forEach((resumable) => {
+              if(resumable.files.length > 0) {
+                  readFirstNLines(
+                      resumable.files[0].file,
+                      10,
+                      [makePreviewUpdater(resumable.preview_table)]);
+              }
+          });
+      }
+  };
+
+  [
+      "#txt-file-separator",
+      "#txt-file-comment-character",
+      "#txt-file-na"
+  ].forEach((elementid) => {
+      $(elementid).on("change", handler_update_previews);
+  });
+
+  [
+      "#finput-phenotype-descriptions",
+      "#finput-phenotype-data",
+      "#finput-phenotype-se",
+      "#finput-phenotype-n"
+  ].forEach((elementid) => {
+      $(elementid).on("change", (event) => {
+          readFirstNLines(
+              event.target.files[0],
+              10,
+              [makePreviewUpdater(
+                  $("#" + event.target.getAttribute("data-preview-table")))]);
+      });
+  });
+
+
+  var resumableDisplayFiles = (display_area, files) => {
+      files.forEach((file) => {
+          display_area.find(".file-display").remove();
+          var display_element = display_area
+              .find(".file-display-template")
+              .clone();
+          remove_class(display_element, "visually-hidden");
+          remove_class(display_element, "file-display-template");
+          add_class(display_element, "file-display");
+          display_element.find(".filename").text(file.name
+                                                || file.fileName
+                                                || file.relativePath
+                                                || file.webkitRelativePath);
+          display_element.find(".filesize").text(
+              (file.size / (1024*1024)).toFixed(2) + "MB");
+          display_element.find(".fileuniqueid").text(file.uniqueIdentifier);
+          display_element.find(".filemimetype").text(file.file.type);
+          display_area.append(display_element);
+      });
+  };
+
+
+  var indicateProgress = (resumable, progress_bar) => {
+      return () => {/*Has no event!*/
+          var progress = (resumable.progress() * 100).toFixed(2);
+          var pbar = progress_bar.find(".progress-bar");
+          remove_class(progress_bar, "visually-hidden");
+          pbar.css("width", progress+"%");
+          pbar.attr("aria-valuenow", progress);
+          pbar.text("Uploading: " + progress + "%");
+      };
+  };
+
+  var retryUpload = (retry_button, cancel_button) => {
+      retry_button.on("click", (event) => {
+          resumable.files.forEach((file) => {file.retry();});
+          add_class(retry_button, "visually-hidden");
+          remove_class(cancel_button, "visually-hidden");
+          add_class(browse_button, "visually-hidden");
+      });
+  };
+
+  var cancelUpload = (cancel_button, retry_button) => {
+      cancel_button.on("click", (event) => {
+          resumable.files.forEach((file) => {
+              if(file.isUploading()) {
+                  file.abort();
+              }
+          });
+          add_class(cancel_button, "visually-hidden");
+          remove_class(retry_button, "visually-hidden");
+          remove_class(browse_button, "visually-hidden");
+      });
+  };
+
+
+  var startUpload = (browse_button, retry_button, cancel_button) => {
+      return (event) => {
+          remove_class(cancel_button, "visually-hidden");
+          add_class(retry_button, "visually-hidden");
+          add_class(browse_button, "visually-hidden");
+      };
+  };
+
+  var processForm = (form) => {
+      var formdata = new FormData(form);
+      uploaded_files.forEach((msg) => {
+          formdata.delete(msg["file-input-name"]);
+          formdata.append(msg["file-input-name"], JSON.stringify({
+              "uploaded-file": msg["uploaded-file"],
+              "original-name": msg["original-name"]
+          }));
+      });
+      formdata.append("resumable-upload", "true");
+      formdata.append("publication-id", $("#txt-publication-id").val());
+      return formdata;
+  }
+
+  var uploaded_files = new Set();
+  var submitForm = (new_file) => {
+      uploaded_files.add(new_file);
+      if(uploaded_files.size === resumables.length) {
+          var form = $("#frm-add-phenotypes");
+          if(form.length !== 1) {
+              // TODO: Handle error somehow?
+              alert("Could not find form!!!");
+              return false;
+          }
+
+          $.ajax({
+              "url": form.attr("action"),
+              "type": "POST",
+              "data": processForm(form[0]),
+              "processData": false,
+              "contentType": false,
+              "success": (data, textstatus, jqxhr) => {
+                  // TODO: Redirect to endpoint that should come as part of the
+                  //       success/error message.
+                  console.log("SUCCESS DATA: ", data);
+                  console.log("SUCCESS STATUS: ", textstatus);
+                  console.log("SUCCESS jqXHR: ", jqxhr);
+                  window.location.assign(window.location.origin + data["redirect-to"]);
+              },
+          });
+          return false;
+      }
+      return false;
+  };
+
+  var uploadSuccess = (file_input_name) => {
+      return (file, message) => {
+          submitForm({...JSON.parse(message), "file-input-name": file_input_name});
+      };
+  };
+
+
+  var uploadError = () => {
+      return (message, file) => {
+          $("#frm-add-phenotypes input[type=submit]").removeAttr("disabled");
+          console.log("THE FILE:", file);
+          console.log("THE ERROR MESSAGE:", message);
+      };
+  };
+
+
+
+  var makeResumableObject = (form_id, file_input_id, resumable_element_id, preview_table_id) => {
+      var the_form = $("#" + form_id);
+      var file_input = $("#" + file_input_id);
+      var submit_button = the_form.find("input[type=submit]");
+      if(file_input.length != 1) {
+          return false;
+      }
+      var r = errorHandler(
+          fileSuccessHandler(
+              uploadStartHandler(
+                  filesAddedHandler(
+                      markResumableDragAndDropElement(
+                          makeResumableElement(
+                              the_form.attr("data-resumable-target"),
+                              file_input.parent(),
+                              $("#" + resumable_element_id),
+                              submit_button,
+                              ["csv", "tsv", "txt"]),
+                          file_input.parent(),
+                          $("#" + resumable_element_id),
+                          $("#" + resumable_element_id + "-browse-button")),
+                      (files) => {
+                          // TODO: Also trigger preview!
+                          resumableDisplayFiles(
+                              $("#" + resumable_element_id + "-selected-files"), files);
+                          files.forEach((file) => {
+                              readFirstNLines(
+                                  file.file,
+                                  10,
+                                  [makePreviewUpdater(
+                                      $("#" + preview_table_id))])
+                          });
+                      }),
+                  startUpload($("#" + resumable_element_id + "-browse-button"),
+                              $("#" + resumable_element_id + "-retry-button"),
+                              $("#" + resumable_element_id + "-cancel-button"))),
+              uploadSuccess(file_input.attr("name"))),
+          uploadError());
+
+      /** Setup progress indicator **/
+      progressHandler(
+          r,
+          indicateProgress(r, $("#" + resumable_element_id + "-progress-bar")));
+
+      return r;
+  };
+
+  var resumables = [
+      ["frm-add-phenotypes", "finput-phenotype-descriptions", "resumable-phenotype-descriptions", "tbl-preview-pheno-desc"],
+      ["frm-add-phenotypes", "finput-phenotype-data", "resumable-phenotype-data", "tbl-preview-pheno-data"],
+      ["frm-add-phenotypes", "finput-phenotype-se", "resumable-phenotype-se", "tbl-preview-pheno-se"],
+      ["frm-add-phenotypes", "finput-phenotype-n", "resumable-phenotype-n", "tbl-preview-pheno-n"],
+  ].map((row) => {
+      r = makeResumableObject(row[0], row[1], row[2], row[3]);
+      r.preview_table = $("#" + row[3]);
+      return r;
+  }).filter((val) => {
+      return Boolean(val);
+  });
+
+  $("#frm-add-phenotypes input[type=submit]").on("click", (event) => {
+      event.preventDefault();
+      console.debug();
+      if ($("#txt-publication-id").val() == "") {
+          alert("You MUST provide a publication for the phenotypes.");
+          return false;
+      }
+      // TODO: Check all the relevant files exist
+      // TODO: Verify that files are not duplicated
+      var filenames = [];
+      var nondupfiles = [];
+      resumables.forEach((r) => {
+          var fname = r.files[0].file.name;
+          filenames.push(fname);
+          if(!nondupfiles.includes(fname)) {
+              nondupfiles.push(fname);
+          }
+      });
+
+      // Check that all files were provided
+      if(resumables.length !== filenames.length) {
+          window.alert("You MUST provide all the files requested.");
+          event.target.removeAttribute("disabled");
+          return false;
+      }
+
+      // Check that there are no duplicate files
+      var duplicates = Object.entries(filenames.reduce(
+          (acc, curr, idx, arr) => {
+              acc[curr] = (acc[curr] || 0) + 1;
+              return acc;
+          },
+          {})).filter((entry) => {return entry[1] !== 1;});
+      if(duplicates.length > 0) {
+          var msg = "The file(s):\r\n";
+          msg = msg + duplicates.reduce(
+              (msgstr, afile) => {
+                  return msgstr + "  • " + afile[0] + "\r\n";
+              },
+              "");
+          msg = msg + "is(are) duplicated. Please fix and try again.";
+          window.alert(msg);
+          event.target.removeAttribute("disabled");
+          return false;
+      }
+      // TODO: Check all fields
+      // Start the uploads.
+      event.target.setAttribute("disabled", "disabled");
+      resumables.forEach((r) => {r.upload();});
+  });
+</script>
+{%endblock%}