Compare commits

...
This repository has been archived on 2025-03-30. You can view files and clone it, but cannot push or open issues or pull requests.

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);
});