about summary refs log tree commit diff
path: root/wqflask
diff options
context:
space:
mode:
Diffstat (limited to 'wqflask')
-rw-r--r--wqflask/wqflask/search_results.py16
-rw-r--r--wqflask/wqflask/server_side.py93
-rw-r--r--wqflask/wqflask/snp_browser/snp_browser.py54
-rw-r--r--wqflask/wqflask/static/new/css/snp_browser.css6
-rw-r--r--wqflask/wqflask/templates/search_result_page.html38
-rw-r--r--wqflask/wqflask/templates/snp_browser.html142
-rw-r--r--wqflask/wqflask/views.py42
7 files changed, 318 insertions, 73 deletions
diff --git a/wqflask/wqflask/search_results.py b/wqflask/wqflask/search_results.py
index 0d0894a4..c4ea2921 100644
--- a/wqflask/wqflask/search_results.py
+++ b/wqflask/wqflask/search_results.py
@@ -28,9 +28,9 @@ class SearchResultPage(object):
     #maxReturn = 3000
 
     def __init__(self, kw):
-        """This class gets invoked after hitting submit on the main menu (in
-views.py).
-
+        """
+            This class gets invoked after hitting submit on the main menu (in
+            views.py).
         """
 
         ###########################################
@@ -86,7 +86,6 @@ views.py).
             else:
                 self.gen_search_result()
 
-
     def gen_search_result(self):
         """
         Get the info displayed in the search result table from the set of results computed in
@@ -161,7 +160,14 @@ views.py).
                         trait_dict[key] = trait_dict[key].decode('utf-8')
                 trait_list.append(trait_dict)
 
-        self.trait_list = json.dumps(trait_list)
+        self.trait_list = trait_list
+
+        if this_trait.dataset.type == "ProbeSet":
+            self.header_data_names = ['index', 'display_name', 'symbol', 'description', 'location', 'mean', 'lrs_score', 'lrs_location', 'additive']
+        elif this_trait.dataset.type == "Publish":
+            self.header_data_names = ['index', 'display_name', 'description', 'mean', 'authors', 'pubmed_text', 'lrs_score', 'lrs_location', 'additive']
+        elif this_trait.dataset.type == "Geno":
+            self.header_data_names = ['index', 'display_name', 'location']
 
     def search(self):
         """
diff --git a/wqflask/wqflask/server_side.py b/wqflask/wqflask/server_side.py
new file mode 100644
index 00000000..824b00aa
--- /dev/null
+++ b/wqflask/wqflask/server_side.py
@@ -0,0 +1,93 @@
+# handles server side table processing
+
+
+
+class ServerSideTable(object):
+    '''
+        This class is used to do server-side processing
+        on the DataTables table such as paginating, sorting,
+        filtering(not implemented) etc. This takes the load off
+        the client-side and reduces the size of data interchanged.
+
+        Usage:
+            ServerSideTable(table_data, request_values)
+        where,
+            `table_data` must have data members
+            `rows_count` as number of rows in the table,
+            `table_rows` as data rows of the table,
+            `header_data_names` as headers names of the table.
+
+            `request_values` must have request arguments values
+            including the DataTables server-side processing arguments.
+
+        Have a look at snp_browser_table() function in 
+        wqflask/wqflask/views.py for reference use.
+    '''
+
+    def __init__(self, rows_count, table_rows, header_data_names, request_values):
+        self.request_values = request_values
+        self.sEcho = self.request_values['sEcho']
+
+        self.rows_count = rows_count
+        self.table_rows = table_rows
+        self.header_data_names = header_data_names
+        
+        self.sort_rows()
+        self.paginate_rows()
+
+    def sort_rows(self):
+        '''
+        Sorts the rows taking in to account the column (or columns) that the
+        user has selected.
+        '''
+        def is_reverse(str_direction):
+            ''' Maps the 'desc' and 'asc' words to True or False. '''
+            return True if str_direction == 'desc' else False
+
+        if (self.request_values['iSortCol_0'] != "") and (int(self.request_values['iSortingCols']) > 0):
+            for i in range(0, int(self.request_values['iSortingCols'])):
+                column_number = int(self.request_values['iSortCol_' + str(i)])
+                column_name = self.header_data_names[column_number - 1]
+                sort_direction = self.request_values['sSortDir_' + str(i)]
+                self.table_rows = sorted(self.table_rows,
+                              key=lambda x: x[column_name],
+                              reverse=is_reverse(sort_direction))
+
+    def paginate_rows(self):
+        '''
+        Selects a subset of the filtered and sorted data based on if the table
+        has pagination, the current page and the size of each page.
+        '''
+        def requires_pagination():
+            ''' Check if the table is going to be paginated '''
+            if self.request_values['iDisplayStart'] != "":
+                if int(self.request_values['iDisplayLength']) != -1:
+                    return True
+            return False
+
+        if not requires_pagination():
+            return
+
+        start = int(self.request_values['iDisplayStart'])
+        length = int(self.request_values['iDisplayLength'])
+
+        # if search returns only one page
+        if len(self.table_rows) <= length:
+            # display only one page
+            self.table_rows = self.table_rows[start:]
+        else:
+            limit = -len(self.table_rows) + start + length
+            if limit < 0:
+                # display pagination
+                self.table_rows = self.table_rows[start:limit]
+            else:
+                # display last page of pagination
+                self.table_rows = self.table_rows[start:]
+
+    def get_page(self):
+        output = {}
+        output['sEcho'] = str(self.sEcho)
+        output['iTotalRecords'] = str(float('Nan'))
+        output['iTotalDisplayRecords'] = str(self.rows_count)
+        output['data'] = self.table_rows
+        return output
diff --git a/wqflask/wqflask/snp_browser/snp_browser.py b/wqflask/wqflask/snp_browser/snp_browser.py
index 6c3fcf53..a52399a2 100644
--- a/wqflask/wqflask/snp_browser/snp_browser.py
+++ b/wqflask/wqflask/snp_browser/snp_browser.py
@@ -4,7 +4,7 @@ import string
 from PIL import (Image)
 
 from utility.logger import getLogger
-logger = getLogger(__name__ )
+logger = getLogger(__name__)
 
 from base import species
 from base import webqtlConfig
@@ -14,21 +14,21 @@ class SnpBrowser(object):
     def __init__(self, start_vars):
         self.strain_lists = get_browser_sample_lists()
         self.initialize_parameters(start_vars)
-        self.limit_number = 10000
 
         if self.first_run == "false":
             self.filtered_results = self.get_browser_results()
+            self.table_rows = self.get_table_rows()
+            self.rows_count = len(self.table_rows)
 
-            if len(self.filtered_results) <= self.limit_number:
-                self.table_rows = self.get_table_rows()
-            else:
-                self.empty_columns = None
+            del self.filtered_results
+
+            if 'sEcho' not in start_vars:
                 self.table_rows = []
 
             if self.limit_strains == "true":
-                self.header_fields, self.empty_field_count = get_header_list(variant_type = self.variant_type, strains = self.chosen_strains, empty_columns = self.empty_columns)
+                self.header_fields, self.empty_field_count, self.header_data_names = get_header_list(variant_type = self.variant_type, strains = self.chosen_strains, empty_columns = self.empty_columns)
             else:
-                self.header_fields, self.empty_field_count = get_header_list(variant_type = self.variant_type, strains = self.strain_lists, species = self.species_name, empty_columns = self.empty_columns)
+                self.header_fields, self.empty_field_count, self.header_data_names = get_header_list(variant_type = self.variant_type, strains = self.strain_lists, species = self.species_name, empty_columns = self.empty_columns)
 
     def initialize_parameters(self, start_vars):
         if 'first_run' in start_vars:
@@ -368,19 +368,19 @@ class SnpBrowser(object):
 
         #ZS: list of booleans representing which columns are entirely empty, so they aren't displayed on the page; only including ones that are sometimes empty (since there's always a location, etc)
         self.empty_columns = {
-                    "snp_source": "false",
-                    "conservation_score": "false",
-                    "gene_name": "false",
-                    "transcript": "false",
-                    "exon": "false",
-                    "domain_2": "false",
-                    "function": "false", 
-                    "function_details": "false"
-            }
+            "snp_source": "false",
+            "conservation_score": "false",
+            "gene_name": "false",
+            "transcript": "false",
+            "exon": "false",
+            "domain_2": "false",
+            "function": "false",
+            "function_details": "false"
+        }
 
         the_rows = []
         for i, result in enumerate(self.filtered_results):
-            this_row = []
+            this_row = {}
             if self.variant_type == "SNP":
                 snp_name, rs, chr, mb, alleles, gene, transcript, exon, domain, function, function_details, snp_source, conservation_score, snp_id = result[:14]
                 allele_value_list = result[14:]
@@ -520,13 +520,10 @@ class SnpBrowser(object):
                     "source_name": str(source_name)
                 }
                 #this_row = [indel_name, indel_chr, indel_mb_s, indel_mb_e, indel_strand, indel_type, indel_size, indel_sequence, source_name]
-            else:
-                this_row = {}
 
             the_rows.append(this_row)
 
         return the_rows
-                
 
     def include_record(self, domain, function, snp_source, conservation_score):
         """ Decide whether to add this record """
@@ -674,9 +671,13 @@ def get_header_list(variant_type, strains, species = None, empty_columns = None)
     empty_field_count = 0 #ZS: This is an awkward way of letting the javascript know the index where the allele value columns start; there's probably a better way of doing this
 
     header_fields = []
+    header_data_names = []
     if variant_type == "SNP":
         header_fields.append(['Index', 'SNP ID', 'Chr', 'Mb', 'Alleles', 'Source', 'ConScore', 'Gene', 'Transcript', 'Exon', 'Domain 1', 'Domain 2', 'Function', 'Details'])
+        header_data_names = ['index', 'snp_name', 'chr', 'mb_formatted', 'alleles', 'snp_source', 'conservation_score', 'gene_name', 'transcript', 'exon', 'domain_1', 'domain_2', 'function', 'function_details']
+
         header_fields.append(strain_list)
+        header_data_names += strain_list
 
         if empty_columns != None:
             if empty_columns['snp_source'] == "false":
@@ -703,11 +704,16 @@ def get_header_list(variant_type, strains, species = None, empty_columns = None)
             if empty_columns['function_details'] == "false":
                 empty_field_count += 1
                 header_fields[0].remove('Details')
+        
+        for col in empty_columns.keys():
+            if empty_columns[col] == "false":
+                header_data_names.remove(col)
 
     elif variant_type == "InDel":
         header_fields = ['Index', 'ID', 'Type', 'InDel Chr', 'Mb Start', 'Mb End', 'Strand', 'Size', 'Sequence', 'Source']
+        header_data_names = ['index', 'indel_name', 'indel_type', 'indel_chr', 'indel_mb_s', 'indel_mb_e', 'indel_strand', 'indel_size', 'indel_sequence', 'source_name']
 
-    return header_fields, empty_field_count
+    return header_fields, empty_field_count, header_data_names
 
 def get_effect_details_by_category(effect_name = None, effect_value = None):
     gene_list = []
@@ -868,8 +874,6 @@ def get_gene_id_name_dict(species_id, gene_name_list):
     if len(results) > 0:
         for item in results:
             gene_id_name_dict[item[1]] = item[0]
-    else:
-        pass
 
     return gene_id_name_dict
 
@@ -883,7 +887,7 @@ def check_if_in_gene(species_id, chr, mb):
         query = """SELECT geneId,geneSymbol
                    FROM GeneList
                    WHERE chromosome = '{0}' AND
-                        (txStart < {1} AND txEnd > {1}); """.format(species_id, chr, mb)
+                        (txStart < {1} AND txEnd > {1}); """.format(chr, mb)
 
     result = g.db.execute(query).fetchone()
 
diff --git a/wqflask/wqflask/static/new/css/snp_browser.css b/wqflask/wqflask/static/new/css/snp_browser.css
index 30fe9a59..a7942d2a 100644
--- a/wqflask/wqflask/static/new/css/snp_browser.css
+++ b/wqflask/wqflask/static/new/css/snp_browser.css
@@ -6,6 +6,10 @@ table.dataTable thead th {
   vertical-align: bottom;
 }
 
+table.dataTable tbody tr.selected {
+  background-color: #ffee99;
+}
+
 table.dataTable thead .sorting,
 table.dataTable thead .sorting_asc,
 table.dataTable thead .sorting_desc,
@@ -18,7 +22,7 @@ table.dataTable thead .sorting_desc_disabled {
 table.dataTable thead th{
   border-right: 1px solid white;
   color: white;
-  background-color: royalblue;
+  background-color: #369;
 }
 
 table.dataTable tbody td {
diff --git a/wqflask/wqflask/templates/search_result_page.html b/wqflask/wqflask/templates/search_result_page.html
index f2334512..765c4ab8 100644
--- a/wqflask/wqflask/templates/search_result_page.html
+++ b/wqflask/wqflask/templates/search_result_page.html
@@ -160,6 +160,16 @@
 
     <script type="text/javascript" charset="utf-8">
         $(document).ready( function () {
+
+            var getParams = function(url) {
+                let parser = document.createElement('a');
+                parser.href = url;
+                let params = parser.search.substring(1);
+                if(params.length > 0) {
+                    return ('?'+params);
+                }
+                return params;
+            };
             
             $('#trait_table tr').click(function(event) {
                 if (event.target.type !== 'checkbox') {
@@ -225,7 +235,7 @@
                       'data': null,
                       'width': "25px",
                       'orderDataType': "dom-checkbox",
-                      'orderSequence': [ "desc", "asc"],
+                      'orderable': false,
                       'render': function(data, type, row, meta) {
                         return '<input type="checkbox" name="searchResult" class="checkbox trait_checkbox" value="' + data.hmac + '">'
                       }
@@ -373,15 +383,33 @@
                     }{% endif %}
                 ],
                 "order": [[1, "asc" ]],
-                'sDom': "itirp",
+                {% if dataset.type != 'Geno' %}
+                buttons: [
+                    {
+                        extend: 'columnsToggle',
+                        columns: function( idx, data, node ) {
+                          if (idx != 0) {
+                            return true;
+                          } else {
+                            return false;
+                          }
+                        },
+                        postfixButtons: [ 'colvisRestore' ]
+                    }
+                ],
+                'sDom': "Bpitirp",
+                {% else %}
+                'sDom': "pitirp",
+                {% endif %}
                 'iDisplayLength': 500,
                 'deferRender': true,
                 'paging': true,
                 'orderClasses': true,
                 'processing': true,
-                'language': {
-                  'loadingRecords': '&nbsp;',
-                  'processing': 'Loading...'
+                'bServerSide': true,
+                'sAjaxSource': '/search_table'+getParams(window.location.href),
+                'infoCallback': function(settings, start, end, max, total, pre) {
+                  return "Showing " + start + " to " + (start + this.api().data().length - 1) + " of " + total + " entries";
                 }
             } );
             
diff --git a/wqflask/wqflask/templates/snp_browser.html b/wqflask/wqflask/templates/snp_browser.html
index a96b8e3b..b9aea570 100644
--- a/wqflask/wqflask/templates/snp_browser.html
+++ b/wqflask/wqflask/templates/snp_browser.html
@@ -15,7 +15,7 @@
         <input type="hidden" name="first_run" value="{{ first_run }}">
         <input type="hidden" name="chosen_strains_mouse" value="{{ chosen_strains_mouse|join(",") }}">
         <input type="hidden" name="chosen_strains_rat" value="{{ chosen_strains_rat|join(",") }}">
-        <div class="col-xs-4" style="padding-left: 0px;">
+        <div class="col-xs-4" style="width: 260px; padding-left: 30px; padding-right: 0px;">
           <div class="form-group row" style="margin-bottom: 5px;">
             <label for="snp_or_indel" style="text-align: right;" class="col-xs-4 col-form-label"><b>Type:</b></label>
             <div class="col-xs-8">
@@ -74,7 +74,7 @@
             </div>
           </div>
         </div>
-        <div class="col-xs-4" style="padding-left: 0px;">
+        <div class="col-xs-4" style="width: 310px; padding-left: 0px; padding-right: 20px;">
           <div class="form-group row" style="margin-bottom: 10px;">
             <label for="strains" style="text-align: right;" class="col-xs-4 col-form-label"><b>Strains:</b></label>
             <div class="col-xs-8">
@@ -107,8 +107,14 @@
               </div>
             </div>
           </div>
+          <div class="form-group row">
+            <label class="col-xs-4 col-form-label"></label>
+            <div class="col-xs-8" style="margin-top: 65px;">
+              <input class="btn btn-primary" type="button" name="export_csv" value="Export to CSV">
+            </div>
+          </div>
         </div>
-        <div class="col-xs-4" style="padding-left: 20px;">
+        <div class="col-xs-4" style="width: 310px; padding-left: 20px;">
           <div class="form-group row" style="margin-bottom: 5px;">
             <label for="domain" style="text-align: right;" class="col-xs-4 col-form-label"><b>Domain:</b></label>
             <div class="col-xs-8">
@@ -182,24 +188,21 @@
     </div>
 
     <div style="margin-top: 20px;">
-    {% if filtered_results is defined %}
-    {% if filtered_results|length > limit_number %}
-    There are more than 10000 results. Consider limiting your search to a smaller range.
-    {% else %}
+    {% if table_rows is defined %}
     <table class="dataTable cell-border nowrap" id="results_table" style="float: left;">
       <thead>
         <tr>
           <th></th>
           {% if header_fields|length == 2 %}
           {% for header in header_fields[0] %}
-          <th data-export="{{ header }}">{{ header }}</th>
+          <th data-export="{{ header }}" name="{{ header }}">{{ header }}</th>
           {% endfor %}
           {% for strain in header_fields[1] %}
-          <th data-export="{{ strain }}" style="align: center; text-align: center; line-height: 15px;">{% for letter in strain %}<div style="transform: rotate(90deg);">{{ letter }}</div>{% endfor %}</th>
+          <th data-export="{{ strain }}" name="{{ strain }}" style="align: center; text-align: center; line-height: 12px;">{% for letter in strain|reverse %}<div style="transform: rotate(270deg);">{{ letter }}</div>{% endfor %}</th>
           {% endfor %}
           {% else %}
           {% for header in header_fields %}
-          <th data-export="{{ header }}">{{ header }}</th>
+          <th data-export="{{ header }}" name="{{ header }}">{{ header }}</th>
           {% endfor %}
           {% endif %}
         </tr>
@@ -208,7 +211,6 @@
       <td colspan="100%" align="center"><br><b><font size="15">Loading...</font></b><br></td>
       </tbody>
     </table>
-    {% endif %}
     {% endif %}     
     </div>
   </div>
@@ -222,12 +224,24 @@
   <script language="javascript" type="text/javascript" src="/static/new/javascript/typeahead_rn6.json"></script>
 
   <script type='text/javascript'>
-      var json_rows = {{ table_rows|safe }};
       var empty_columns = {{ empty_columns|safe }};
+
+      var remain_field_count = 15 - {{ empty_field_count|safe }};
+      var total_field_count = 15 - {{ empty_field_count|safe }} + {{ allele_list|safe|length }};
   </script>
 
   <script language="javascript">
 
+    var getParams = function(url) {
+      let parser = document.createElement('a');
+      parser.href = url;
+      let params = parser.search.substring(1);
+      if(params.length > 0) {
+        return ('?'+params);
+      }
+      return params;
+    };
+
     var substringMatcher = function(strs) {
       return function findMatches(q, cb) {
         var matches, substringRegex;
@@ -260,19 +274,20 @@
       source: substringMatcher(rat_genes)
     });
 
-    {% if filtered_results is defined %}
+    {% if table_rows is defined %}
     $("#results_table").DataTable( {
-      'data': json_rows,
       {% if variant_type == "SNP" %}
       'columns': [
         {
           'data': null,
+          'className': 'dt-body-center',
           'orderable': false,
           'render': function(data, type, row, meta) {
-            return '<input type="checkbox" name="trait_check">'
+            return '<input type="checkbox" class="checkbox" id="variant_checkbox" onchange="onVarinatCheck(this)" name="trait_check">'
           }
         }, {
-          'data': 'index'
+          'data': 'index',
+          'className': 'dt-body-right'
         }, {
           'data': null,
           'render': function(data, type, row, meta) {
@@ -283,27 +298,30 @@
             }
           }
         }, {
-          'data': 'chr'
+          'data': 'chr',
+          'className': 'dt-body-center'
         }, {
-          'data': 'mb_formatted'
+          'data': 'mb_formatted',
+          'className': 'dt-body-right'
         }, {
           'data': 'alleles'
         }, {% if empty_columns['snp_source'] == "true" %}{
           'data': null,
           'render': function(data, type, row, meta) {
             if (data.snp_source == "Sanger/UCLA") {
-              return '<a href="' + data.source_urls[0] + '">Sanger</a><a href="' + data.source_urls[1] + '">UCLA</a>'
+              return '<a href="' + data.source_urls[0] + '">Sanger</a>, <a href="' + data.source_urls[1] + '">UCLA</a>'
             } else {
               return data.snp_source
             }
           }
         }, {% endif %} {% if empty_columns['conservation_score'] == "true" %}{
-          'data': 'conservation_score'
+          'data': 'conservation_score',
+          'className': 'dt-body-right'
         }, {% endif %} {% if empty_columns['gene_name'] == "true" %}{
           'data': null,
           'render': function(data, type, row, meta) {
             if (data.gene_name != "") {
-              return '<i>' + data.gene_name + '</i><br><a href="' + data.gene_link + '">NCBI</a>'
+              return '<i>' + data.gene_name + '</i>, <a href="' + data.gene_link + '">NCBI</a>'
             } else {
               return data.gene_name
             }
@@ -330,6 +348,7 @@
         }, {% endif %} {% for item in allele_list %} {
           'data': null,
           'orderable': false,
+          'className': 'dt-body-center',
           'render': function(data, type, row, meta) {
             if (typeof data.allele_value_list[{{ loop.index - 1 }}][0] !== "undefined") {
               return data.allele_value_list[{{ loop.index - 1 }}][0]
@@ -339,12 +358,9 @@
           }
         }{% if loop.index < allele_list|length %},{% endif %}{% endfor %}
       ],
-      'createdRow': function( row, data, dataIndex) {
-        $('td', row).eq(0).attr("style", "text-align: center; padding: 4px 10px 2px 10px;");
-        $('td', row).eq(1).attr("align", "right");
-        for (i = {{ 15 - empty_field_count }}; i < ({{ 15 - empty_field_count }} + {{ allele_list|length }}); i++) {
+      'createdRow': function(row, data, dataIndex) {
+        for (i = remain_field_count; i < total_field_count; i++) {
           var this_allele = $('td', row).eq(i).text();
-          $('td', row).eq(i).attr("style", "text-align: center; padding: 4px 10px 2px 10px;");
           switch (this_allele) {
             case "A":
               $('td', row).eq(i).addClass('A_allele_color');
@@ -380,24 +396,29 @@
         {
           'data': null,
           'render': function(data, type, row, meta) {
-            return '<input type="checkbox" name="trait_check">'
+            return '<input type="checkbox" class="checkbox" id="variant_checkbox" onchange="onVarinatCheck(this)" name="trait_check">'
           }
         }, {
-          'data': 'index'
+          'data': 'index',
+          'className': 'dt-body-right'
         }, {
           'data': 'indel_name'
         }, {
           'data': 'indel_type'
         }, {
-          'data': 'indel_chr'
+          'data': 'indel_chr',
+          'className': 'dt-body-center'
         }, {
-          'data': 'indel_mb_s'
+          'data': 'indel_mb_s',
+          'className': 'dt-body-right'
         }, {
-          'data': 'indel_mb_e'
+          'data': 'indel_mb_e',
+          'className': 'dt-body-right'
         }, {
           'data': 'indel_strand'
         }, {
-          'data': 'indel_size'
+          'data': 'indel_size',
+          'className': 'dt-body-right'
         }, {
           'data': 'indel_sequence'
         }, {
@@ -407,15 +428,28 @@
       {% endif %}
       'order': [[1, "asc" ]],
       'sDom': "rtip",
-      'iDisplayLength': 500,
-      'processing': true,
-      'language': {
-        'loadingRecords': '&nbsp;',
-        'processing': 'Loading...'
+      'iDisplayLength': 100,
+      'bServerSide': true,
+      'sAjaxSource': '/snp_browser_table'+getParams(window.location.href),
+      'infoCallback': function(settings, start, end, max, total, pre) {
+        return "Showing " + start + " to " + (start + this.api().data().length - 1) + " of " + total + " entries";
       }
     });
     {% endif %}
 
+    function onVarinatCheck(checkboxElem) {
+        if (checkboxElem.checked) {
+            if (!checkboxElem.parentElement.parentElement.classList.contains('selected')) {
+                checkboxElem.parentElement.parentElement.classList.add('selected')
+            }
+        }
+        else {
+            if (checkboxElem.parentElement.parentElement.classList.contains('selected')) {
+                checkboxElem.parentElement.parentElement.classList.remove('selected')
+            }
+        }
+    }
+
     $("#species_select").change(function() {
         this_species = $(this).val();
         $("#strain_select").empty()
@@ -509,6 +543,40 @@
       });
       $("input[name=chosen_strains]").val(strain_list.join(","));
     });
+
+    
+
+    $("input[name=export_csv]").click(function() {
+      var csv = [];
+      var rows = document.querySelectorAll("table tr");
+
+      var headers = [];
+      var col_header = rows[0].querySelectorAll("th");
+      for(let i = 1; i < col_header.length; i++) {
+        headers.push(col_header[i].getAttribute("name"));
+      }
+      csv.push(headers.join(","));
+
+      for (let i = 1; i < rows.length; i++) {
+        var row = [], cols = rows[i].querySelectorAll("td");
+        var checkBox = rows[i].querySelector("input");
+
+        if(checkBox.checked == true) {
+          for (let j = 1; j < cols.length; j++)
+            row.push(cols[j].innerText);
+
+            csv.push(row.join(","));
+        }
+      }
+
+      var csvFile = new Blob([csv.join("\n")], {type: "text/csv"});
+      var downloadLink = document.createElement("a");
+      downloadLink.download = "variant_data.csv";
+      downloadLink.href = window.URL.createObjectURL(csvFile);
+      downloadLink.style.display = "none";
+      document.body.appendChild(downloadLink);
+      downloadLink.click();
+    });
   </script>
 {% endblock %}
 
diff --git a/wqflask/wqflask/views.py b/wqflask/wqflask/views.py
index 25563e86..632e0d2d 100644
--- a/wqflask/wqflask/views.py
+++ b/wqflask/wqflask/views.py
@@ -26,6 +26,15 @@ import sqlalchemy
 from wqflask import app
 from flask import g, Response, request, make_response, render_template, send_from_directory, jsonify, redirect, url_for, send_file
 
+from wqflask import group_manager
+from wqflask import resource_manager
+from wqflask import search_results
+from wqflask import export_traits
+from wqflask import gsearch
+from wqflask import update_search_results
+from wqflask import docs
+from wqflask import news
+from wqflask import server_side
 from wqflask.submit_bnw import get_bnw_input
 from base.data_set import create_dataset, DataSet    # Used by YAML in marker_regression
 from wqflask.show_trait import show_trait
@@ -220,6 +229,26 @@ def search_page():
     else:
         return render_template("search_error.html")
 
+@app.route("/search_table", methods=('GET',))
+def search_page_table():
+    logger.info("in search_page table")
+    logger.info(request.url)
+
+    logger.info("request.args is", request.args)
+    the_search = search_results.SearchResultPage(request.args)
+
+    logger.info(type(the_search.trait_list))
+    logger.info(the_search.trait_list)
+    
+    current_page = server_side.ServerSideTable(
+        len(the_search.trait_list),
+        the_search.trait_list,
+        the_search.header_data_names,
+        request.args,
+    ).get_page()
+
+    return flask.jsonify(current_page)
+
 @app.route("/gsearch", methods=('GET',))
 def gsearchact():
     logger.info(request.url)
@@ -896,6 +925,19 @@ def db_info_page():
 
     return render_template("info_page.html", **template_vars.__dict__)
 
+@app.route("/snp_browser_table", methods=('GET',))
+def snp_browser_table():
+    logger.info(request.url)
+    snp_table_data = snp_browser.SnpBrowser(request.args)
+    current_page = server_side.ServerSideTable(
+        snp_table_data.rows_count,
+        snp_table_data.table_rows,
+        snp_table_data.header_data_names,
+        request.args,
+    ).get_page()
+
+    return flask.jsonify(current_page)
+
 @app.route("/tutorial/WebQTLTour", methods=('GET',))
 def tutorial_page():
     #ZS: Currently just links to GN1