Switch to side-by-side view

--- a
+++ b/qiita_pet/static/js/sampleTemplateVue.js
@@ -0,0 +1,666 @@
+var sampleTemplatePage = null;
+
+// taken from https://stackoverflow.com/a/19963011
+function unique(array) {
+    return $.grep(array, function(el, index) {
+        return index == $.inArray(el, array);
+    });
+}
+
+function get_column_summary(study_id, portal, column, numSamples){
+  var row = $('.' + column + 'collapsed');
+
+  if (row.is(":hidden")) {
+    var cell = $(row.children()[0]);
+    cell.html('<img src="' + portal + '/static/img/waiting.gif" style="display:block;margin-left: auto;margin-right: auto"/>');
+    $.get(portal + '/study/description/sample_template/columns/', {study_id: study_id, column: column}, function(data) {
+      cell.html('');
+      var values = data['values'];
+      var uniques = unique(values);
+      var table = $('<table>').addClass('table').appendTo(cell);
+      if (uniques.length === 1) {
+        var tr = $('<tr>').appendTo(table);
+        var td = $('<td>').append(uniques[0] + ' is repeated in all rows.').appendTo(tr);
+      } else if (uniques.length === numSamples) {
+        var tr = $('<tr>').appendTo(table);
+        var td = $('<td>').append('All the values in this category are different').appendTo(tr);
+      } else {
+        var counts = {};
+        for (let u of uniques) { counts[u] = 0; }
+        for (let v of values) { counts[v]++; }
+        for (let u of uniques) {
+          var tr = $('<tr>').appendTo(table);
+          var td = $('<td>').append(u).appendTo(tr);
+          var td = $('<td>').append(counts[u]).appendTo(tr);
+        }
+      }
+    })
+  }
+}
+
+Vue.component('sample-template-page', {
+  template: '<div id="sample-template-main-div">' +
+              // Title div
+              '<div class="row">' +
+                '<div class="col-md-12">' +
+                  '<h3>Sample Information<span id="title-h3"></span></h3>' +
+                  '<h6>Last update: <span id="current-sample-info"></span></h6>' +
+                '</div>' +
+              '</div>' +
+              // Processing div
+              '<div class="row" id="st-processsing-div" style="border-radius: 10px; background: #EEE;">' +
+                '<div class="col-md-12">' +
+                  '<h4>We are processing your request via job "<span id="job-id-span"></span>". Status: "<span id="job-status-span"></span>"</h4>' +
+                '</div>' +
+              '</div>' +
+              '</br>' +
+              // Content div
+              '<div class="row">' +
+                '<div class="col-md-12" id="sample-template-contents">' +
+                '</div>' +
+              '</div>' +
+            '</div>',
+  props: ['portal', 'study-id'],
+  maxDirectUploadSize: 10485760,
+  methods: {
+    /**
+     *
+     * Checks the status of the job performing some task over the sample
+     * information
+     *
+     **/
+    checkJob: function() {
+      let vm = this;
+      $.get(vm.portal + '/study/process/job/', {job_id: vm.job}, function(data) {
+        var jobStatus;
+        jobStatus = data['job_status'];
+        $('#job-id-span').text(data['job_id']);
+        $('#job-status-span').text(jobStatus);
+
+        if (jobStatus === 'error' || jobStatus === 'success') {
+          vm.stopJobCheckInterval();
+          // Hide the processing div
+          $('#st-processsing-div').hide();
+          // Enable interaction bits
+          $('#update-st-div').show();
+          $('.st-interactive').prop('disabled', false);
+          if (jobStatus === 'error') {
+            // The job errored - show the error
+            bootstrapAlert(data['job_error'], "danger");
+          } else {
+            // The job succeeded - reload the interface to show changes
+            if (vm.refresh) {
+              vm.refresh = false;
+              location.reload();
+            } else {
+              vm.updateSampleTemplateOverview();
+            }
+          }
+        }
+      })
+        .fail(function(object, status, error_msg) {
+          bootstrapAlert("Error checking the job status: " + object.statusText, "danger");
+        });
+    },
+
+    /**
+     *
+     * Starts the interval that checks the status of the job.
+     *
+     **/
+    startJobCheckInterval: function(jobId) {
+      let vm = this;
+      vm.job = jobId;
+
+      // Hide the current error message (if any)
+      $('#alert-message').alert('close');
+      // Show the processing div
+      $('#st-processsing-div').show();
+      // Disable interaction bits
+      $('.st-interactive').prop('disabled', true);
+      $('#update-st-div').hide();
+      // Force the first check to happen now
+      vm.checkJob();
+      // Set the interval for further checking - this jobs tend to be way faster
+      // hence setting the interval every 2 seconds.
+      vm.interval = setInterval(vm.checkJob, 2000);
+    },
+
+    /**
+     *
+     * Stops the interval checking the status of the job
+     *
+     **/
+    stopJobCheckInterval: function() {
+      let vm = this;
+      clearInterval(vm.interval);
+    },
+
+    /**
+     *
+     * Performs a call to the server API to create a new sample template
+     *
+     **/
+    createSampleTemplate: function() {
+      let vm = this;
+      var fp = $('#file-select').val();
+      var dtype = $('#data-type-select').val();
+      var file = $('#st-direct-upload')[0].files[0];
+
+      var fd = new FormData();
+      fd.append('study_id', vm.studyId);
+      fd.append('filepath', fp);
+      fd.append('data_type', dtype);
+
+      if (file !== undefined) {
+        fd.append('direct_upload', true);
+        fd.append('theFile', file);
+      }
+
+      $.ajax({
+        url: vm.portal + '/study/description/sample_template/',
+        type: 'POST',
+        processData: false,
+        contentType: false,
+        data: fd,
+        success: function(data) {
+          vm.startJobCheckInterval(data['job']);
+        },
+        error: function (object, status, error_msg) {
+          bootstrapAlert("Error updating sample template: " + error_msg, "danger")
+        }
+      });
+    },
+
+    /**
+     *
+     * Performs a call to the server API to udpate the sample information
+     *
+     **/
+    updateSampleTemplate: function() {
+      let vm = this;
+      var file = $('#st-direct-upload')[0].files[0];
+
+      var fd = new FormData();
+      fd.append('op', 'replace');
+      fd.append('path', vm.studyId + '/data/');
+
+      if (file !== undefined) {
+        fd.append('direct_upload', true);
+        fd.append('value', file);
+      } else {
+        fd.append('value', $('#file-select').val());
+      }
+
+      $.ajax({
+        url: vm.portal + '/study/description/sample_template/',
+        type: 'PATCH',
+        processData: false,
+        contentType: false,
+        data: fd,
+        success: function(data) {
+          vm.startJobCheckInterval(data['job']);
+        },
+        error: function (object, status, error_msg) {
+          bootstrapAlert("Error updating sample template: " + error_msg, "danger")
+        }
+      });
+    },
+
+    /**
+     *
+     * Performs a call to the server API to delete a column from the sample template
+     *
+     * @param category str The category to be removed
+     * @param rowId int The row number where this category was placed
+     *
+     **/
+    deleteColumn: function(category, rowId) {
+      let vm = this;
+      $.ajax({
+        url: vm.portal + '/study/description/sample_template/',
+        type: 'PATCH',
+        data: {'op': 'remove', 'path': vm.studyId + '/columns/' + category},
+        success: function(data) {
+          vm.rowId = rowId;
+          vm.rowType = 'column';
+          vm.startJobCheckInterval(data['job']);
+        },
+        error: function (object, status, error_msg) {
+          bootstrapAlert("Error deleting column: " + error_msg, "danger")
+        }
+      });
+    },
+
+    /**
+     *
+     * Performs a call to the server API to delete a sample from the sample template
+     *
+     * @param sample str The sample to be removed
+     * @param rowId int The row number where this sample was placed
+     *
+     **/
+    deleteSamples: function(samples) {
+      let vm = this;
+      var total_samples = samples.length;
+      if (total_samples === 0){
+        alert('No samples selected!');
+      } else {
+        if (confirm('Are you sure you want to delete ' + total_samples + ' samples?')) {
+          $.ajax({
+            url: vm.portal + '/study/description/sample_template/',
+            type: 'PATCH',
+            data: {'op': 'remove', 'path': vm.studyId + '/samples/' + samples},
+            success: function(data) {
+              vm.rowId = 0;
+              vm.rowType = 'sample';
+              vm.startJobCheckInterval(data['job']);
+            },
+            error: function (object, status, error_msg) {
+              bootstrapAlert("Error deleting sample: " + error_msg, "danger")
+            }
+          });
+        }
+      }
+    },
+
+    /**
+     *
+     * Performs a call to the server API to delete the sample template
+     *
+     **/
+    deleteSampleTemplate: function() {
+      let vm = this;
+      if(confirm("Are you sure you want to delete the sample information?")) {
+        vm.refresh = true;
+        $.ajax({
+          url: vm.portal + '/study/description/sample_template/?study_id=' + vm.studyId,
+          type: 'DELETE',
+          success: function(data) {
+            vm.startJobCheckInterval(data['job']);
+          },
+          error: function (object, status, error_msg) {
+            bootstrapAlert("Error deleting sample information: " + error_msg, "danger")
+          }
+        });
+      }
+    },
+
+    /**
+     *
+     * Creates the GUI for the summary table
+     *
+     **/
+    populateSampleInfoTable: function() {
+      let vm = this;
+      // Gathering this information is expensive in some studies. By issuing
+      // a different AJAX call for it, we can keep showing the rest of the interface
+      // (and interact with it) without having to wait for this information to
+      // show up - also the creation of the table occurs now in client side
+      // rather than in server side.
+      $.get(vm.portal + '/study/description/sample_template/columns/', {study_id: vm.studyId}, function(data) {
+        var catValues, $tr, $td, rowIdx, collapsedId, $trVal, $div, $btn;
+        $div = $('<div>').addClass('panel panel-default').appendTo('#sample-info-tab');
+        $('<div>').addClass('panel-heading').appendTo($div).append('Information summary');
+        var $mtable = $('<table>').addClass('table').appendTo($div);
+        var $table = $('<tbody>').appendTo($mtable);
+        var categories = data['values'];
+        categories.sort(function(a, b){return a[0].localeCompare(b[0], 'en', {'sensitivity': 'base'});});
+
+        rowIdx = 0;
+        for (var cat of categories) {
+          $tr = $('<tr>').attr('id', 'col-row-' + rowIdx).appendTo($table);
+          if (vm.editable && vm.userCanEdit) {
+            $td = $('<td>').appendTo($tr);
+            $btn = $('<button>').addClass('btn btn-danger st-interactive').appendTo($td).attr('data-column', cat).attr('data-row-id', rowIdx);
+            $('<span>').addClass('glyphicon glyphicon-trash').appendTo($btn);
+            $btn.on('click', function () {
+              if (confirm('Are you sure you want to delete `' + $(this).attr('data-column') + '`?')) {
+                vm.deleteColumn($(this).attr('data-column'), $(this).attr('data-row-id'));
+              }
+            });
+          }
+          rowIdx += 1;
+          $td = $('<td>').appendTo($tr);
+          $('<b>').append(cat + ': ').appendTo($td);
+          $td.append('&nbsp;&nbsp;&nbsp;&nbsp;');
+          collapsedId = cat + 'collapsed';
+          fcall = 'get_column_summary(' + vm.studyId + ', "' + vm.portal + '", "' + cat + '", ' + vm.numSamples + ')';
+          $bt = $('<button>').addClass('btn btn-default').attr('onclick', fcall).attr('data-toggle', 'collapse').attr('data-target', '.' + collapsedId).append('Values').appendTo($td);
+          $trVal = $('<tr>').addClass('collapse').addClass(collapsedId).appendTo($table);
+          $('<td>').attr('colspan', '3').append('&nbsp;').appendTo($trVal);
+        }
+
+        // Scroll to the desired row
+        if(vm.rowType === 'column' && vm.rowId !== null) {
+          // taken from: http://stackoverflow.com/a/2906009
+          var container = $('html, body');
+          var scrollTo = $('#col-row-' + vm.rowId);
+          container.animate({
+              scrollTop: scrollTo.offset().top - container.offset().top + container.scrollTop()
+          });
+          vm.rowId = null;
+        }
+      })
+        .fail(function(object, status, error_msg) {
+          bootstrapAlert("Error loading sample information: " + object.statusText, "danger");
+        });
+    },
+
+    /**
+     *
+     * Creates the GUI for the Sample-Prep table
+     *
+     **/
+    populateSamplePrepTab: function() {
+      let vm = this;
+      show_loading('sample-prep-tab');
+      $.get(vm.portal + '/study/description/sample_summary/', {study_id: vm.studyId}, function(data) {
+        $('#sample-prep-tab').html(data);
+      })
+        .fail(function(object, status, error_msg) {
+          bootstrapAlert("Error loading sample-prep information: " + object.statusText, "danger");
+        });
+    },
+
+    /**
+     *
+     * Creates the GUI for the case that a sample template exists
+     *
+     **/
+    populateExistingSampleTemplate: function() {
+      let vm = this;
+      var $btn, $div, $small, $row, $col, $ul, $li, $tab;
+
+      //  Clear the contents of the div
+      $('#sample-template-contents').empty();
+
+      // Add the buttons next to the title
+      // Download Sample Information button
+      $('#current-sample-info').append(vm.current_file.split('_')[1].split('.')[0]);
+      $('#title-h3').append(' ');
+      $btn = $('<a>').addClass('btn btn-default').attr('href', vm.portal + '/download/' + vm.downloadId).appendTo('#title-h3');
+      $('<span>').addClass('glyphicon glyphicon-download-alt').appendTo($btn);
+      $btn.append(' Sample Info');
+      // Delete button (only if the user can edit)
+      if (vm.userCanEdit) {
+        $('#title-h3').append(' ');
+        $btn = $('<button>').addClass('btn btn-danger st-interactive').on('click', vm.deleteSampleTemplate).appendTo('#title-h3');
+        $('<span>').addClass('glyphicon glyphicon-trash').appendTo($btn);
+        $btn.append(' Delete');
+      }
+      // Show older files button (only if the sample information has older files)
+      if (vm.oldFiles.length > 0) {
+        // Add the button
+        $('#title-h3').append(' ');
+        $btn = $('<button>').addClass('btn btn-default').attr('data-toggle', 'collapse').attr('data-target', '#st-old-files').appendTo('#title-h3');
+        $('<span>').addClass('glyphicon glyphicon-eye-open').appendTo($btn);
+        $btn.append(' Show old files');
+
+        // Add the div that hold the old files
+        $div = $('<div>').attr('id', 'st-old-files').addClass('collapse').appendTo('#sample-template-contents');
+        $div.css('padding', '10px 10px 10px 10px').css('border-radius', '10px').css('background', '#EEE');
+        $small = $('<small>').appendTo($div).append('<label>Old files</label>');
+        for (var oldFile of vm.oldFiles) {
+          $small.append('<br/>' + oldFile);
+        }
+      }
+
+      // Adding the alert for restrictions
+      if (vm.sample_restrictions !== '') {
+        $row = $('<div>').addClass('row').appendTo('#sample-template-contents');
+        $col = $('<h5>').appendTo($row);
+        $('<div>').addClass('alert').addClass('alert-warning').append(vm.sample_restrictions).appendTo($row)
+      }
+
+      // After adding the buttons we can add the two tabs - one holding the Sample Information
+      // and the other one holding the Sample and preparation summary
+      $row = $('<div>').addClass('row').appendTo('#sample-template-contents');
+      $col = $('<div>').addClass('col-md-12').appendTo($row);
+
+      // The two "pills"
+      $ul = $('<ul>').addClass('nav nav-pills').appendTo($col);
+      $li = $('<li>').css('border', '1px solid #428bca').css('border-radius', '5px').appendTo($ul);
+      if (vm.rowType == 'column') {
+        $li.addClass('active');
+      }
+      $('<a>').attr('data-toggle', 'tab').attr('href', '#sample-info-tab').appendTo($li).append('Sample Information');
+      $li = $('<li>').css('border', '1px solid #428bca').css('border-radius', '5px').appendTo($ul);
+      if (vm.rowType == 'sample') {
+        $li.addClass('active');
+      }
+      $('<a>').attr('data-toggle', 'tab').attr('href', '#sample-prep-tab').appendTo($li).append('Sample-Prep Summary');
+
+      // The two tab divs are contained in a single tab-content div
+      $div = $('<div>').addClass('tab-content').appendTo($col);
+      $tab = $('<div>').addClass('tab-pane').attr('id', 'sample-info-tab').appendTo($div);
+      if (vm.rowType == 'column') {
+        $tab.addClass('active');
+      }
+      // Add the number of samples
+      $tab.append('<label>Number of samples:</label> ' + vm.numSamples + '</br>')
+      // Add the number of columns
+      $tab.append('<label>Number of columns:</label> ' + vm.numColumns + '</br>')
+
+      if (vm.userCanEdit) {
+        // Add the select to update the sample information
+        $row = $('<div>').attr('id', 'update-st-div').addClass('row form-group').appendTo($tab);
+        $('<label>').addClass('col-sm-2 col-form-label').append('Update sample information:').appendTo($row);
+
+        // Add the direct upload field
+        $('<label>')
+          .addClass('btn btn-default')
+            .append('Direct upload file <small>(< 2MB)</small>')
+            .appendTo($row)
+            .append('<input type="file" style="display: none;" id="st-direct-upload">');
+        $('#st-direct-upload').on('change', function() {
+          if (this.files.length != 1) {
+            alert('You can only upload one file.')
+            return false;
+          }
+          if (this.files[0].size > vm.maxDirectUploadSize) {
+            alert('You can only upload files smaller than 2MB. For larger files please use the "Upload Files" button on the left.');
+            return false;
+          }
+          vm.updateSampleTemplate()
+        });
+
+
+        $col = $('<div>').addClass('col-sm-3').appendTo($row);
+        $select = $('<select>').attr('id', 'file-select').addClass('form-control').appendTo($col);
+        $('<option>').attr('value', "").append('Choose file...').appendTo($select);
+        for (var opt of vm.uploadedFiles) {
+          $('<option>').attr('value', opt).append(opt).appendTo($select);
+        }
+        // Add the button to trigger the update
+        $col = $('<div>').addClass('col-sm-2').attr('id', 'update-btn-div').appendTo($row).hide();
+        $('<button>').addClass('btn btn-success form-control').append('Update').appendTo($col).on('click', vm.updateSampleTemplate);
+        $('#file-select').on('change', function() {
+          if (this.value === "") {
+            $('#update-btn-div').hide()
+          } else {
+            $('#update-btn-div').show()
+          }
+        });
+      }
+
+      // Populate the sample information table
+      vm.populateSampleInfoTable();
+
+      // Sample-prep tab
+      $tab = $('<div>').addClass('tab-pane').attr('id', 'sample-prep-tab').appendTo($div);
+      if (vm.rowType === 'sample') {
+        $tab.addClass('active');
+      }
+      vm.populateSamplePrepTab();
+    },
+
+    /**
+     *
+     * Creates the GUI to create a new sample template
+     *
+     **/
+    populateNewSampleTemplateForm: function() {
+      let vm = this;
+      var $row, $col, $select;
+
+      // Clear the contents of the div
+      $('#sample-template-contents').empty();
+
+      // To avoid code duplication creating the DOM elements, create a list
+      // with the contents and create the DOM elements in the for loop
+      var rowContents = [
+        // First one contains the uploaded files
+        {label: 'Select sample information file:', selectId: 'file-select', options: vm.uploadedFiles, placeholder: 'Choose file...'},
+        // Second one contains the data types
+        {label: 'If uploading a QIIME mapping file, select the data type of the prep information:', selectId: 'data-type-select', options: vm.dataTypes, placeholder: 'Choose a data type...'}]
+
+      // Create the DOM elements
+      for (var rC of rowContents) {
+        $row = $('<div>').addClass('row form-group').appendTo('#sample-template-contents');
+        $('<label>').addClass('col-sm-3 col-form-label').append(rC.label).appendTo($row);
+        $col = $('<div>').addClass('col-sm-3').appendTo($row);
+        $select = $('<select>').attr('id', rC.selectId).addClass('form-control').appendTo($col);
+        $('<option>').attr('value', "").append(rC.placeholder).appendTo($select);
+        for (var opt of rC.options) {
+          $('<option>').attr('value', opt).append(opt).appendTo($select);
+        }
+
+        if (rC['selectId'] == 'file-select') {
+          // Add the direct upload field
+          $('<label>')
+            .addClass('btn btn-default')
+              .append('Direct upload file <small>(< 2MB)</small>')
+              .appendTo($row)
+              .append('<input type="file" style="display: none;" id="st-direct-upload">');
+          $('#st-direct-upload').on('change', function() {
+            if (this.files.length != 1) {
+              alert('You can only upload one file.')
+              return false;
+            }
+            if (this.files[0].size > vm.maxDirectUploadSize) {
+              alert('You can only upload files smaller than 2MB. For larger files please use the "Upload Files" button on the left.');
+              return false;
+            }
+            vm.createSampleTemplate()
+          });
+        }
+      }
+
+      // Add the button - by default hidden
+      $row = $('<div>').attr('id', 'create-btn-div').addClass('row form-group').appendTo('#sample-template-contents').hide();
+      $col = $('<div>').addClass('col-sm-1').appendTo($row);
+      $('<button>').addClass('btn btn-success form-control st-interactive').append('Create').appendTo($col).on('click', vm.createSampleTemplate);
+
+      // Show/hide the button base on the value of the file selector
+      $('#file-select').on('change', function() {
+        if (this.value === "") {
+          $('#create-btn-div').hide()
+        } else {
+          $('#create-btn-div').show()
+        }
+      });
+    },
+
+    /**
+     *
+     * Performs a query to the server to update the sample template overview information
+     *
+     **/
+    updateSampleTemplateOverview: function () {
+      let vm = this;
+
+      $.get(vm.portal + '/study/description/sample_template/overview/', {study_id: vm.studyId}, function(data) {
+        vm.exists = data['exists'];
+        vm.dataTypes = data['data_types'];
+        vm.uploadedFiles = data['uploaded_files'];
+        vm.userCanEdit = data['user_can_edit'];
+        vm.job = data['job'];
+        vm.downloadId = data['download_id'];
+        vm.current_file = data['st_files'][0];
+        vm.oldFiles = data['st_files'].slice(1);
+        vm.numSamples = data['num_samples'];
+        vm.numColumns = data['num_columns'];
+        vm.columns = data['columns'];
+        vm.sample_restrictions = data['sample_restrictions'];
+
+        // fixing message for nicer display
+        if (vm.sample_restrictions !== '') {
+          vm.sample_restrictions = "Sample Info " + vm.sample_restrictions.split(" ").slice(2).join(" ");
+        }
+
+        // Populate the sample-template-contents
+        $('#title-h3').empty();
+        if (!vm.exists) {
+          vm.populateNewSampleTemplateForm();
+        } else {
+          vm.populateExistingSampleTemplate();
+        }
+
+        // Check the job for first time
+        if (vm.job !== null) {
+          $.get(vm.portal + '/study/process/job/', {job_id: vm.job}, function(data) {
+            var jobStatus = data['job_status'];
+            if (jobStatus === 'error') {
+              bootstrapAlert(data['job_error'], "danger");
+            } else if (jobStatus !== 'success') {
+              vm.startJobCheckInterval(vm.job);
+            }
+          })
+            .fail(function(object, status, error_msg) {
+              bootstrapAlert("Error checking the job status: " + object.statusText, "danger");
+            });
+        }
+
+      })
+        .fail(function(object, status, error_msg) {
+          bootstrapAlert("Error gathering Sample Information from server: " + object.statusText, "danger");
+        });
+    },
+
+    /**
+     *
+     * Cleans up the current object
+     *
+     **/
+    destroy: function() {
+      let vm = this;
+      vm.stopJobCheckInterval()
+    }
+  },
+  /**
+   *
+   * This function gets called by Vue once the HTML template is ready in the DOM
+   *
+   **/
+  mounted() {
+    let vm = this;
+
+    vm.interval = null;
+    vm.job = null;
+    vm.editable = true;
+    vm.rowId = null;
+    vm.rowType = 'column';
+    vm.refresh = false;
+    $('#st-processsing-div').hide();
+
+    show_loading('sample-template-contents');
+
+    // Get the overview information from the server
+    vm.updateSampleTemplateOverview();
+  }
+});
+
+/**
+ *
+ * Creates a new Vue object for the Sample Template in a safe way
+ *
+ * @param target str The id of the target div for the new Vue object
+ *
+ **/
+function newSampleTemplateVue(target) {
+  if (sampleTemplatePage !== null) {
+    sampleTemplatePage.$refs.stElem.destroy();
+  }
+  sampleTemplatePage = new Vue({el: target});
+};