Compare commits

...
This repository has been archived on 2025-03-30. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

57 commits

Author SHA1 Message Date
b3a5d1caa2 MAINT: Read-only user 2020-01-27 14:40:20 -07:00
Matthew Dillon
14283f0c77 Strain form referencing wrong notes variable 2015-12-02 11:52:36 -07:00
Matthew Dillon
3313b6e5f9 Don't unload user models on delete 2015-12-02 11:43:58 -07:00
Matthew Dillon
8d4affd572 Merge pull request #70 from thermokarst/refactormeas
Refactor measurements
2015-12-02 11:31:13 -07:00
Matthew Dillon
e283f3f046 Stop creating unnecessary characteristic 2015-12-02 11:18:18 -07:00
Matthew Dillon
0956ef3a25 Fix characteristic naming to measurement 2015-12-02 10:47:07 -07:00
Matthew Dillon
658304f728 Clean up no measurements on record 2015-12-02 10:42:59 -07:00
Matthew Dillon
50cc073d2f Prevent null-related error in new strain, linting 2015-12-02 10:25:04 -07:00
Matthew Dillon
66c185e441 Chained error handling in strains controller 2015-12-02 10:13:44 -07:00
Matthew Dillon
df5a50b1e2 Use ajax error service in password reset 2015-12-02 10:13:22 -07:00
Matthew Dillon
689ea55bed Ajax Error Service 2015-12-02 10:13:06 -07:00
Matthew Dillon
9496de21d9 Clean up characteristics dropdown 2015-12-01 19:25:29 -07:00
Matthew Dillon
2811143066 Queue new measurements
Still some bound attributes / coupling, but getting better
2015-12-01 13:13:24 -07:00
Matthew Dillon
14698d0394 Fix up serializer for ember-data 2 2015-12-01 13:11:42 -07:00
Matthew Dillon
233a2d09a1 Refactoring delete measurement 2015-12-01 07:03:43 -07:00
Matthew Dillon
a4dbb4a94d Add measurement table cancel 2015-11-30 17:31:12 -07:00
Matthew Dillon
6d97466075 ember-cli 1.13.13 2015-11-28 11:34:04 -07:00
Matthew Dillon
bb1efd6733 ember 2.2.0 & ember-data 2.2.1 2015-11-25 21:47:51 -07:00
Matthew Dillon
1c16c7239f Easiest to just unload the entire store
Fixes #66
2015-11-25 21:29:39 -07:00
Matthew Dillon
f93c4af029 Hide measurements on new strain
Fixes #65
2015-11-20 19:00:07 -07:00
Matthew Dillon
edc95ecb3a Fix char/strain sort order
Fixes #64.
2015-11-20 18:49:14 -07:00
Matthew Dillon
e45ca02afb Context-sensitive save-model mixin 2015-11-17 16:36:08 -07:00
Matthew Dillon
2bbb29d785 Removing char edit text 2015-11-17 16:17:32 -07:00
Matthew Dillon
8b4a26a932 Detailed characteristics view 2015-11-17 16:10:33 -07:00
Matthew Dillon
e33852120a Use API sort order for species 2015-11-17 15:53:22 -07:00
Matthew Dillon
a669945c35 Compare - link to chars 2015-11-17 13:47:04 -07:00
Matthew Dillon
9d57e1bbfb Fix up full name, again 2015-11-17 13:16:00 -07:00
Matthew Dillon
c350dd6fcc Force sort order for compare 2015-11-17 13:10:10 -07:00
Matthew Dillon
4e5a91f776 select2 4.0 2015-11-17 12:33:30 -07:00
Matthew Dillon
b19913aece User adapter
coalesceFindRequests: false
2015-11-17 09:59:02 -07:00
Matthew Dillon
40ac4b30af User permission error handling 2015-11-17 09:33:44 -07:00
Matthew Dillon
9fc81bcc87 Password reset verbiage 2015-11-17 09:20:18 -07:00
Matthew Dillon
4e0d67800c Fix strains index strain name formatting
Fixes #63
2015-11-17 09:07:49 -07:00
Matthew Dillon
14be82212d ember 2 series 2015-11-17 08:11:59 -07:00
Matthew Dillon
99309e0e91 Remove globals initializer
Fixes #62
2015-11-17 06:43:33 -07:00
Matthew Dillon
04d05f9795 ESA 1.0.1 2015-11-16 18:29:06 -07:00
Matthew Dillon
de630dba68 Lint 2015-11-16 18:15:20 -07:00
Matthew Dillon
55f71b0a00 Customizing refresh token flow
Departs from oauth2, oh well. Fixes #58.
2015-11-16 16:58:00 -07:00
Matthew Dillon
6b30b33281 Remove quint-notifications
Trying on travis
2015-11-16 12:42:02 -07:00
Matthew Dillon
9218c29c0e Tweaking bower.json to fix CI build errors 2015-11-16 12:25:27 -07:00
Matthew Dillon
c561e0ec76 Changed mind on strain model MU prop
Closes #55.
2015-11-16 11:39:53 -07:00
Matthew Dillon
b8e4ba3c84 Remove ES6 features from config 2015-11-16 11:39:16 -07:00
Matthew Dillon
7d05740901 Inline images in CSP 2015-11-16 10:53:28 -07:00
Matthew Dillon
7673b225f8 Strains form formatting
Gridforms doesn't play well with textarea, so just split rows for now.
Fixes #60.
2015-11-16 10:49:46 -07:00
Matthew Dillon
5ba3b125e8 Clean up delete button and tests
Fixes #59
2015-11-16 10:32:49 -07:00
Matthew Dillon
dfe2c9cd74 Lint 2015-11-16 09:37:31 -07:00
Matthew Dillon
41e79b6890 select2 ember-cli-build 2015-11-13 17:00:10 -07:00
Matthew Dillon
d05c31cc94 Dropping ember-select2 for custom component
Fixes #53
2015-11-13 15:13:46 -07:00
Matthew Dillon
024836cab0 Update getProperty helper 2015-11-13 15:01:16 -07:00
Matthew Dillon
62e651b597 #55, species 2015-11-13 14:31:29 -07:00
Matthew Dillon
075ef0aaa1 Rest of strain MU
#55
2015-11-13 14:27:08 -07:00
Matthew Dillon
81b6b9aee4 strain.strainNameMU to component
Part of #55
2015-11-13 14:24:26 -07:00
Matthew Dillon
20a6707144 Refactor delete button
Fixes #50
2015-11-13 14:16:50 -07:00
Matthew Dillon
031a83808c Handle password change errors 2015-11-13 13:43:55 -07:00
Matthew Dillon
936edd41d7 New Ajax Error util 2015-11-13 13:43:46 -07:00
Matthew Dillon
28e82d59ba ember-cli 1.13.12 2015-11-13 08:55:20 -07:00
Matthew Dillon
bb05e114d7 Clean up Quill Component
Fixes #54
2015-11-13 08:29:48 -07:00
66 changed files with 13697 additions and 261 deletions

2
.gitignore vendored
View file

@ -15,4 +15,4 @@
/libpeerconnection.log /libpeerconnection.log
npm-debug.log npm-debug.log
testem.log testem.log
.divshot-cache .firebase

View file

@ -1,9 +1,13 @@
import DS from 'ember-data'; import DS from 'ember-data';
import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin'; import DataAdapterMixin from 'ember-simple-auth/mixins/data-adapter-mixin';
import Ember from 'ember';
const { inject: { service } } = Ember;
const { RESTAdapter } = DS; const { RESTAdapter } = DS;
export default RESTAdapter.extend(DataAdapterMixin, { export default RESTAdapter.extend(DataAdapterMixin, {
globals: service(),
authorizer: 'authorizer:application', authorizer: 'authorizer:application',
namespace: function() { namespace: function() {

7
app/adapters/user.js Normal file
View file

@ -0,0 +1,7 @@
import ApplicationAdapter from '../adapters/application';
export default ApplicationAdapter.extend({
// If coalesceFindRequests is on, and we 403 on any requests, ESA logs
// the current user out. Better to split the requests up at the adapter level.
coalesceFindRequests: false,
});

View file

@ -1,6 +1,70 @@
import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant'; import OAuth2PasswordGrant from 'ember-simple-auth/authenticators/oauth2-password-grant';
import config from '../config/environment'; import config from '../config/environment';
import parseBase64 from '../utils/parse-base64';
import Ember from 'ember';
const { RSVP: { Promise }, isEmpty, run, Logger: { warn } } = Ember;
export default OAuth2PasswordGrant.extend({ export default OAuth2PasswordGrant.extend({
serverTokenEndpoint: `${config.apiURL}/api/authenticate`, serverTokenEndpoint: `${config.apiURL}/api/authenticate`,
serverTokenRefreshEndpoint: `${config.apiURL}/api/refresh`,
authenticate: function(identification, password) {
return new Promise((resolve, reject) => {
const data = { username: identification, password };
const serverTokenEndpoint = this.get('serverTokenEndpoint');
this.makeRequest(serverTokenEndpoint, data).then((response) => {
run(() => {
const token = parseBase64(response['access_token']);
const expiresAt = this._absolutizeExpirationTime(token['exp']);
this._scheduleAccessTokenRefresh(expiresAt, response['access_token']);
if (!isEmpty(expiresAt)) {
response = Ember.merge(response, { 'expires_at': expiresAt });
}
resolve(response);
});
}, (xhr) => {
run(null, reject, xhr.responseJSON || xhr.responseText);
});
});
},
_scheduleAccessTokenRefresh: function(expiresAt, accessToken) {
if (this.get('refreshAccessTokens')) {
const now = (new Date()).getTime();
const offset = (Math.floor(Math.random() * 5) + 5) * 1000;
if (!isEmpty(accessToken) && !isEmpty(expiresAt) && expiresAt > now - offset) {
run.cancel(this._refreshTokenTimeout);
delete this._refreshTokenTimeout;
if (!Ember.testing) {
this._refreshTokenTimeout = run.later(this, this._refreshAccessToken, expiresAt, accessToken, expiresAt - now - offset);
}
}
}
},
_refreshAccessToken: function(expiresAt, accessToken) {
const data = { 'token': accessToken };
const serverTokenRefreshEndpoint = this.get('serverTokenRefreshEndpoint');
return new Promise((resolve, reject) => {
this.makeRequest(serverTokenRefreshEndpoint, data).then((response) => {
run(() => {
const token = parseBase64(response['access_token']);
const expiresAt = this._absolutizeExpirationTime(token['exp']);
const data = Ember.merge(response, { 'expires_at': expiresAt });
this._scheduleAccessTokenRefresh(expiresAt, response['access_token']);
this.trigger('sessionDataUpdated', data);
resolve(data);
});
}, (xhr, status, error) => {
warn(`Access token could not be refreshed - server responded with ${error}.`);
reject();
});
});
},
_absolutizeExpirationTime: function(expiresAt) {
if (!isEmpty(expiresAt)) {
return new Date(expiresAt * 1000).getTime();
}
}
}); });

View file

@ -1,8 +1,7 @@
import Ember from 'ember'; import Ember from 'ember';
// This will be unneccesary when ember 2.0 lands const { get, Helper: { helper } } = Ember;
export function getProperty(params) {
return Ember.get(params[0], params[1]);
}
export default Ember.HTMLBars.makeBoundHelper(getProperty); export default helper(function(params) {
return get(params[0], params[1]);
});

View file

@ -1,20 +0,0 @@
import Ember from 'ember';
import config from '../config/environment';
var globals = Ember.Object.extend({
genus: config.APP.genus,
apiURL: config.apiURL,
});
export function initialize(container, application) {
application.register('service:globals', globals, {singleton: true});
application.inject('route', 'globals', 'service:globals');
application.inject('controller', 'globals', 'service:globals');
application.inject('component', 'globals', 'service:globals');
application.inject('adapter', 'globals', 'service:globals');
}
export default {
name: 'global-variables',
initialize: initialize
};

View file

@ -19,11 +19,13 @@ export function testConfig() {
this.post('/species'); this.post('/species');
this.get('/species/:id'); this.get('/species/:id');
this.put('/species/:id'); this.put('/species/:id');
this.delete('/species/:id');
this.get('/characteristics'); this.get('/characteristics');
this.post('/characteristics'); this.post('/characteristics');
this.get('/characteristics/:id'); this.get('/characteristics/:id');
this.put('/characteristics/:id'); this.put('/characteristics/:id');
this.delete('/characteristics/:id');
this.get('/strains', function(db /*, request*/) { this.get('/strains', function(db /*, request*/) {
return { return {
@ -39,4 +41,5 @@ export function testConfig() {
}; };
}); });
this.put('/strains/:id'); this.put('/strains/:id');
this.delete('/strains/:id');
} }

View file

@ -8,6 +8,10 @@ export default Mixin.create({
actions: { actions: {
delete: function() { delete: function() {
this.get('model').destroyRecord().then(() => { this.get('model').destroyRecord().then(() => {
// Instead of unloading the entire store, we keep the loaded user models
['species', 'strain', 'characteristic', 'measurement'].map((model) => {
this.get('store').unloadAll(model);
});
this.transitionToRoute(this.get('transitionRoute')); this.transitionToRoute(this.get('transitionRoute'));
}); });
}, },

View file

@ -23,11 +23,16 @@ export default Mixin.create({
cancel: function() { cancel: function() {
const model = this.get('model'); const model = this.get('model');
const isNew = model.get('isNew');
model.get('errors').clear(); model.get('errors').clear();
model.rollbackAttributes(); model.rollbackAttributes();
this.transitionToRoute(this.get('fallbackRouteCancel'), model); if (isNew) {
this.transitionToRoute(this.get('fallbackRouteCancel'));
} else {
this.transitionToRoute(this.get('fallbackRouteCancel'), model);
}
}, },
}, },
}); });

View file

@ -1,6 +1,5 @@
import DS from 'ember-data'; import DS from 'ember-data';
import config from '../config/environment'; import config from '../config/environment';
import Ember from 'ember';
const { Model, attr, hasMany } = DS; const { Model, attr, hasMany } = DS;
@ -17,9 +16,4 @@ export default Model.extend({
updatedBy : attr('number'), updatedBy : attr('number'),
sortOrder : attr('number'), sortOrder : attr('number'),
canEdit : attr('boolean'), canEdit : attr('boolean'),
// TODO: move this to component/helper
speciesNameMU: function() {
return Ember.String.htmlSafe(`<em>${this.get('speciesName')}</em>`);
}.property('speciesName').readOnly(),
}); });

View file

@ -2,6 +2,7 @@ import DS from 'ember-data';
import Ember from 'ember'; import Ember from 'ember';
const { Model, hasMany, belongsTo, attr } = DS; const { Model, hasMany, belongsTo, attr } = DS;
const { computed, String: { htmlSafe } } = Ember;
export default Model.extend({ export default Model.extend({
measurements : hasMany('measurements', { async: false }), measurements : hasMany('measurements', { async: false }),
@ -22,19 +23,8 @@ export default Model.extend({
sortOrder : attr('number'), sortOrder : attr('number'),
canEdit : attr('boolean'), canEdit : attr('boolean'),
// TODO: move this to component/helper fullNameMU: computed('species', 'strainName', function() {
strainNameMU: function() { const type = this.get('typeStrain') ? '<sup>T</sup>' : '';
let type = this.get('typeStrain') ? '<sup>T</sup>' : ''; return htmlSafe(`<em>${this.get('species.speciesName')}</em> ${this.get('strainName')}${type}`);
return Ember.String.htmlSafe(`${this.get('strainName')}${type}`);
}.property('strainName', 'typeStrain').readOnly(),
// TODO: move this to component/helper
fullName: Ember.computed('species', 'strainName', function() {
return `${this.get('species.speciesName')} ${this.get('strainNameMU')}`;
}), }),
// TODO: move this to component/helper
fullNameMU: function() {
return Ember.String.htmlSafe(`<em>${this.get('species.speciesName')}</em> ${this.get('strainNameMU')}`);
}.property('species', 'strainNameMU').readOnly(),
}); });

View file

@ -1,13 +1,23 @@
import Ember from 'ember'; import Ember from 'ember';
export default Ember.Component.extend({ const { Component } = Ember;
tagName: 'button',
classNames: ["button-red", "smaller"],
click: function() { export default Component.extend({
if (window.confirm("Do you really want to delete this?")) { tagName: 'span',
showConfirmDelete: false,
actions: {
initialClick: function() {
this.set('showConfirmDelete', true);
},
cancelDelete: function() {
this.set('showConfirmDelete', false);
},
confirmDelete: function() {
this.attrs.delete(); this.attrs.delete();
} },
}, },
}); });

View file

@ -1 +1,12 @@
Delete {{#unless showConfirmDelete}}
<button class="button-red smaller delete" {{action "initialClick"}}>
Delete
</button>
{{else}}
<button class="button-red smaller delete-confirm" {{action "confirmDelete"}}>
Confirm Delete
</button>
<button class="button-gray smaller delete-cancel" {{action "cancelDelete"}}>
Cancel Delete
</button>
{{/unless}}

View file

@ -1,8 +1,14 @@
import Ember from 'ember'; import Ember from 'ember';
export default Ember.Component.extend({ const { Component, computed, inject: { service } } = Ember;
export default Component.extend({
globals: service(),
tagName: 'em', tagName: 'em',
genus: function() {
genus: computed('globals.genus', function() {
return this.get('globals.genus').capitalize(); return this.get('globals.genus').capitalize();
}.property().readOnly(), }),
}); });

View file

@ -1,3 +1,7 @@
import Ember from 'ember'; import Ember from 'ember';
export default Ember.Component.extend({}); const { Component, inject: { service } } = Ember;
export default Component.extend({
globals: service(),
});

View file

@ -0,0 +1 @@
{{strain.strainName}}{{{if strain.typeStrain '<sup>T</sup>' ''}}}

View file

@ -1,13 +1,25 @@
import Ember from 'ember'; import Ember from 'ember';
/* global Quill */ /* global Quill */
export default Ember.Component.extend({ const { Component } = Ember;
quill: null,
export default Component.extend({
// Passed in
value: null, value: null,
update: null,
// Internal
quill: null,
didReceiveAttrs() {
this._super(...arguments);
if (!this.attrs.update) {
throw new Error(`You must provide an \`update\` action.`);
}
},
didInsertElement: function() { didInsertElement: function() {
let quill = new Quill(`#${this.get('elementId')} .editor`, { const quill = new Quill(`#${this.get('elementId')} .editor`, {
formats: ['bold', 'italic', 'underline'], formats: ['bold', 'italic', 'underline'],
modules: { modules: {
'toolbar': { container: `#${this.get('elementId')} .toolbar` } 'toolbar': { container: `#${this.get('elementId')} .toolbar` }

View file

@ -1,10 +1,12 @@
import Ember from 'ember'; import Ember from 'ember';
export default Ember.Component.extend({ const { Component, inject: { service } } = Ember;
export default Component.extend({
classNames: ["flakes-frame"], classNames: ["flakes-frame"],
session: Ember.inject.service('session'), session: service(),
currentUser: Ember.inject.service('session-account'), currentUser: service('session-account'),
didInsertElement: function() { didInsertElement: function() {
FlakesFrame.init(); FlakesFrame.init();

View file

@ -33,7 +33,7 @@
<br> <br>
{{link-to 'Sign Up' 'users.new'}} {{link-to 'Sign Up' 'users.new'}}
<br> <br>
{{link-to 'Locked Out?' 'users.requestlockouthelp'}} {{link-to 'Reset Password' 'users.requestlockouthelp'}}
</p> </p>
{{/if}} {{/if}}
</div> </div>

View file

@ -0,0 +1,54 @@
import Ember from 'ember';
const { Component, get, run: { schedule } } = Ember;
export default Component.extend({
tagName: 'select',
attributeBindings: [
'multiple',
],
options: null,
selected: null,
nameAttr: null,
placeholder: null,
change: function() {
let selectedInComponent = this.get('selected');
let selectedInWidget = this.$().val();
if (this.get('multiple')) {
if (selectedInWidget === null) {
selectedInWidget = [];
}
selectedInComponent = selectedInComponent.toString();
selectedInWidget = selectedInWidget.toString();
}
// We need this to prevent an infinite loop of afterRender -> change.
if (selectedInComponent !== selectedInWidget) {
this.attrs.update(this.$().val());
}
},
didInsertElement: function() {
let options = {};
options.placeholder = this.get('placeholder');
options.templateResult = function(item) {
if (!item.disabled) {
const text = get(item, 'element.innerHTML');
const $item = Ember.$(`<span>${text}</span>`);
return $item;
}
};
this.$().select2(options);
},
didRender: function() {
const selected = this.get('selected');
schedule('afterRender', this, function() {
this.$().val(selected).trigger('change');
});
},
});

View file

@ -0,0 +1,3 @@
{{#each options as |option|}}
<option value={{option.id}}>{{get-property option nameAttr}}</option>
{{/each}}

View file

@ -10,3 +10,7 @@
<div> <div>
{{link-to 'Forget your password?' 'users.requestlockouthelp'}} {{link-to 'Forget your password?' 'users.requestlockouthelp'}}
</div> </div>
<br>
<div>
Just checking things out? Log in with email <code>read-only</code> and password <code>bacteria</code>!
</div>

View file

@ -26,7 +26,6 @@
<dl class="span-2"> <dl class="span-2">
<dt>Measurements</dt> <dt>Measurements</dt>
<dd> <dd>
<p>To add/edit/remove a measurement, please visit the strain's page (links below)</p>
{{protected/characteristics/show/measurements-table characteristic=characteristic}} {{protected/characteristics/show/measurements-table characteristic=characteristic}}
</dd> </dd>
</dl> </dl>

View file

@ -1,7 +1,7 @@
import Ember from 'ember'; import Ember from 'ember';
const { Component, computed } = Ember; const { Component, computed } = Ember;
const { sort } = computed; const { alias, sort } = computed;
export default Component.extend({ export default Component.extend({
characteristic: null, characteristic: null,
@ -10,10 +10,9 @@ export default Component.extend({
return this.get('characteristic.measurements.length') > 0; return this.get('characteristic.measurements.length') > 0;
}), }),
sortParams: ['characteristic.characteristicTypeName', 'characteristic.sortOrder', 'characteristic.characteristicName'], measurements: alias('characteristic.measurements'),
sortAsc: true, sortParams: ['strain.sortOrder'],
paramsChanged: false, sortedMeasurements: sort('measurements', 'sortParams'),
sortedMeasurements: sort('characteristic.measurements', 'sortParams'),
actions: { actions: {
changeSortParam: function(col) { changeSortParam: function(col) {

View file

@ -7,24 +7,24 @@
<table class="flakes-table"> <table class="flakes-table">
<thead> <thead>
<tr> <tr>
<th {{action "changeSortParam" "strain.strainName"}} class="click">Strain</th> <th {{action "changeSortParam" "strain.sortOrder"}} class="click">Strain</th>
<th {{action "changeSortParam" "value"}} class="click">Value</th> <th {{action "changeSortParam" "value"}} class="click">Value</th>
<th {{action "changeSortParam" "notes"}} class="click">Notes</th> <th {{action "changeSortParam" "notes"}} class="click">Notes</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
{{#each sortedMeasurements as |row|}} {{#each sortedMeasurements as |measurement|}}
<tr> <tr>
<td> <td>
{{#link-to 'protected.strains.show' row.strain.id}} {{#link-to 'protected.strains.show' measurement.strain.id classBinding="measurement.strain.typeStrain:type-strain"}}
{{{row.strain.strainNameMU}}} {{measurement.strain.fullNameMU}}
{{/link-to}} {{/link-to}}
</td> </td>
<td> <td>
{{row.value}} {{measurement.value}}
</td> </td>
<td> <td>
{{row.notes}} {{measurement.notes}}
</td> </td>
</tr> </tr>
{{/each}} {{/each}}

View file

@ -4,7 +4,7 @@ const { Route } = Ember;
export default Route.extend({ export default Route.extend({
model: function(params) { model: function(params) {
return this.store.findRecord('characteristic', params.characteristic_id); return this.store.findRecord('characteristic', params.characteristic_id, { reload: true });
}, },
}); });

View file

@ -3,8 +3,8 @@ import Ember from 'ember';
const { Controller } = Ember; const { Controller } = Ember;
export default Controller.extend({ export default Controller.extend({
selectedStrains: null, selectedStrains: [],
selectedCharacteristics: null, selectedCharacteristics: [],
actions: { actions: {
search: function(query) { search: function(query) {

View file

@ -4,6 +4,7 @@ const { Component, computed, inject: { service } } = Ember;
export default Component.extend({ export default Component.extend({
session: service(), session: service(),
globals: service(),
strains: null, strains: null,
characteristics: null, characteristics: null,

View file

@ -14,8 +14,16 @@
<tbody> <tbody>
{{#each characteristics as |row|}} {{#each characteristics as |row|}}
<tr> <tr>
{{#each row key="@index" as |col|}} {{#each row key="@index" as |col index|}}
<td>{{col}}</td> <td>
{{#unless index}}
{{#link-to 'protected.characteristics.show' col.id}}
{{col.characteristicName}}
{{/link-to}}
{{else}}
{{col}}
{{/unless}}
</td>
{{/each}} {{/each}}
</tr> </tr>
{{/each}} {{/each}}

View file

@ -30,29 +30,28 @@ export default Route.extend({
} }
const compare = this.controllerFor('protected.compare'); const compare = this.controllerFor('protected.compare');
compare.set('selectedStrains', params.strain_ids); compare.set('selectedStrains', params.strain_ids.split(","));
compare.set('selectedCharacteristics', params.characteristic_ids); compare.set('selectedCharacteristics', params.characteristic_ids.split(","));
return this.get('ajax').request('/compare', { data: params }); return this.get('ajax').request('/compare', { data: params });
}, },
setupController: function(controller, model) { setupController: function(controller, model) {
model.forEach((m, i) => { model.forEach((m, i) => {
const c = this.store.peekRecord('characteristic', m[0]); model[i][0] = this.store.peekRecord('characteristic', m[0]);
model[i][0] = c.get('characteristicName');
}); });
const compare = this.controllerFor('protected.compare'); const compare = this.controllerFor('protected.compare');
const strains = []; const strains = [];
const strain_ids = compare.get('selectedStrains').split(','); const strain_ids = compare.get('selectedStrains');
strain_ids.forEach((id) => { strain_ids.forEach((id) => {
strains.push(this.store.peekRecord('strain', id)); strains.push(this.store.peekRecord('strain', id));
}); });
controller.set('strains', strains); controller.set('strains', strains);
const characteristics = []; const characteristics = [];
const characteristic_ids = compare.get('selectedCharacteristics').split(','); const characteristic_ids = compare.get('selectedCharacteristics');
characteristic_ids.forEach((id) => { characteristic_ids.forEach((id) => {
characteristics.push(this.store.peekRecord('characteristic', id)); characteristics.push(this.store.peekRecord('characteristic', id));
}); });

View file

@ -1,6 +1,6 @@
import Ember from 'ember'; import Ember from 'ember';
const { Component } = Ember; const { Component, computed: { sort } } = Ember;
export default Component.extend({ export default Component.extend({
characteristics: null, characteristics: null,
@ -10,8 +10,14 @@ export default Component.extend({
"update-strains": null, "update-strains": null,
"update-characteristics": null, "update-characteristics": null,
selectedStrains: null,
selectedCharacteristics: null, charSortParams: ['characteristicTypeName', 'sortOrder', 'characteristicName'],
sortedCharacteristics: sort('characteristics', 'charSortParams'),
strainSortParams: ['sortOrder'],
sortedStrains: sort('strains', 'sortParams'),
selectedStrains: [],
selectedCharacteristics: [],
updateStrains: function(selection) { updateStrains: function(selection) {
this.set('selectedStrains', selection); this.set('selectedStrains', selection);
@ -38,11 +44,11 @@ export default Component.extend({
strains.forEach((strain) => { strains.forEach((strain) => {
strain_ids.push(strain.get('id')); strain_ids.push(strain.get('id'));
}); });
this.updateStrains(strain_ids.join(",")); this.updateStrains(strain_ids);
}, },
deselectAllStrains: function() { deselectAllStrains: function() {
this.updateStrains(""); this.updateStrains([]);
}, },
selectAllCharacteristics: function() { selectAllCharacteristics: function() {
@ -51,11 +57,19 @@ export default Component.extend({
chars.forEach((char) => { chars.forEach((char) => {
char_ids.push(char.get('id')); char_ids.push(char.get('id'));
}); });
this.updateCharacteristics(char_ids.join(",")); this.updateCharacteristics(char_ids);
}, },
deselectAllCharacteristics: function() { deselectAllCharacteristics: function() {
this.updateCharacteristics(""); this.updateCharacteristics([]);
},
updateStrainSelection: function(selection) {
this.updateStrains(selection);
},
updateCharacteristicsSelection: function(selection) {
this.updateCharacteristics(selection);
}, },
}, },
}); });

View file

@ -5,12 +5,12 @@
<li> <li>
<label>Strains</label> <label>Strains</label>
{{ {{
select-2 x-select
options=sortedStrains
nameAttr='fullNameMU'
multiple=true multiple=true
content=strains selected=selectedStrains
value=selectedStrains update=(action "updateStrainSelection")
optionValuePath="id"
optionLabelPath="fullNameMU"
placeholder="Select one or more strains" placeholder="Select one or more strains"
}} }}
</li> </li>
@ -25,12 +25,12 @@
<li> <li>
<label>Characteristics</label> <label>Characteristics</label>
{{ {{
select-2 x-select
options=sortedCharacteristics
nameAttr='characteristicName'
multiple=true multiple=true
content=characteristics selected=selectedCharacteristics
value=selectedCharacteristics update=(action "updateCharacteristicsSelection")
optionValuePath="id"
optionLabelPath="characteristicName"
placeholder="Select one or more characteristics" placeholder="Select one or more characteristics"
}} }}
</li> </li>

View file

@ -6,7 +6,7 @@ const { Component, computed: { sort } } = Ember;
export default Component.extend(SetupMetaData, { export default Component.extend(SetupMetaData, {
species: null, species: null,
sortParams: ['speciesName', 'strainCount'], sortParams: ['sortOrder'],
sortedSpecies: sort('species', 'sortParams'), sortedSpecies: sort('species', 'sortParams'),
}); });

View file

@ -23,7 +23,7 @@
{{#each species.strains as |strain index|}} {{#each species.strains as |strain index|}}
{{if index ","}} {{if index ","}}
{{#link-to 'protected.strains.show' strain.id}} {{#link-to 'protected.strains.show' strain.id}}
{{{strain.strainNameMU}}} {{strain-name strain=strain}}
{{/link-to}} {{/link-to}}
{{/each}} {{/each}}
</td> </td>

View file

@ -4,7 +4,7 @@ const { Route } = Ember;
export default Route.extend({ export default Route.extend({
model: function(params) { model: function(params) {
return this.store.findRecord('species', params.species_id); return this.store.findRecord('species', params.species_id, { reload: true });
}, },
}); });

View file

@ -14,7 +14,7 @@
{{#each species.strains as |strain index|}} {{#each species.strains as |strain index|}}
<li> <li>
{{#link-to 'protected.strains.show' strain.id}} {{#link-to 'protected.strains.show' strain.id}}
{{{strain.strainNameMU}}} {{strain-name strain=strain}}
{{/link-to}} {{/link-to}}
</li> </li>
{{/each}} {{/each}}

View file

@ -19,7 +19,7 @@
{{#each strains as |strain index|}} {{#each strains as |strain index|}}
{{if index ","}} {{if index ","}}
{{#link-to 'protected.strains.show' strain.id}} {{#link-to 'protected.strains.show' strain.id}}
{{{strain.strainNameMU}}} {{strain-name strain=strain}}
{{/link-to}} {{/link-to}}
{{/each}} {{/each}}
{{add-button label="Add Strain" link="protected.strains.new" canAdd=metaData.canAdd}} {{add-button label="Add Strain" link="protected.strains.new" canAdd=metaData.canAdd}}

View file

@ -1,36 +1,61 @@
import Ember from 'ember'; import Ember from 'ember';
import SaveModel from '../../../../mixins/save-model';
import ajaxError from '../../../../utils/ajax-error';
const { Controller } = Ember; const { Controller, RSVP, inject: { service } } = Ember;
export default Controller.extend({
ajaxError: service('ajax-error'),
export default Controller.extend(SaveModel, {
// Required for SaveModel mixin
fallbackRouteSave: 'protected.strains.show', fallbackRouteSave: 'protected.strains.show',
fallbackRouteCancel: 'protected.strains.show', fallbackRouteCancel: 'protected.strains.show',
actions: { actions: {
addCharacteristic: function() { save: function(properties, deleteQueue, updateQueue) {
return this.store.createRecord('measurement', { let promises = [];
characteristic: this.store.createRecord('characteristic', { sortOrder: -999 }), properties.measurements.forEach((measurement) => {
if (measurement.get('isNew')) {
promises.push(measurement.save());
}
});
updateQueue.forEach((measurement) => {
promises.push(measurement.save());
});
deleteQueue.forEach((measurement) => {
promises.push(measurement.destroyRecord());
});
const model = this.get('model');
const fallbackRoute = this.get('fallbackRouteSave');
RSVP.all(promises).then(() => {
// Can't call _super inside promise, have to reproduce save-model
// mixin here :-(
model.setProperties(properties);
model.save().then((model) => {
this.get('flashMessages').clearMessages();
this.transitionToRoute(fallbackRoute, model);
});
}, (errors) => {
this.get('ajaxError').alert(errors);
}); });
}, },
saveMeasurement: function(measurement, properties) { cancel: function() {
measurement.setProperties(properties); const model = this.get('model');
measurement.save().then(() => {
this.get('flashMessages').clearMessages();
}, () => {
ajaxError(measurement.get('errors'), this.get('flashMessages'));
});
},
deleteMeasurement: function(measurement) { model.get('errors').clear();
const characteristic = measurement.get('characteristic'); model.rollbackAttributes();
if (characteristic.get('isNew')) {
characteristic.destroyRecord(); if (model.get('isNew')) {
this.transitionToRoute(this.get('fallbackRouteCancel'));
} else {
this.transitionToRoute(this.get('fallbackRouteCancel'), model);
} }
measurement.destroyRecord(); },
addMeasurement: function() {
return this.store.createRecord('measurement');
}, },
}, },

View file

@ -2,10 +2,8 @@
protected/strains/strain-form protected/strains/strain-form
strain=model strain=model
speciesList=speciesList speciesList=speciesList
add-characteristic=(action "addCharacteristic") add-measurement=(action "addMeasurement")
allCharacteristics=allCharacteristics allCharacteristics=allCharacteristics
save-measurement=(action "saveMeasurement")
delete-measurement=(action "deleteMeasurement")
on-save=(action "save") on-save=(action "save")
on-cancel=(action "cancel") on-cancel=(action "cancel")
}} }}

View file

@ -6,7 +6,7 @@ const { Component, computed: { sort } } = Ember;
export default Component.extend(SetupMetaData, { export default Component.extend(SetupMetaData, {
strains: null, strains: null,
sortParams: ['fullName'], sortParams: ['sortOrder'],
sortedStrains: sort('strains', 'sortParams'), sortedStrains: sort('strains', 'sortParams'),
}); });

View file

@ -13,7 +13,7 @@
{{#each sortedStrains as |strain|}} {{#each sortedStrains as |strain|}}
<tr> <tr>
<td> <td>
{{#link-to 'protected.strains.show' strain classBinding="data.typeStrain:type-strain"}} {{#link-to 'protected.strains.show' strain classBinding="strain.typeStrain:type-strain"}}
{{strain.fullNameMU}} {{strain.fullNameMU}}
{{/link-to}} {{/link-to}}
</td> </td>

View file

@ -10,6 +10,8 @@ export default Component.extend({
allCharacteristics: null, allCharacteristics: null,
measurement: null, measurement: null,
isDirty: null, isDirty: null,
isNew: false,
isQueued: false,
// Actions // Actions
"save-measurement": null, "save-measurement": null,
@ -22,11 +24,23 @@ export default Component.extend({
notes: null, notes: null,
resetOnInit: Ember.on('init', function() { resetOnInit: Ember.on('init', function() {
this._resetProperties();
}),
_resetProperties: function() {
this.get('propertiesList').forEach((field) => { this.get('propertiesList').forEach((field) => {
const valueInMeasurement = this.get('measurement').get(field); const valueInMeasurement = this.get('measurement').get(field);
this.set(field, valueInMeasurement); this.set(field, valueInMeasurement);
}); });
}), // Read-only attributes
this.set('isNew', this.get('measurement.isNew'));
if (this.get('isNew') && !this.get('isQueued')) {
this.set('isEditing', true);
} else {
this.set('isEditing', false);
}
this.set('isDirty', false);
},
updateField: function(property, value) { updateField: function(property, value) {
this.set(property, value); this.set(property, value);
@ -40,12 +54,22 @@ export default Component.extend({
actions: { actions: {
edit: function() { edit: function() {
this.toggleProperty('isEditing'); this.set('isEditing', true);
}, },
save: function() { save: function() {
this.attrs['save-measurement'](this.get('measurement'), this.getProperties(this.get('propertiesList'))); this.attrs['save-measurement'](this.get('measurement'), this.getProperties(this.get('propertiesList')));
this.toggleProperty('isEditing'); this.set('isQueued', true);
this._resetProperties();
},
cancel: function() {
if (this.get('isNew')) {
this.attrs['delete-measurement'](this.get('measurement'));
} else {
this._resetProperties();
this.set('isEditing', false);
}
}, },
delete: function() { delete: function() {

View file

@ -0,0 +1 @@
{{loading-panel}}

View file

@ -1,5 +1,7 @@
{{#if isEditing}} {{#if isEditing}}
<td></td> <td>
{{{characteristic.characteristicTypeName}}}
</td>
<td> <td>
<select onchange={{action "characteristicDidChange" value="target.value"}}> <select onchange={{action "characteristicDidChange" value="target.value"}}>
{{#each allCharacteristics as |characteristicChoice|}} {{#each allCharacteristics as |characteristicChoice|}}
@ -15,14 +17,13 @@
</td> </td>
{{#if canEdit}} {{#if canEdit}}
<td> <td>
{{#if isDirty}} <button class="button-gray smaller" {{action 'cancel'}}>
<button class="button-green smaller" {{action 'save'}}>
Save
</button>
{{else}}
<button class="button-gray smaller" {{action 'save'}}>
Cancel Cancel
</button> </button>
{{#if isDirty}}
<button class="button-green smaller" {{action 'save'}}>
Save
</button>
{{/if}} {{/if}}
</td> </td>
{{/if}} {{/if}}

View file

@ -5,13 +5,13 @@ const { sort } = computed;
export default Component.extend({ export default Component.extend({
// Passed in // Passed in
strain: null, measurements: null,
allCharacteristics: null, allCharacteristics: null,
canEdit: false, canEdit: false,
canAdd: false, canAdd: false,
// Actions // Actions
"add-characteristic": null, "add-measurement": null,
"save-measurement": null, "save-measurement": null,
"delete-measurement": null, "delete-measurement": null,
@ -19,15 +19,11 @@ export default Component.extend({
sortParams: ['characteristic.characteristicTypeName', 'characteristic.sortOrder', 'characteristic.characteristicName'], sortParams: ['characteristic.characteristicTypeName', 'characteristic.sortOrder', 'characteristic.characteristicName'],
sortAsc: true, sortAsc: true,
paramsChanged: false, paramsChanged: false,
sortedMeasurements: sort('strain.measurements', 'sortParams'), sortedMeasurements: sort('measurements', 'sortParams'),
measurementsPresent: computed('strain.measurements', function() {
return this.get('strain.measurements.length') > 0;
}),
actions: { actions: {
addCharacteristic: function() { addMeasurement: function() {
const newChar = this.attrs['add-characteristic'](); return this.attrs['add-measurement']();
this.get('strain.measurements').addObject(newChar);
}, },
changeSortParam: function(col) { changeSortParam: function(col) {

View file

@ -1,17 +1,16 @@
{{#if canAdd}} {{#if canAdd}}
<br> <br>
<button class="button-green smaller" {{action "addCharacteristic"}}> <button class="button-green smaller" {{action "addMeasurement"}}>
Add characteristic Add measurement
</button> </button>
<br><br> <br><br>
{{/if}} {{/if}}
{{#if measurementsPresent}} {{#if paramsChanged}}
{{#if paramsChanged}} <button class="button-gray smaller" {{action 'resetSortParam'}}>
<button class="button-gray smaller" {{action 'resetSortParam'}}> Reset sort
Reset sort </button>
</button> {{/if}}
{{/if}}
<table class="flakes-table"> <table class="flakes-table">
<colgroup> <colgroup>
{{#if canEdit}} {{#if canEdit}}
@ -48,9 +47,10 @@
allCharacteristics=allCharacteristics allCharacteristics=allCharacteristics
canEdit=canEdit canEdit=canEdit
}} }}
{{else}}
<tr>
<td colspan="5">No Measurements on Record</td>
</tr>
{{/each}} {{/each}}
</tbody> </tbody>
</table> </table>
{{else}}
No measurements on record.
{{/if}}

View file

@ -4,7 +4,7 @@ const { Route } = Ember;
export default Route.extend({ export default Route.extend({
model: function(params) { model: function(params) {
return this.store.findRecord('strain', params.strain_id); return this.store.findRecord('strain', params.strain_id, { reload: true });
}, },
}); });

View file

@ -1,7 +1,7 @@
<div class="span-1"> <div class="span-1">
<fieldset class="flakes-information-box"> <fieldset class="flakes-information-box">
<legend> <legend>
{{strain.strainNameMU}} {{strain-name strain=strain}}
</legend> </legend>
{{! ROW 1 }} {{! ROW 1 }}
@ -10,7 +10,7 @@
<dt>Species</dt> <dt>Species</dt>
<dd> <dd>
{{#link-to 'protected.species.show' strain.species.id}} {{#link-to 'protected.species.show' strain.species.id}}
<em>{{strain.species.speciesNameMU}}</em> <em>{{strain.species.speciesName}}</em>
{{/link-to}} {{/link-to}}
</dd> </dd>
</dl> </dl>
@ -71,11 +71,11 @@
{{! ROW 5 }} {{! ROW 5 }}
<div class="grid-1 gutter-20"> <div class="grid-1 gutter-20">
<dl class="span-1"> <dl class="span-1">
<dt>Characteristics</dt> <dt>Characteristic Measurements</dt>
<dd> <dd>
{{ {{
protected/strains/measurements-table protected/strains/measurements-table
strain=strain measurements=strain.measurements
canEdit=false canEdit=false
canAdd=false canAdd=false
}} }}

View file

@ -1,7 +1,7 @@
import Ember from 'ember'; import Ember from 'ember';
import SetupMetaData from '../../../../mixins/setup-metadata'; import SetupMetaData from '../../../../mixins/setup-metadata';
const { Component } = Ember; const { Component, computed: { sort } } = Ember;
export default Component.extend(SetupMetaData, { export default Component.extend(SetupMetaData, {
// Read-only attributes // Read-only attributes
@ -9,18 +9,22 @@ export default Component.extend(SetupMetaData, {
isNew: null, isNew: null,
isDirty: false, isDirty: false,
speciesList: null, speciesList: null,
allCharacteristics: null, allCharacteristics: [],
updateQueue: [],
deleteQueue: [],
// Actions // Actions
"on-save": null, "on-save": null,
"on-cancel": null, "on-cancel": null,
"on-update": null, "on-update": null,
"add-characteristic": null, "add-measurements": null,
"save-measurement": null,
"delete-measurement": null, // CPs
sortParams: ['sortOrder'],
sortedSpeciesList: sort('speciesList', 'sortParams'),
// Property mapping // Property mapping
propertiesList: ['strainName', 'typeStrain', 'species', 'isolatedFrom', 'accessionNumbers', 'genbank', 'wholeGenomeSequence', 'notes'], propertiesList: ['strainName', 'typeStrain', 'species', 'isolatedFrom', 'accessionNumbers', 'genbank', 'wholeGenomeSequence', 'notes', 'measurements'],
strainName: null, strainName: null,
typeStrain: null, typeStrain: null,
species: null, species: null,
@ -29,15 +33,55 @@ export default Component.extend(SetupMetaData, {
genbank: null, genbank: null,
wholeGenomeSequence: null, wholeGenomeSequence: null,
notes: null, notes: null,
measurements: [],
// Dropdown menu
characteristics: [],
charSortParams: ['characteristicTypeName', 'sortOrder', 'characteristicName'],
sortedCharacteristics: sort('characteristics', 'charSortParams'),
setupCharacteristics: Ember.on('init', function() {
const tempArray = this._resetArray(this.get('allCharacteristics'));
this.set('characteristics', tempArray);
}),
resetOnInit: Ember.on('init', function() { resetOnInit: Ember.on('init', function() {
this._resetProperties();
}),
_resetArray: function(arr) {
let tempArray = [];
arr.forEach((val) => {
if (!val.get('isNew')) {
tempArray.push(val);
}
});
return tempArray;
},
_resetProperties: function() {
// Still some coupling going on here because of adding strain to measurement
this.get('measurements').forEach((val) => {
if (val.get('hasDirtyAttributes')) {
val.rollbackAttributes();
}
if (val.get('isNew')) {
this.get('strain.measurements').removeObject(val);
}
});
this.get('propertiesList').forEach((field) => { this.get('propertiesList').forEach((field) => {
const valueInStrain = this.get('strain').get(field); const valueInStrain = this.get('strain').get(field);
this.set(field, valueInStrain); if (field === 'measurements') {
const tempArray = this._resetArray(valueInStrain);
this.set(field, tempArray);
} else {
this.set(field, valueInStrain);
}
}); });
this.set('updateQueue', []);
this.set('deleteQueue', []);
// Read-only attributes // Read-only attributes
this.set('isNew', this.get('strain.isNew')); this.set('isNew', this.get('strain.isNew'));
}), },
updateField: function(property, value) { updateField: function(property, value) {
this.set(property, value); this.set(property, value);
@ -51,23 +95,32 @@ export default Component.extend(SetupMetaData, {
actions: { actions: {
save: function() { save: function() {
return this.attrs['on-save'](this.getProperties(this.get('propertiesList'))); return this.attrs['on-save'](this.getProperties(this.get('propertiesList')), this.get('deleteQueue'), this.get('updateQueue'));
}, },
cancel: function() { cancel: function() {
this._resetProperties();
return this.attrs['on-cancel'](); return this.attrs['on-cancel']();
}, },
addCharacteristic: function() { addMeasurement: function() {
return this.attrs['add-characteristic'](); const measurement = this.attrs['add-measurement']();
this.get('measurements').pushObject(measurement);
}, },
saveMeasurement: function(measurement, properties) { saveMeasurement: function(measurement, properties) {
return this.attrs['save-measurement'](measurement, properties); measurement.setProperties(properties);
measurement.set('strain', this.get('strain'));
if (!measurement.get('isNew')) {
this.get('updateQueue').pushObject(measurement);
}
this.set('isDirty', true);
}, },
deleteMeasurement: function(measurement) { deleteMeasurement: function(measurement) {
return this.attrs['delete-measurement'](measurement); this.get('deleteQueue').pushObject(measurement);
this.get('measurements').removeObject(measurement);
this.set('isDirty', true);
}, },
strainNameDidChange: function(value) { strainNameDidChange: function(value) {
@ -100,7 +153,7 @@ export default Component.extend(SetupMetaData, {
}, },
notesDidChange: function(value) { notesDidChange: function(value) {
this.updateField('strain.notes', value); this.updateField('notes', value);
}, },
}, },
}); });

View file

@ -15,11 +15,13 @@
<div data-row-span="2"> <div data-row-span="2">
<div data-field-span="2"> <div data-field-span="2">
<label>Species</label> <label>Species</label>
<select onchange={{action "speciesDidChange" value="target.value"}}> {{
{{#each speciesList as |speciesChoice|}} x-select
<option value={{speciesChoice.id}} selected={{equal species.id speciesChoice.id}}>{{speciesChoice.speciesName}}</option> options=sortedSpeciesList
{{/each}} nameAttr='speciesName'
</select> selected=species.id
update=(action "speciesDidChange")
}}
</div> </div>
</div> </div>
<div data-row-span="2"> <div data-row-span="2">
@ -28,7 +30,7 @@
{{text-editor value=isolatedFrom update=(action "isolatedFromDidChange")}} {{text-editor value=isolatedFrom update=(action "isolatedFromDidChange")}}
</div> </div>
</div> </div>
<div data-row-span="3"> <div data-row-span="2">
<div data-field-span="1"> <div data-field-span="1">
<label>Accession Numbers</label> <label>Accession Numbers</label>
{{one-way-input type="text" class="accession-numbers" value=accessionNumbers update=(action "accessionNumbersNameDidChange")}} {{one-way-input type="text" class="accession-numbers" value=accessionNumbers update=(action "accessionNumbersNameDidChange")}}
@ -37,6 +39,8 @@
<label>GenBank</label> <label>GenBank</label>
{{one-way-input type="text" class="genbank" value=genbank update=(action "genbankDidChange")}} {{one-way-input type="text" class="genbank" value=genbank update=(action "genbankDidChange")}}
</div> </div>
</div>
<div data-row-span="1">
<div data-field-span="1"> <div data-field-span="1">
<label>Whole Genome Sequence</label> <label>Whole Genome Sequence</label>
{{one-way-input type="text" class="whole-genome-sequenc" value=wholeGenomeSequence update=(action "wholeGenomeSequenceDidChange")}} {{one-way-input type="text" class="whole-genome-sequenc" value=wholeGenomeSequence update=(action "wholeGenomeSequenceDidChange")}}
@ -49,18 +53,24 @@
</div> </div>
</div> </div>
</fieldset> </fieldset>
<div> {{#if isNew}}
{{ <div>
protected/strains/measurements-table Please save before adding measurements
strain=strain </div>
add-characteristic=(action "addCharacteristic") {{else}}
allCharacteristics=allCharacteristics <div>
save-measurement=(action "saveMeasurement") {{
delete-measurement=(action "deleteMeasurement") protected/strains/measurements-table
canEdit=strain.canEdit measurements=measurements
canAdd=metaData.canAdd add-measurement=(action "addMeasurement")
}} allCharacteristics=sortedCharacteristics
</div> save-measurement=(action "saveMeasurement")
delete-measurement=(action "deleteMeasurement")
canEdit=strain.canEdit
canAdd=metaData.canAdd
}}
</div>
{{/if}}
<br> <br>
<a class="button-red smaller" {{action 'cancel'}}> <a class="button-red smaller" {{action 'cancel'}}>
Cancel Cancel

View file

@ -5,18 +5,23 @@ const { Controller, inject: { service } } = Ember;
export default Controller.extend({ export default Controller.extend({
session: service(), session: service(),
ajax: service(), ajax: service(),
ajaxError: service('ajax-error'),
currentUser: service('session-account'), currentUser: service('session-account'),
actions: { actions: {
save: function(password) { save: function(password) {
const id = this.get('currentUser.account.id'); const id = this.get('currentUser.account.id');
const data = { id: id, password: password }; const data = { id: id, password: password };
this.get('ajax').post('/users/password', { data: data }); this.get('ajax').post('/users/password', { data: data }).then(() => {
this.transitionToRoute('protected.users.show', id); this.transitionToRoute('protected.users.show', id);
this.get('flashMessages').information('Your password has been changed.'); this.get('flashMessages').information('Your password has been changed.');
}, (errors) => {
this.get('ajaxError').alert(errors);
});
}, },
cancel: function() { cancel: function() {
this.get('flashMessages').clearMessages();
this.transitionToRoute('protected.users.show', this.get('currentUser.account.id')); this.transitionToRoute('protected.users.show', this.get('currentUser.account.id'));
}, },
}, },

View file

@ -11,6 +11,8 @@ export default Route.extend({
if (!user.get('isAdmin')) { if (!user.get('isAdmin')) {
this.transitionTo('protected.index'); this.transitionTo('protected.index');
} }
}, () => {
this.transitionTo('protected.index');
}); });
}, },

View file

@ -15,11 +15,13 @@ export default Route.extend({
if (!user.get('isAdmin') && user.get('id') !== user_id) { if (!user.get('isAdmin') && user.get('id') !== user_id) {
this.transitionTo('protected.users.index'); this.transitionTo('protected.users.index');
} }
}, () => {
this.transitionTo('protected.users.index');
}); });
}, },
model: function(params) { model: function(params) {
return this.store.findRecord('user', params.user_id); return this.store.findRecord('user', params.user_id, { reload: true });
}, },
}); });

View file

@ -5,24 +5,41 @@ const { RESTSerializer } = DS;
const { isNone } = Ember; const { isNone } = Ember;
export default RESTSerializer.extend({ export default RESTSerializer.extend({
isNewSerializerAPI: true,
serializeBelongsTo: function(snapshot, json, relationship) { serializeBelongsTo: function(snapshot, json, relationship) {
let key = relationship.key; const key = relationship.key;
const belongsTo = snapshot.belongsTo(key); if (this._canSerialize(key)) {
key = this.keyForRelationship ? this.keyForRelationship(key, "belongsTo", "serialize") : key; const belongsToId = snapshot.belongsTo(key, { id: true });
json[key] = isNone(belongsTo) ? belongsTo : +belongsTo.record.id; let payloadKey = this._getMappedKey(key, snapshot.type);
if (payloadKey === key && this.keyForRelationship) {
payloadKey = this.keyForRelationship(key, "belongsTo", "serialize");
}
if (isNone(belongsToId)) {
json[payloadKey] = null;
} else {
json[payloadKey] = +belongsToId;
}
if (relationship.options.polymorphic) {
this.serializePolymorphicType(snapshot, json, relationship);
}
}
}, },
serializeHasMany: function(snapshot, json, relationship) { serializeHasMany: function(snapshot, json, relationship) {
let key = relationship.key; const key = relationship.key;
const hasMany = snapshot.hasMany(key); if (this._shouldSerializeHasMany(snapshot, key, relationship)) {
key = this.keyForRelationship ? this.keyForRelationship(key, "hasMany", "serialize") : key; const hasMany = snapshot.hasMany(key, { ids: true });
if (hasMany !== undefined) {
json[key] = []; let payloadKey = this._getMappedKey(key, snapshot.type);
hasMany.forEach((item) => { if (payloadKey === key && this.keyForRelationship) {
json[key].push(+item.id); payloadKey = this.keyForRelationship(key, "hasMany", "serialize");
}); }
json[payloadKey] = [];
hasMany.forEach((item) => {
json[payloadKey].push(+item);
});
}
}
}, },
}); });

View file

@ -0,0 +1,19 @@
import Ember from 'ember';
const { Service, inject: { service } } = Ember;
export default Service.extend({
flashMessages: service(),
alert: function(error) {
const flash = this.get('flashMessages');
flash.clearMessages();
window.scrollTo(0,0);
error.errors.forEach((error) => {
console.error(error);
const source = error.source.pointer.split('/');
flash.error(`${source[source.length-1].replace(/([A-Z])/g, ' $1').capitalize()} - ${error.detail}`);
});
}
});

9
app/services/globals.js Normal file
View file

@ -0,0 +1,9 @@
import Ember from 'ember';
import config from '../config/environment';
const { Service } = Ember;
export default Service.extend({
genus: config.APP.genus,
apiURL: config.apiURL,
});

View file

@ -2,19 +2,19 @@
"name": "hymenobacterdotinfo", "name": "hymenobacterdotinfo",
"dependencies": { "dependencies": {
"jquery": "~2.1.1", "jquery": "~2.1.1",
"ember": "1.13.10", "ember": "~2.2.0",
"ember-cli-shims": "0.0.6", "ember-cli-shims": "0.0.6",
"ember-cli-test-loader": "0.2.1", "ember-cli-test-loader": "0.2.1",
"ember-data": "1.13.15", "ember-data": "~2.2.1",
"ember-load-initializers": "0.1.7", "ember-load-initializers": "0.1.7",
"ember-qunit": "0.4.16", "ember-qunit": "0.4.16",
"ember-qunit-notifications": "0.1.0", "ember-qunit-notifications": "0.1.0",
"ember-resolver": "~0.1.20", "ember-resolver": "~0.1.20",
"loader.js": "ember-cli/loader.js#3.2.1", "loader.js": "ember-cli/loader.js#3.4.0",
"qunit": "~1.20.0", "qunit": "~1.20.0",
"flakes": "~1.0.0", "flakes": "~1.0.0",
"moment": "~2.10.6", "moment": "~2.10.6",
"select2": "3.5.2", "select2": "4.0.1-rc.1",
"antiscroll": "git://github.com/azirbel/antiscroll.git#90391fb371c7be769bc32e7287c5271981428356", "antiscroll": "git://github.com/azirbel/antiscroll.git#90391fb371c7be769bc32e7287c5271981428356",
"jquery-mousewheel": "~3.1.4", "jquery-mousewheel": "~3.1.4",
"jquery-ui": "~1.11.4", "jquery-ui": "~1.11.4",

View file

@ -29,7 +29,7 @@ module.exports = function(environment) {
'script-src': "'self'", 'script-src': "'self'",
'font-src': "'self'", 'font-src': "'self'",
'connect-src': "'self'", 'connect-src': "'self'",
'img-src': "'self'", 'img-src': "'self' data:",
'style-src': "'self' 'unsafe-inline'", 'style-src': "'self' 'unsafe-inline'",
'media-src': "'self'" 'media-src': "'self'"
} }
@ -61,7 +61,7 @@ module.exports = function(environment) {
} }
ENV.apiURL = apiURL; ENV.apiURL = apiURL;
ENV.contentSecurityPolicy['connect-src'] = `'self' ${apiURL}`; ENV.contentSecurityPolicy['connect-src'] = "'self' " + apiURL;
return ENV; return ENV;
}; };

View file

@ -14,6 +14,8 @@ module.exports = function(defaults) {
// quill // quill
app.import('bower_components/quill/dist/quill.base.css'); app.import('bower_components/quill/dist/quill.base.css');
app.import('bower_components/quill/dist/quill.snow.css'); app.import('bower_components/quill/dist/quill.snow.css');
// select2
app.import('bower_components/select2/dist/css/select2.min.css');
// LIBS //////////////////////////////////////////////////////////////////////// // LIBS ////////////////////////////////////////////////////////////////////////
// flakes (and deps) // flakes (and deps)
@ -25,6 +27,8 @@ module.exports = function(defaults) {
app.import('bower_components/moment/moment.js'); app.import('bower_components/moment/moment.js');
// quill // quill
app.import('bower_components/quill/dist/quill.min.js'); app.import('bower_components/quill/dist/quill.min.js');
// select2
app.import('bower_components/select2/dist/js/select2.full.min.js');
return app.toTree(); return app.toTree();
}; };

13051
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,11 @@
"scripts": { "scripts": {
"build": "ember build", "build": "ember build",
"start": "ember server", "start": "ember server",
"test": "ember test" "test": "ember test",
"bower": "bower",
"ember": "ember",
"firebase": "firebase",
"deployProd": "firebase deploy -e prod"
}, },
"repository": "", "repository": "",
"engines": { "engines": {
@ -20,7 +24,7 @@
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"broccoli-asset-rev": "^2.2.0", "broccoli-asset-rev": "^2.2.0",
"ember-cli": "1.13.11", "ember-cli": "1.13.13",
"ember-cli-app-version": "^1.0.0", "ember-cli-app-version": "^1.0.0",
"ember-cli-babel": "^5.1.5", "ember-cli-babel": "^5.1.5",
"ember-cli-content-security-policy": "0.4.0", "ember-cli-content-security-policy": "0.4.0",
@ -33,13 +37,16 @@
"ember-cli-mirage": "0.1.11", "ember-cli-mirage": "0.1.11",
"ember-cli-qunit": "^1.0.4", "ember-cli-qunit": "^1.0.4",
"ember-cli-release": "0.2.8", "ember-cli-release": "0.2.8",
"ember-cli-sri": "^1.1.0", "ember-cli-sri": "^1.2.0",
"ember-cli-uglify": "^1.2.0", "ember-cli-uglify": "^1.2.0",
"ember-data": "1.13.15", "ember-data": "~2.2.1",
"ember-disable-proxy-controllers": "^1.0.1", "ember-disable-proxy-controllers": "^1.0.1",
"ember-export-application-global": "^1.0.4", "ember-export-application-global": "^1.0.4",
"ember-one-way-input": "0.1.3", "ember-one-way-input": "0.1.3",
"ember-select-2": "1.3.0", "ember-simple-auth": "1.0.1"
"ember-simple-auth": "1.0.0" },
"dependencies": {
"bower": "^1.8.8",
"firebase-tools": "^7.12.1"
} }
} }

View file

@ -6,10 +6,10 @@ import { authenticateSession } from '../helpers/ember-simple-auth';
module('Acceptance | characteristics', { module('Acceptance | characteristics', {
beforeEach: function() { beforeEach: function() {
this.application = startApp(); this.application = startApp();
server.create('users', { role: 'A', canEdit: true, sub: 1 });
authenticateSession(this.application, { authenticateSession(this.application, {
access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiYWN0ZGIiLCJzdWIiOiIxIiwiZXhwIjoxNDQ2NTAyMjI2LCJpYXQiOjE0NDY0OTg2MjZ9.vIjKHAsp2TkCV505EbtCo2xQT-2oQkB-Nv5y0b6E7Mg" access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiYWN0ZGIiLCJzdWIiOiIxIiwiZXhwIjoxNDQ2NTAyMjI2LCJpYXQiOjE0NDY0OTg2MjZ9.vIjKHAsp2TkCV505EbtCo2xQT-2oQkB-Nv5y0b6E7Mg"
}); });
server.create('users', { role: 'A', canEdit: true });
}, },
afterEach: function() { afterEach: function() {
@ -70,3 +70,22 @@ test('creating /characteristics/new', function(assert) {
}); });
}); });
}); });
test('deleting /characteristics/:id', function(assert) {
const characteristic = server.create('characteristics', { 'canEdit': true });
visit(`/characteristics/${characteristic.id}`);
andThen(function() {
assert.equal(currentURL(), `/characteristics/${characteristic.id}`);
click('button.delete');
andThen(function() {
click('button.delete-confirm');
andThen(function() {
assert.equal(currentURL(), `/characteristics`);
assert.equal(server.db.characteristics.length, 0);
});
});
});
});

View file

@ -6,10 +6,10 @@ import { authenticateSession } from '../helpers/ember-simple-auth';
module('Acceptance | species', { module('Acceptance | species', {
beforeEach: function() { beforeEach: function() {
this.application = startApp(); this.application = startApp();
server.create('users', { role: 'A', canEdit: true, sub: 1 });
authenticateSession(this.application, { authenticateSession(this.application, {
access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiYWN0ZGIiLCJzdWIiOiIxIiwiZXhwIjoxNDQ2NTAyMjI2LCJpYXQiOjE0NDY0OTg2MjZ9.vIjKHAsp2TkCV505EbtCo2xQT-2oQkB-Nv5y0b6E7Mg" access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiYWN0ZGIiLCJzdWIiOiIxIiwiZXhwIjoxNDQ2NTAyMjI2LCJpYXQiOjE0NDY0OTg2MjZ9.vIjKHAsp2TkCV505EbtCo2xQT-2oQkB-Nv5y0b6E7Mg"
}); });
server.create('users', { role: 'A', canEdit: true });
}, },
afterEach: function() { afterEach: function() {
@ -69,3 +69,22 @@ test('creating /species/new', function(assert) {
}); });
}); });
}); });
test('deleting /species/:id', function(assert) {
const species = server.create('species', { 'canEdit': true });
visit(`/species/${species.id}`);
andThen(function() {
assert.equal(currentURL(), `/species/${species.id}`);
click('button.delete');
andThen(function() {
click('button.delete-confirm');
andThen(function() {
assert.equal(currentURL(), `/species`);
assert.equal(server.db.species.length, 0);
});
});
});
});

View file

@ -6,10 +6,10 @@ import { authenticateSession } from '../helpers/ember-simple-auth';
module('Acceptance | strains', { module('Acceptance | strains', {
beforeEach: function() { beforeEach: function() {
this.application = startApp(); this.application = startApp();
server.create('users', { role: 'A', canEdit: true, sub: 1 });
authenticateSession(this.application, { authenticateSession(this.application, {
access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiYWN0ZGIiLCJzdWIiOiIxIiwiZXhwIjoxNDQ2NTAyMjI2LCJpYXQiOjE0NDY0OTg2MjZ9.vIjKHAsp2TkCV505EbtCo2xQT-2oQkB-Nv5y0b6E7Mg" access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiYWN0ZGIiLCJzdWIiOiIxIiwiZXhwIjoxNDQ2NTAyMjI2LCJpYXQiOjE0NDY0OTg2MjZ9.vIjKHAsp2TkCV505EbtCo2xQT-2oQkB-Nv5y0b6E7Mg"
}); });
server.create('users', { role: 'A', canEdit: true });
}, },
afterEach: function() { afterEach: function() {
@ -74,3 +74,23 @@ test('creating /strains/new', function(assert) {
}); });
}); });
}); });
test('deleting /strains/:id', function(assert) {
const species = server.create('species');
const strain = server.create('strains', { canEdit: true , species: species.id });
visit(`/strains/${strain.id}`);
andThen(function() {
assert.equal(currentURL(), `/strains/${strain.id}`);
click('button.delete');
andThen(function() {
click('button.delete-confirm');
andThen(function() {
assert.equal(currentURL(), `/strains`);
assert.equal(server.db.strains.length, 0);
});
});
});
});

View file

@ -6,10 +6,10 @@ import { invalidateSession, authenticateSession } from '../helpers/ember-simple-
module('Acceptance | users', { module('Acceptance | users', {
beforeEach: function() { beforeEach: function() {
this.application = startApp(); this.application = startApp();
server.create('users', { role: 'A', canEdit: true, sub: 1 });
authenticateSession(this.application, { authenticateSession(this.application, {
access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiYWN0ZGIiLCJzdWIiOiIxIiwiZXhwIjoxNDQ2NTAyMjI2LCJpYXQiOjE0NDY0OTg2MjZ9.vIjKHAsp2TkCV505EbtCo2xQT-2oQkB-Nv5y0b6E7Mg" access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiYWN0ZGIiLCJzdWIiOiIxIiwiZXhwIjoxNDQ2NTAyMjI2LCJpYXQiOjE0NDY0OTg2MjZ9.vIjKHAsp2TkCV505EbtCo2xQT-2oQkB-Nv5y0b6E7Mg"
}); });
server.create('users', { role: 'A', canEdit: true });
}, },
afterEach: function() { afterEach: function() {

View file

@ -1,23 +0,0 @@
import Ember from 'ember';
import { initialize } from '../../../initializers/global-variables';
import { module, test } from 'qunit';
var container, application;
module('Unit | Initializer | global variables', {
beforeEach: function() {
Ember.run(function() {
application = Ember.Application.create();
container = application.__container__;
application.deferReadiness();
});
}
});
// Replace this with your real tests.
test('it works', function(assert) {
initialize(container, application);
// you would normally confirm the results of the initializer here
assert.ok(true);
});