From 17651e071ecc41bea456eeefd0818ffae9a8e0e7 Mon Sep 17 00:00:00 2001
From: Matthew Dillon <matthewrdillon@gmail.com>
Date: Fri, 10 Nov 2017 11:18:33 -0700
Subject: [PATCH] ENH: Collection filterings (#42)

Fixes #21
Fixes #28
Fixes #34
---
 app/components/ccdb-filter.js                 |   5 +
 app/components/collection/list-container.js   |   5 +-
 app/controllers/collections/index.js          |  55 ++++++++-
 app/models/region.js                          |  11 ++
 app/models/site.js                            |  13 ++
 app/models/study-location.js                  |   4 +-
 app/routes/collections/index.js               |  64 +++++++++-
 app/styles/app.css                            |   4 +
 app/templates/collections/index.hbs           |   3 +
 app/templates/components/ccdb-filter.hbs      |   1 +
 .../components/collection/list-container.hbs  | 113 ++++++++++++++++++
 package.json                                  |   2 +
 tests/unit/models/region-test.js              |  12 ++
 tests/unit/models/site-test.js                |  12 ++
 14 files changed, 296 insertions(+), 8 deletions(-)
 create mode 100644 app/components/ccdb-filter.js
 create mode 100644 app/models/region.js
 create mode 100644 app/models/site.js
 create mode 100644 app/templates/components/ccdb-filter.hbs
 create mode 100644 tests/unit/models/region-test.js
 create mode 100644 tests/unit/models/site-test.js

diff --git a/app/components/ccdb-filter.js b/app/components/ccdb-filter.js
new file mode 100644
index 0000000..a6b3c20
--- /dev/null
+++ b/app/components/ccdb-filter.js
@@ -0,0 +1,5 @@
+import Ember from 'ember';
+
+const { Component } = Ember;
+
+export default Component.extend({ });
diff --git a/app/components/collection/list-container.js b/app/components/collection/list-container.js
index e26790e..e9cb377 100644
--- a/app/components/collection/list-container.js
+++ b/app/components/collection/list-container.js
@@ -8,9 +8,10 @@ export default Component.extend({
 
   columns: [
     { label: 'Project', valuePath: 'project.name', },
+    { label: 'Region', valuePath: 'studyLocation.site.region.name', },
+    { label: 'Site', valuePath: 'studyLocation.site.name', },
     { label: 'Study Location', valuePath: 'studyLocation.code', },
-    { label: 'Method', valuePath: 'collectionMethod.code', },
-    { label: 'Type', valuePath: 'collectionType.name', },
+    { label: 'Method', valuePath: 'collectionMethod.name', },
     { label: '# of Traps', valuePath: 'numberOfTraps', },
     { label: 'Start', valuePath: 'startDateTime', },
     { label: 'End', valuePath: 'endDateTime', },
diff --git a/app/controllers/collections/index.js b/app/controllers/collections/index.js
index 7a022f4..48482d2 100644
--- a/app/controllers/collections/index.js
+++ b/app/controllers/collections/index.js
@@ -1,10 +1,36 @@
 import Ember from 'ember';
 
-const { Controller } = Ember;
+const { Controller, computed, get, set } = Ember;
+
 
 export default Controller.extend({
-  queryParams: ['page'],
+  queryParams: ['page', 'project', 'region', 'site', 'study_location',
+                'collection_method', 'number_of_traps', 'collection_start_date',
+                'collection_end_date'],
   page: 1,
+  project: [],
+  region: [],
+  site: [],
+  study_location: [],
+  collection_method: [],
+  number_of_traps: '',
+  collection_start_date: '',
+  collection_end_date: '',
+
+  options: computed('projectOptions', 'regionOptions', 'siteOptions',
+                    'studyLocationOptions', 'collectionMethodOptions', function() {
+    return {
+      projects: this.get('projectOptions'),
+      regions: this.get('regionOptions'),
+      sites: this.get('siteOptions'),
+      studyLocations: this.get('studyLocationOptions'),
+      collectionMethods: this.get('collectionMethodOptions'),
+    };
+  }),
+
+  _coerceId(model) {
+    return +get(model, 'id');
+  },
 
   actions: {
     changePage(page) {
@@ -16,5 +42,30 @@ export default Controller.extend({
     createCollection() {
       this.transitionToRoute('collections.create');
     },
+    changeFilter(filter) {
+      // Need to reset the page so that things don't get weird
+      set(this, 'page', 1);
+
+      const filterModelFields = ['project', 'region', 'site', 'study_location',
+                                 'collection_method'];
+
+      filterModelFields.forEach((field) => {
+        let fields = get(filter, field);
+        fields = fields.map(this._coerceId);
+        set(this, field, fields);
+      });
+
+      set(this, 'number_of_traps', get(filter, 'number_of_traps'));
+
+      ['collection_start_date', 'collection_end_date'].forEach((field) => {
+        let value = get(filter, field);
+        if (value) {
+          value = value.toJSON().split('T')[0];
+        } else {
+          value = '';
+        }
+        set(this, field, value);
+      });
+    },
   },
 });
diff --git a/app/models/region.js b/app/models/region.js
new file mode 100644
index 0000000..3530700
--- /dev/null
+++ b/app/models/region.js
@@ -0,0 +1,11 @@
+import DS from 'ember-data';
+
+const { Model, attr, hasMany } = DS;
+
+export default Model.extend({
+  name:      attr('string'),
+  code:      attr('string'),
+  sortOrder: attr('number'),
+
+  site: hasMany('site'),
+});
diff --git a/app/models/site.js b/app/models/site.js
new file mode 100644
index 0000000..04402e7
--- /dev/null
+++ b/app/models/site.js
@@ -0,0 +1,13 @@
+import DS from 'ember-data';
+
+const { Model, attr, hasMany, belongsTo } = DS;
+
+export default Model.extend({
+  name:        attr('string'),
+  code:        attr('string'),
+  description: attr('string'),
+  sortOrder:   attr('number'),
+
+  region:        belongsTo('region'),
+  studyLocation: hasMany('study-location'),
+});
diff --git a/app/models/study-location.js b/app/models/study-location.js
index 7568218..28969aa 100644
--- a/app/models/study-location.js
+++ b/app/models/study-location.js
@@ -1,6 +1,6 @@
 import DS from 'ember-data';
 
-const { Model, attr } = DS;
+const { Model, attr, belongsTo } = DS;
 
 export default Model.extend({
   name:               attr('string'),
@@ -10,4 +10,6 @@ export default Model.extend({
   collectingLocation: attr('string'),
   description:        attr('string'),
   sortOrder:          attr('number'),
+
+  site: belongsTo('site'),
 });
diff --git a/app/routes/collections/index.js b/app/routes/collections/index.js
index 08d7b8d..ab1eee6 100644
--- a/app/routes/collections/index.js
+++ b/app/routes/collections/index.js
@@ -1,14 +1,72 @@
 import Ember from 'ember';
 
-const { Route } = Ember;
+const { Route, RSVP } = Ember;
 
 export default Route.extend({
   queryParams: {
+    // qps are snake_case for the django api
     page: { refreshModel: true },
+    project: { refreshModel: true },
+    region: { refreshModel: true },
+    site: { refreshModel: true },
+    study_location: { refreshModel: true },
+    collection_method: { refreshModel: true },
+    number_of_traps: { refreshModel: true },
+    collection_start_date: { refreshModel: true },
+    collection_end_date: { refreshModel: true },
   },
 
   model(params) {
-    const include = {include: 'project,study-location,collection-method,collection-type'};
-    return this.get('store').query('collection', Object.assign(params, include));
+    const store = this.get('store');
+    const opts = {
+      include: 'project,study-location,study-location.site,site,collection-method',
+    };
+
+    return RSVP.hash({
+      projectOptions: store.findAll('project'),
+      regionOptions: store.findAll('region'),
+      siteOptions: store.findAll('site'),
+      studyLocationOptions: store.findAll('study-location'),
+      collectionMethodOptions: store.findAll('collection-method'),
+      model: store.query('collection', Object.assign(params, opts)),
+    });
+  },
+
+  setupController(controller, models) {
+    this._super(...arguments);
+    controller.setProperties(models);
+
+    const store = this.get('store');
+
+    let project = controller.get('project');
+    project = project.map(id => store.peekRecord('project', id));
+
+    let region = controller.get('region');
+    region = region.map(id => store.peekRecord('region', id));
+
+    let site = controller.get('site');
+    site = site.map(id => store.peekRecord('site', id));
+
+    let studyLocation = controller.get('study_location');
+    studyLocation = studyLocation.map(id => store.peekRecord('study-location', id));
+
+    let collectionMethod = controller.get('collection_method');
+    collectionMethod = collectionMethod.map(id => store.peekRecord('collection-method', id));
+
+    const numberOfTraps = controller.get('number_of_traps');
+    const collectionStartDate = controller.get('collection_start_date');
+    const collectionEndDate = controller.get('collection_end_date');
+
+    let filter = {
+      project,
+      region,
+      site,
+      study_location: studyLocation,
+      collection_method: collectionMethod,
+      number_of_traps: numberOfTraps,
+      collection_start_date: collectionStartDate,
+      collection_end_date: collectionEndDate,
+    }
+    controller.set('filters', filter);
   },
 });
diff --git a/app/styles/app.css b/app/styles/app.css
index 680bff1..1a2d097 100644
--- a/app/styles/app.css
+++ b/app/styles/app.css
@@ -50,6 +50,10 @@
   border-top-right-radius: 0;
 }
 
+.top-buffer {
+  margin-top: 20px;
+}
+
 /* Sidebar */
 .sidebar {
   position: fixed;
diff --git a/app/templates/collections/index.hbs b/app/templates/collections/index.hbs
index 07d407b..ca47c63 100644
--- a/app/templates/collections/index.hbs
+++ b/app/templates/collections/index.hbs
@@ -1,6 +1,9 @@
 {{
   collection/list-container
   model=model
+  filters=filters
+  options=options
+  changeFilter=(action 'changeFilter')
   changePage=(action 'changePage')
   onRowClick=(action 'rowClick')
   createCollection=(action 'createCollection')
diff --git a/app/templates/components/ccdb-filter.hbs b/app/templates/components/ccdb-filter.hbs
new file mode 100644
index 0000000..889d9ee
--- /dev/null
+++ b/app/templates/components/ccdb-filter.hbs
@@ -0,0 +1 @@
+{{yield}}
diff --git a/app/templates/components/collection/list-container.hbs b/app/templates/components/collection/list-container.hbs
index 872a0c8..6b7c9c9 100644
--- a/app/templates/components/collection/list-container.hbs
+++ b/app/templates/components/collection/list-container.hbs
@@ -4,6 +4,119 @@
   label='New Collection'
   onClick=(action createCollection)
 }}
+
+<hr>
+
+{{#ccdb-filter
+  filters=filters
+  options=options
+}}
+  <div class="well">
+    <div class="row">
+      <div class="col-md-3">
+        <label>Projects</label>
+        {{#power-select-multiple
+          options=options.projects
+          selected=filters.project
+          onchange=(action (mut filters.project))
+          searchField='name'
+          as |project|
+        }}
+          {{project.name}}
+        {{/power-select-multiple}}
+      </div>
+      <div class="col-md-3">
+        <label>Regions</label>
+        {{#power-select-multiple
+          options=options.regions
+          selected=filters.region
+          onchange=(action (mut filters.region))
+          searchField='name'
+          as |region|
+        }}
+          {{region.name}}
+        {{/power-select-multiple}}
+      </div>
+      <div class="col-md-3">
+        <label>Sites</label>
+        {{#power-select-multiple
+          options=options.sites
+          selected=filters.site
+          onchange=(action (mut filters.site))
+          searchField='name'
+          as |site|
+        }}
+          {{site.name}}
+        {{/power-select-multiple}}
+      </div>
+      <div class="col-md-3">
+        <label>Study Locations</label>
+        {{#power-select-multiple
+          options=options.studyLocations
+          selected=filters.study_location
+          onchange=(action (mut filters.study_location))
+          searchField='code'
+          as |studyLocation|
+        }}
+          {{studyLocation.code}}
+        {{/power-select-multiple}}
+      </div>
+    </div>
+
+    <div class="row">
+      <div class="col-md-3">
+        <label>Collection Methods</label>
+        {{#power-select-multiple
+          options=options.collectionMethods
+          selected=filters.collection_method
+          onchange=(action (mut filters.collection_method))
+          searchField='name'
+          as |collectionMethod|
+        }}
+          {{collectionMethod.name}}
+        {{/power-select-multiple}}
+      </div>
+      <div class="col-md-3">
+        <label>Number of Traps</label>
+        {{input type="text" class="form-control" value=filters.number_of_traps}}
+      </div>
+      <div class="col-md-3">
+        <label>Start Date</label>
+        {{
+          pikaday-input
+          onSelection=(action (mut filters.collection_start_date))
+          value=filters.collection_start_date
+          useUTC=true
+          format='YYYY-MM-DD'
+          class='form-control'
+        }}
+      </div>
+      <div class="col-md-3">
+        <label>End Date</label>
+        {{
+          pikaday-input
+          onSelection=(action (mut filters.collection_end_date))
+          value=filters.collection_end_date
+          useUTC=true
+          format='YYYY-MM-DD'
+          class='form-control'
+        }}
+      </div>
+    </div>
+
+    <div class="row top-buffer">
+      <div class="col-md-12">
+        {{
+          action-button
+          isSuccess=true
+          label='Search'
+          onClick=(action changeFilter filters)
+        }}
+      </div>
+    </div>
+  </div>
+{{/ccdb-filter}}
+
 {{
   ccdb-table
   model=model
diff --git a/package.json b/package.json
index b0dcd2d..b8c6c29 100644
--- a/package.json
+++ b/package.json
@@ -29,6 +29,7 @@
     "ember-cli-htmlbars": "^2.0.1",
     "ember-cli-htmlbars-inline-precompile": "^0.4.3",
     "ember-cli-inject-live-reload": "^1.4.1",
+    "ember-cli-moment-shim": "^3.5.0",
     "ember-cli-qunit": "^4.0.0",
     "ember-cli-shims": "^1.1.0",
     "ember-cli-sri": "^2.1.0",
@@ -40,6 +41,7 @@
     "ember-light-table": "^1.10.0",
     "ember-load-initializers": "^1.0.0",
     "ember-moment": "7.3.1",
+    "ember-pikaday": "^2.2.3",
     "ember-power-select": "^1.8.5",
     "ember-power-select-with-create": "0.4.3",
     "ember-resolver": "^4.0.0",
diff --git a/tests/unit/models/region-test.js b/tests/unit/models/region-test.js
new file mode 100644
index 0000000..e9468e8
--- /dev/null
+++ b/tests/unit/models/region-test.js
@@ -0,0 +1,12 @@
+import { moduleForModel, test } from 'ember-qunit';
+
+moduleForModel('region', 'Unit | Model | region', {
+  // Specify the other units that are required for this test.
+  needs: []
+});
+
+test('it exists', function(assert) {
+  let model = this.subject();
+  // let store = this.store();
+  assert.ok(!!model);
+});
diff --git a/tests/unit/models/site-test.js b/tests/unit/models/site-test.js
new file mode 100644
index 0000000..c75a2ee
--- /dev/null
+++ b/tests/unit/models/site-test.js
@@ -0,0 +1,12 @@
+import { moduleForModel, test } from 'ember-qunit';
+
+moduleForModel('site', 'Unit | Model | site', {
+  // Specify the other units that are required for this test.
+  needs: []
+});
+
+test('it exists', function(assert) {
+  let model = this.subject();
+  // let store = this.store();
+  assert.ok(!!model);
+});