Merge pull request #52 from thermokarst/refactor-strains

Refactor strains
This commit is contained in:
Matthew Dillon 2015-11-11 12:36:56 -07:00
commit 7497bacc4b
33 changed files with 690 additions and 439 deletions

View file

@ -24,4 +24,19 @@ export function testConfig() {
this.post('/characteristics');
this.get('/characteristics/:id');
this.put('/characteristics/:id');
this.get('/strains', function(db /*, request*/) {
return {
strains: db.strains,
species: db.species,
};
});
this.post('/strains');
this.get('/strains/:id', function(db, request) {
return {
strain: db.strains.find(request.params.id),
species: db.species, // Just send back everything we've got
};
});
this.put('/strains/:id');
}

View file

@ -0,0 +1,17 @@
import Mirage, { faker } from 'ember-cli-mirage';
export default Mirage.Factory.extend({
measurements: [],
characteristics: [],
species: 0,
strainName() { return faker.lorem.words().join(' '); },
typeStrain: faker.random.boolean(),
accessionNumbers() { return faker.lorem.words().join(' '); },
genbank() { return faker.lorem.words().join(' '); },
wholeGenomeSequence() { return faker.lorem.words().join(' '); },
isolatedFrom: faker.lorem.sentences(),
notes: faker.lorem.sentences(),
totalMeasurements: 0,
sortOrder: faker.random.number(),
canEdit: faker.random.boolean(),
});

View file

@ -13,17 +13,12 @@ export default Mixin.create({
const fallbackRoute = this.get('fallbackRouteSave');
model.setProperties(properties);
if (model.get('hasDirtyAttributes')) {
model.save().then((model) => {
this.get('flashMessages').clearMessages();
this.transitionToRoute(fallbackRoute, model);
}, () => {
ajaxError(model.get('errors'), this.get('flashMessages'));
});
} else {
this.transitionToRoute(fallbackRoute, model);
}
},
cancel: function() {

View file

@ -25,6 +25,10 @@ export default DS.Model.extend({
return Ember.String.htmlSafe(`${this.get('strainName')}${type}`);
}.property('strainName', 'typeStrain').readOnly(),
fullName: Ember.computed('species', 'strainName', function() {
return `${this.get('species.speciesName')} ${this.get('strainNameMU')}`;
}),
fullNameMU: function() {
return Ember.String.htmlSafe(`<em>${this.get('species.speciesName')}</em> ${this.get('strainNameMU')}`);
}.property('species', 'strainNameMU').readOnly(),

View file

@ -1,12 +1,12 @@
import Ember from 'ember';
import SetupMetaData from '../../../../../mixins/setup-metadata';
const { Component } = Ember;
const { Component, computed: { sort } } = Ember;
export default Component.extend(SetupMetaData, {
species: null,
sortParams: ['speciesName', 'strainCount'],
sortedSpecies: Ember.computed.sort('species', 'sortParams'),
sortedSpecies: sort('species', 'sortParams'),
});

View file

@ -1,31 +1,36 @@
import Ember from 'ember';
import SaveModel from '../../../../mixins/save-model';
import ajaxError from '../../../../utils/ajax-error';
export default Ember.Controller.extend({
actions: {
save: function() {
let strain = this.get('strain');
const { Controller } = Ember;
if (strain.get('hasDirtyAttributes')) {
strain.save().then((strain) => {
this.transitionToRoute('protected.strains.show', strain);
}, () => {
ajaxError(strain.get('errors'), this.get('flashMessages'));
export default Controller.extend(SaveModel, {
// Required for SaveModel mixin
fallbackRouteSave: 'protected.strains.show',
fallbackRouteCancel: 'protected.strains.show',
actions: {
addCharacteristic: function() {
return this.store.createRecord('measurement', {
characteristic: this.store.createRecord('characteristic', { sortOrder: -999 }),
});
} else {
strain.destroyRecord().then(() => {
this.transitionToRoute('protected.strains.show', strain);
});
}
},
cancel: function() {
let strain = this.get('strain');
saveMeasurement: function(measurement, properties) {
measurement.setProperties(properties);
measurement.save().then(() => {
this.get('flashMessages').clearMessages();
}, () => {
ajaxError(measurement.get('errors'), this.get('flashMessages'));
});
},
strain.get('errors').clear();
strain.rollbackAttributes();
this.transitionToRoute('protected.strains.show', strain);
deleteMeasurement: function(measurement) {
const characteristic = measurement.get('characteristic');
if (characteristic.get('isNew')) {
characteristic.destroyRecord();
}
measurement.destroyRecord();
},
},

View file

@ -1,35 +1,44 @@
import Ember from 'ember';
import ElevatedAccess from '../../../../mixins/elevated-access';
export default Ember.Route.extend({
currentUser: Ember.inject.service('session-account'),
const { Route } = Ember;
beforeModel: function(transition) {
this._super(transition);
this.get('currentUser.account').then((user) => {
if (user.get('isReader')) {
this.transitionTo('protected.strains.index');
}
});
},
export default Route.extend(ElevatedAccess, {
// Required for ElevatedAccess mixin
fallbackRouteBefore: 'protected.strains.index',
fallbackRouteAfter: 'protected.strains.show',
model: function(params) {
return Ember.RSVP.hash({
strain: this.store.find('strain', params.strain_id),
strain: this.store.findRecord('strain', params.strain_id),
species: this.store.findAll('species'), // Need for dropdown
characteristics: this.store.findAll('characteristic'), // Need for dropdown
});
},
// Overriding afterModel because of RSVP hash
afterModel: function(models) {
if (!models.strain.get('canEdit')) {
this.transitionTo('strains.show', models.strain.get('id'));
if (!models.strain.get('isNew') && !models.strain.get('canEdit')) {
this.transitionTo(this.get('fallbackRouteAfter'), models.strain.get('id'));
}
},
// Setting up controller because of RSVP hash
setupController: function(controller, models) {
controller.setProperties(models);
this.get('currentUser.account').then((user) => {
controller.set('metaData', user.get('metaData'));
});
controller.set('model', models.strain);
controller.set('speciesList', models.species);
controller.set('allCharacteristics', models.characteristics);
},
actions: {
// Overriding willTransition because of RSVP hash
willTransition: function(/*transition*/) {
const controller = this.get('controller');
const model = controller.get('model');
if (model.get('isNew')) {
model.destroyRecord();
}
},
},
});

View file

@ -1,8 +1,11 @@
{{
protected/strains/strain-form
strain=strain
species=species
canAdd=metaData.canAdd
save="save"
cancel="cancel"
strain=model
speciesList=speciesList
add-characteristic=(action "addCharacteristic")
allCharacteristics=allCharacteristics
save-measurement=(action "saveMeasurement")
delete-measurement=(action "deleteMeasurement")
on-save=(action "save")
on-cancel=(action "cancel")
}}

View file

@ -1,6 +0,0 @@
import Ember from 'ember';
export default Ember.Controller.extend({
sortParams: ['sortOrder'],
sortedStrains: Ember.computed.sort('model', 'sortParams'),
});

View file

@ -1,17 +1,10 @@
import Ember from 'ember';
export default Ember.Route.extend({
currentUser: Ember.inject.service('session-account'),
const { Route } = Ember;
export default Route.extend({
model: function() {
return this.store.findAll('strain');
},
setupController: function(controller, model) {
controller.set('model', model);
this.get('currentUser.account').then((user) => {
controller.set('metaData', user.get('metaData'));
});
},
});

View file

@ -0,0 +1,12 @@
import Ember from 'ember';
import SetupMetaData from '../../../../../mixins/setup-metadata';
const { Component, computed: { sort } } = Ember;
export default Component.extend(SetupMetaData, {
strains: null,
sortParams: ['fullName'],
sortedStrains: sort('strains', 'sortParams'),
});

View file

@ -0,0 +1,26 @@
<h3 id="total-strains">Total strains: {{strains.length}}</h3>
{{add-button label="Add Strain" link="protected.strains.new" canAdd=metaData.canAdd}}
<table class="flakes-table">
<thead>
<tr>
<th>Species</th>
<th>Total Measurements</th>
</tr>
</thead>
<tbody>
{{#each sortedStrains as |strain|}}
<tr>
<td>
{{#link-to 'protected.strains.show' strain classBinding="data.typeStrain:type-strain"}}
{{strain.fullNameMU}}
{{/link-to}}
</td>
<td>
{{strain.totalMeasurements}}
</td>
</tr>
{{/each}}
</tbody>
</table>

View file

@ -1,27 +1,6 @@
<h2>{{genus-name}} Strains</h2>
<h3>Total strains: {{model.length}}</h3>
{{add-button label="Add Strain" link="protected.strains.new" canAdd=metaData.canAdd}}
<table class="flakes-table">
<thead>
<tr>
<th>Species</th>
<th>Total Measurements</th>
</tr>
</thead>
<tbody>
{{#each sortedStrains as |row|}}
<tr>
<td>
{{#link-to 'protected.strains.show' row classBinding="data.typeStrain:type-strain"}}
{{row.fullNameMU}}
{{/link-to}}
</td>
<td>
{{row.totalMeasurements}}
</td>
</tr>
{{/each}}
</tbody>
</table>
{{
protected/strains/index/strain-table
strains=model
}}

View file

@ -0,0 +1,69 @@
import Ember from 'ember';
const { Component } = Ember;
export default Component.extend({
tagName: 'tr',
// Read-only attributes
isEditing: false,
allCharacteristics: null,
measurement: null,
isDirty: null,
// Actions
"save-measurement": null,
"delete-measurement": null,
// Property mapping
propertiesList: ['characteristic', 'value', 'notes'],
characteristic: null,
value: null,
notes: null,
resetOnInit: Ember.on('init', function() {
this.get('propertiesList').forEach((field) => {
const valueInMeasurement = this.get('measurement').get(field);
this.set(field, valueInMeasurement);
});
}),
updateField: function(property, value) {
this.set(property, value);
// Manually compare against passed in value
if (this.get('measurement').get(property) !== value) {
this.set('isDirty', true);
} else {
this.set('isDirty', false);
}
},
actions: {
edit: function() {
this.toggleProperty('isEditing');
},
save: function() {
this.attrs['save-measurement'](this.get('measurement'), this.getProperties(this.get('propertiesList')));
this.toggleProperty('isEditing');
},
delete: function() {
this.attrs['delete-measurement'](this.get('measurement'));
},
characteristicDidChange: function(value) {
const newCharacteristic = this.get('allCharacteristics').findBy('id', value);
this.updateField('characteristic', newCharacteristic);
},
valueDidChange: function(value) {
this.updateField('value', value);
},
notesDidChange: function(value) {
this.updateField('notes', value);
},
},
});

View file

@ -0,0 +1,52 @@
{{#if isEditing}}
<td></td>
<td>
<select onchange={{action "characteristicDidChange" value="target.value"}}>
{{#each allCharacteristics as |characteristicChoice|}}
<option value={{characteristicChoice.id}} selected={{equal characteristic.id characteristicChoice.id}}>{{characteristicChoice.characteristicName}}</option>
{{/each}}
</select>
</td>
<td>
{{one-way-input type="text" class="measurement-value" value=value update=(action "valueDidChange")}}
</td>
<td>
{{one-way-input type="text" class="measurement-notes" value=notes update=(action "notesDidChange")}}
</td>
{{#if canEdit}}
<td>
{{#if isDirty}}
<button class="button-green smaller" {{action 'save'}}>
Save
</button>
{{else}}
<button class="button-gray smaller" {{action 'save'}}>
Cancel
</button>
{{/if}}
</td>
{{/if}}
{{else}}
<td>
{{{measurement.characteristic.characteristicTypeName}}}
</td>
<td>
{{#link-to 'protected.characteristics.show' measurement.characteristic.id}}
{{{measurement.characteristic.characteristicName}}}
{{/link-to}}
</td>
<td>
{{measurement.value}}
</td>
<td>
{{measurement.notes}}
</td>
{{#if canEdit}}
<td>
<button class="button-gray smaller" {{action 'edit'}}>
Edit
</button>
{{delete-button delete=(action 'delete')}}
</td>
{{/if}}
{{/if}}

View file

@ -0,0 +1,55 @@
import Ember from 'ember';
const { Component, computed } = Ember;
const { sort } = computed;
export default Component.extend({
// Passed in
strain: null,
allCharacteristics: null,
canEdit: false,
canAdd: false,
// Actions
"add-characteristic": null,
"save-measurement": null,
"delete-measurement": null,
// Properties
sortParams: ['characteristic.characteristicTypeName', 'characteristic.sortOrder', 'characteristic.characteristicName'],
sortAsc: true,
paramsChanged: false,
sortedMeasurements: sort('strain.measurements', 'sortParams'),
measurementsPresent: computed('strain.measurements', function() {
return this.get('strain.measurements.length') > 0;
}),
actions: {
addCharacteristic: function() {
const newChar = this.attrs['add-characteristic']();
this.get('strain.measurements').addObject(newChar);
},
changeSortParam: function(col) {
const sort = this.get('sortAsc') ? 'asc' : 'desc';
this.set('sortParams', [`${col}:${sort}`]);
this.set('paramsChanged', true);
this.toggleProperty('sortAsc');
},
resetSortParam: function() {
this.set('sortParams', ['characteristic.characteristicTypeName', 'characteristic.sortOrder', 'characteristic.characteristicName']);
this.set('paramsChanged', false);
this.set('sortAsc', true);
},
saveMeasurement: function(measurement, properties) {
return this.attrs['save-measurement'](measurement, properties);
},
deleteMeasurement: function(measurement) {
return this.attrs['delete-measurement'](measurement);
},
},
});

View file

@ -1,17 +1,17 @@
{{#if canAdd}}
<br>
<button class="button-green smaller" {{action "addCharacteristic"}}>
<br>
<button class="button-green smaller" {{action "addCharacteristic"}}>
Add characteristic
</button>
<br><br>
</button>
<br><br>
{{/if}}
{{#if measurementsPresent}}
{{#if paramsChanged}}
{{#if paramsChanged}}
<button class="button-gray smaller" {{action 'resetSortParam'}}>
Reset sort
</button>
{{/if}}
{{/if}}
<table class="flakes-table">
<colgroup>
{{#if canEdit}}
@ -41,8 +41,11 @@
<tbody>
{{#each sortedMeasurements as |measurement|}}
{{
protected/strains/show/measurements-table-row
row=measurement
protected/strains/measurements-table-row
measurement=measurement
save-measurement=(action "saveMeasurement")
delete-measurement=(action "deleteMeasurement")
allCharacteristics=allCharacteristics
canEdit=canEdit
}}
{{/each}}

View file

@ -1,29 +1,10 @@
import Ember from 'ember';
import ajaxError from '../../../../utils/ajax-error';
import SaveModel from '../../../../mixins/save-model';
export default Ember.Controller.extend({
actions: {
save: function() {
let strain = this.get('strain');
const { Controller } = Ember;
if (strain.get('hasDirtyAttributes')) {
strain.save().then((strain) => {
this.transitionToRoute('protected.strains.show', strain);
}, () => {
ajaxError(strain.get('errors'), this.get('flashMessages'));
});
} else {
strain.destroyRecord().then(() => {
this.transitionToRoute('protected.strains.index');
});
}
},
cancel: function() {
this.get('strain').destroyRecord().then(() => {
this.transitionToRoute('protected.strains.index');
});
},
},
export default Controller.extend(SaveModel, {
// Required for SaveModel mixin
fallbackRouteSave: 'protected.strains.show',
fallbackRouteCancel: 'protected.strains.index',
});

View file

@ -1,16 +1,12 @@
import Ember from 'ember';
import ElevatedAccess from '../../../../mixins/elevated-access';
export default Ember.Route.extend({
currentUser: Ember.inject.service('session-account'),
const { Route } = Ember;
beforeModel: function(transition) {
this._super(transition);
this.get('currentUser.account').then((user) => {
if (user.get('isReader')) {
this.transitionTo('protected.strains.index');
}
});
},
export default Route.extend(ElevatedAccess, {
// Required for ElevatedAccess mixin
fallbackRouteBefore: 'protected.strains.index',
fallbackRouteAfter: 'protected.strains.show',
model: function() {
return Ember.RSVP.hash({
@ -19,19 +15,28 @@ export default Ember.Route.extend({
});
},
// Overriding afterModel because of RSVP hash
afterModel: function(models) {
if (!models.strain.get('isNew') && !models.strain.get('canEdit')) {
this.transitionTo(this.get('fallbackRouteAfter'), models.strain.get('id'));
}
},
// Setting up controller because of RSVP hash
setupController: function(controller, models) {
controller.setProperties(models);
controller.set('model', models.strain);
controller.set('speciesList', models.species);
},
actions: {
// Overriding willTransition because of RSVP hash
willTransition: function(/*transition*/) {
const controller = this.get('controller');
const strain = controller.get('strain');
const model = controller.get('model');
if (strain.get('isNew')) {
strain.destroyRecord();
if (model.get('isNew')) {
model.destroyRecord();
}
},
},
});

View file

@ -1,7 +1,7 @@
{{
protected/strains/strain-form
strain=strain
species=species
save="save"
cancel="cancel"
strain=model
speciesList=speciesList
on-save=(action "save")
on-cancel=(action "cancel")
}}

View file

@ -1,12 +1,9 @@
import Ember from 'ember';
import DeleteModel from '../../../../mixins/delete-model';
export default Ember.Controller.extend({
actions: {
delete: function() {
this.get('model').destroyRecord().then(() => {
this.transitionToRoute('protected.strains.index');
});
},
},
const { Controller } = Ember;
export default Controller.extend(DeleteModel, {
// Required for DeleteModel mixin
transitionRoute: 'protected.strains.index',
});

View file

@ -1,47 +0,0 @@
import Ember from 'ember';
import ajaxError from '../../../../../utils/ajax-error';
export default Ember.Component.extend({
tagName: 'tr',
isEditing: false,
oldCharacteristicId: function() {
let json = this.get('row').toJSON();
return json.characteristic;
}.property(),
rowChanged: Ember.computed('row.notes', 'row.value', 'row.characteristic.id', function() {
return this.get('row.hasDirtyAttributes') ||
this.get('oldCharacteristicId') !== this.get('row.characteristic.id');
}),
actions: {
edit: function() {
// The parent table fetches all of the characteristics ahead of time
this.set('characteristics', this.store.peekAll('characteristic'));
this.toggleProperty('isEditing');
},
save: function() {
if (this.get('rowChanged')) {
this.get('row').save().then(() => {
this.get('flashMessages').clearMessages();
this.toggleProperty('isEditing');
}, () => {
ajaxError(this.get('row.errors'), this.get('flashMessages'));
});
} else {
this.toggleProperty('isEditing');
}
},
delete: function() {
let char = this.get('row.characteristic');
if (char.get('isNew')) {
char.destroyRecord();
}
this.get('row').destroyRecord();
}
},
});

View file

@ -1,54 +0,0 @@
{{#if isEditing}}
<td></td>
<td>
{{
select-2
multiple=false
content=characteristics
value=row.characteristic
optionLabelPath="characteristicName"
}}
</td>
<td>
{{input value=row.value}}
</td>
<td>
{{input value=row.notes}}
</td>
{{#if canEdit}}
<td>
{{#if rowChanged}}
<button class="button-green smaller" {{action 'save'}}>
Save
</button>
{{else}}
<button class="button-gray smaller" {{action 'save'}}>
Cancel
</button>
{{/if}}
</td>
{{/if}}
{{else}}
<td>
{{{row.characteristic.characteristicTypeName}}}
</td>
<td>
{{#link-to 'protected.characteristics.show' row.characteristic.id}}
{{{row.characteristic.characteristicName}}}
{{/link-to}}
</td>
<td>
{{row.value}}
</td>
<td>
{{row.notes}}
</td>
{{#if canEdit}}
<td>
<button class="button-gray smaller" {{action 'edit'}}>
Edit
</button>
{{delete-button delete=(action 'delete')}}
</td>
{{/if}}
{{/if}}

View file

@ -1,47 +0,0 @@
import Ember from 'ember';
export default Ember.Component.extend({
measurementsPresent: function() {
return this.get('model.measurements.length') > 0;
}.property('model.measurements'),
fetchCharacteristics: function() {
if (this.get('canEdit')) {
this.store.findAll('characteristic');
}
}.on('didInsertElement'),
sortParams: ['characteristic.characteristicTypeName', 'characteristic.sortOrder', 'characteristic.characteristicName'],
sortAsc: true,
paramsChanged: false,
sortedMeasurements: Ember.computed.sort('model.measurements', 'sortParams'),
actions: {
addCharacteristic: function() {
const c = this.store.createRecord('characteristic', {
sortOrder: -999
});
const m = this.store.createRecord('measurement', {
characteristic: c
});
this.get('model.measurements').addObject(m);
},
changeSortParam: function(col) {
let sort = this.get('sortAsc') ? 'asc' : 'desc';
let sortCol = `${col}:${sort}`;
this.set('sortParams', [sortCol]);
this.set('paramsChanged', true);
this.toggleProperty('sortAsc');
return false;
},
resetSortParam: function() {
this.set('sortParams', ['characteristic.characteristicTypeName', 'characteristic.sortOrder', 'characteristic.characteristicName']);
this.set('paramsChanged', false);
this.set('sortAsc', true);
return false;
},
},
});

View file

@ -1,8 +1,10 @@
import Ember from 'ember';
export default Ember.Route.extend({
const { Route } = Ember;
export default Route.extend({
model: function(params) {
return this.store.findRecord('strain', params.strain_id, { reload: true });
return this.store.findRecord('strain', params.strain_id);
},
});

View file

@ -0,0 +1,14 @@
import Ember from 'ember';
const { Component } = Ember;
export default Component.extend({
strain: null,
"on-delete": null,
actions: {
deleteStrain: function() {
return this.attrs['on-delete']();
},
},
});

View file

@ -0,0 +1,105 @@
<div class="span-1">
<fieldset class="flakes-information-box">
<legend>
{{strain.strainNameMU}}
</legend>
{{! ROW 1 }}
<div class="grid-2 gutter-20">
<dl class="span-1">
<dt>Species</dt>
<dd>
{{#link-to 'protected.species.show' strain.species.id}}
<em>{{strain.species.speciesNameMU}}</em>
{{/link-to}}
</dd>
</dl>
<dl class="span-1">
<dt>Type Strain?</dt>
<dd>
{{if strain.typeStrain 'Yes' 'No'}}
</dd>
</dl>
</div>
{{! ROW 2 }}
<div class="grid-3 gutter-20">
<dl class="span-1">
<dt>Accession Numbers</dt>
<dd>
{{strain.accessionNumbers}}
</dd>
</dl>
<dl class="span-1">
<dt>Genbank</dt>
<dd>
{{genbank-url genbank=strain.genbank}}
</dd>
</dl>
<dl class="span-1">
<dt>Whole Genome Sequence</dt>
<dd>
{{genbank-url genbank=strain.wholeGenomeSequence}}
</dd>
</dl>
</div>
{{! ROW 3 }}
<div class="grid-1 gutter-20">
<dl class="span-1">
<dt>Isolated From</dt>
<dd>
{{{strain.isolatedFrom}}}
</dd>
</dl>
</div>
{{! ROW 4 }}
<div class="grid-1 gutter-20">
<dl class="span-1">
<dt>Notes</dt>
<dd>
{{#if strain.notes}}
{{{strain.notes}}}
{{else}}
No notes.
{{/if}}
</dd>
</dl>
</div>
{{! ROW 5 }}
<div class="grid-1 gutter-20">
<dl class="span-1">
<dt>Characteristics</dt>
<dd>
{{
protected/strains/measurements-table
strain=strain
canEdit=false
canAdd=false
}}
</dd>
</dl>
</div>
{{! ROW 6 }}
<div class="grid-2 gutter-20">
<dl class="span-1">
<dt>Record Created</dt>
<dd>{{null-time strain.createdAt 'LL'}}</dd>
</dl>
<dl class="span-1">
<dt>Record Updated</dt>
<dd>{{null-time strain.updatedAt 'LL'}}</dd>
</dl>
</div>
</fieldset>
</div>
{{#if strain.canEdit}}
<br>
{{#link-to 'protected.strains.edit' strain.id class="button-gray smaller"}}
Edit
{{/link-to}}
{{delete-button delete=(action 'deleteStrain')}}
{{/if}}

View file

@ -1,105 +1,5 @@
<div class="span-1">
<fieldset class="flakes-information-box">
<legend>
Strain {{model.strainNameMU}}
</legend>
{{! ROW 1 }}
<div class="grid-2 gutter-20">
<dl class="span-1">
<dt>Species</dt>
<dd>
{{#link-to 'protected.species.show' model.species.id}}
<em>{{model.species.speciesNameMU}}</em>
{{/link-to}}
</dd>
</dl>
<dl class="span-1">
<dt>Type Strain?</dt>
<dd>
{{if model.typeStrain 'Yes' 'No'}}
</dd>
</dl>
</div>
{{! ROW 2 }}
<div class="grid-3 gutter-20">
<dl class="span-1">
<dt>Accession Numbers</dt>
<dd>
{{model.accessionNumbers}}
</dd>
</dl>
<dl class="span-1">
<dt>Genbank</dt>
<dd>
{{genbank-url genbank=model.genbank}}
</dd>
</dl>
<dl class="span-1">
<dt>Whole Genome Sequence</dt>
<dd>
{{genbank-url genbank=model.wholeGenomeSequence}}
</dd>
</dl>
</div>
{{! ROW 3 }}
<div class="grid-1 gutter-20">
<dl class="span-1">
<dt>Isolated From</dt>
<dd>
{{{model.isolatedFrom}}}
</dd>
</dl>
</div>
{{! ROW 4 }}
<div class="grid-1 gutter-20">
<dl class="span-1">
<dt>Notes</dt>
<dd>
{{#if model.notes}}
{{{model.notes}}}
{{else}}
No notes.
{{/if}}
</dd>
</dl>
</div>
{{! ROW 5 }}
<div class="grid-1 gutter-20">
<dl class="span-1">
<dt>Characteristics</dt>
<dd>
{{
protected/strains/show/measurements-table
model=model
canEdit=false
canAdd=false
}}
</dd>
</dl>
</div>
{{! ROW 6 }}
<div class="grid-2 gutter-20">
<dl class="span-1">
<dt>Record Created</dt>
<dd>{{null-time model.createdAt 'LL'}}</dd>
</dl>
<dl class="span-1">
<dt>Record Updated</dt>
<dd>{{null-time model.updatedAt 'LL'}}</dd>
</dl>
</div>
</fieldset>
</div>
{{#if model.canEdit}}
<br>
{{#link-to 'protected.strains.edit' model.id class="button-gray smaller"}}
Edit
{{/link-to}}
{{delete-button delete=(action 'delete')}}
{{/if}}
{{
protected/strains/show/strain-card
strain=model
on-delete=(action 'delete')
}}

View file

@ -1,21 +1,106 @@
import Ember from 'ember';
import SetupMetaData from '../../../../mixins/setup-metadata';
const { Component } = Ember;
export default Component.extend(SetupMetaData, {
// Read-only attributes
strain: null,
isNew: null,
isDirty: false,
speciesList: null,
allCharacteristics: null,
// Actions
"on-save": null,
"on-cancel": null,
"on-update": null,
"add-characteristic": null,
"save-measurement": null,
"delete-measurement": null,
// Property mapping
propertiesList: ['strainName', 'typeStrain', 'species', 'isolatedFrom', 'accessionNumbers', 'genbank', 'wholeGenomeSequence', 'notes'],
strainName: null,
typeStrain: null,
species: null,
isolatedFrom: null,
accessionNumbers: null,
genbank: null,
wholeGenomeSequence: null,
notes: null,
resetOnInit: Ember.on('init', function() {
this.get('propertiesList').forEach((field) => {
const valueInStrain = this.get('strain').get(field);
this.set(field, valueInStrain);
});
// Read-only attributes
this.set('isNew', this.get('strain.isNew'));
}),
updateField: function(property, value) {
this.set(property, value);
// Manually compare against passed in value
if (this.get('strain').get(property) !== value) {
this.set('isDirty', true);
} else {
this.set('isDirty', false);
}
},
export default Ember.Component.extend({
actions: {
save: function() {
this.sendAction('save');
return this.attrs['on-save'](this.getProperties(this.get('propertiesList')));
},
cancel: function() {
this.sendAction('cancel');
return this.attrs['on-cancel']();
},
addCharacteristic: function() {
return this.attrs['add-characteristic']();
},
saveMeasurement: function(measurement, properties) {
return this.attrs['save-measurement'](measurement, properties);
},
deleteMeasurement: function(measurement) {
return this.attrs['delete-measurement'](measurement);
},
strainNameDidChange: function(value) {
this.updateField('strainName', value);
},
typeStrainDidChange: function() {
this.updateField('typeStrain', !this.get('typeStrain'));
},
speciesDidChange: function(value) {
const newSpecies = this.get('speciesList').findBy('id', value);
this.updateField('species', newSpecies);
},
isolatedFromDidChange: function(value) {
this.set('strain.isolatedFrom', value);
this.updateField('isolatedFrom', value);
},
accessionNumbersNameDidChange: function(value) {
this.updateField('accessionNumbers', value);
},
genbankDidChange: function(value) {
this.updateField('genbank', value);
},
wholeGenomeSequenceDidChange: function(value) {
this.updateField('wholeGenomeSequence', value);
},
notesDidChange: function(value) {
this.set('strain.notes', value);
this.updateField('strain.notes', value);
},
},
});

View file

@ -1,68 +1,72 @@
<form class="grid-form" {{action 'save' on='submit'}}>
<fieldset>
<legend><em>{{strain.strainName}}</em></legend>
<legend><em>{{strainName}}</em></legend>
<div data-row-span="2">
<div data-field-span="1">
<label>Strain Name</label>
{{input value=strain.strainName}}
{{one-way-input type="text" class="strain-name" value=strainName update=(action "strainNameDidChange")}}
</div>
<div data-field-span="1">
<label>Type Strain?</label>
{{input type="checkbox" checked=strain.typeStrain}} {{if strain.typeStrain 'Yes' 'No'}}
<input type="checkbox" checked={{typeStrain}} value="{{typeStrain}}" onchange={{action "typeStrainDidChange"}}>
{{if typeStrain 'Yes' 'No'}}
</div>
</div>
<div data-row-span="2">
<div data-field-span="2">
<label>Species</label>
{{
select-2
content=species
optionLabelPath="speciesName"
value=strain.species
}}
<select onchange={{action "speciesDidChange" value="target.value"}}>
{{#each speciesList as |speciesChoice|}}
<option value={{speciesChoice.id}} selected={{equal species.id speciesChoice.id}}>{{speciesChoice.speciesName}}</option>
{{/each}}
</select>
</div>
</div>
<div data-row-span="2">
<div data-field-span="2">
<label>Isolated From</label>
{{text-editor value=strain.isolatedFrom update=(action "isolatedFromDidChange")}}
{{text-editor value=isolatedFrom update=(action "isolatedFromDidChange")}}
</div>
</div>
<div data-row-span="3">
<div data-field-span="1">
<label>Accession Numbers</label>
{{input value=strain.accessionNumbers}}
{{one-way-input type="text" class="accession-numbers" value=accessionNumbers update=(action "accessionNumbersNameDidChange")}}
</div>
<div data-field-span="1">
<label>GenBank</label>
{{input value=strain.genbank}}
{{one-way-input type="text" class="genbank" value=genbank update=(action "genbankDidChange")}}
</div>
<div data-field-span="1">
<label>Whole Genome Sequence</label>
{{input value=strain.wholeGenomeSequence}}
{{one-way-input type="text" class="whole-genome-sequenc" value=wholeGenomeSequence update=(action "wholeGenomeSequenceDidChange")}}
</div>
</div>
<div data-row-span="2">
<div data-field-span="2">
<label>Notes</label>
{{text-editor value=strain.notes update=(action "notesDidChange")}}
{{text-editor value=notes update=(action "notesDidChange")}}
</div>
</div>
</fieldset>
<div>
{{
protected/strains/show/measurements-table
model=strain
protected/strains/measurements-table
strain=strain
add-characteristic=(action "addCharacteristic")
allCharacteristics=allCharacteristics
save-measurement=(action "saveMeasurement")
delete-measurement=(action "deleteMeasurement")
canEdit=strain.canEdit
canAdd=canAdd
canAdd=metaData.canAdd
}}
</div>
<br>
<a class="button-red smaller" {{action 'cancel'}}>
Cancel
</a>
{{#if strain.hasDirtyAttributes}}
<button type="submit" class="button-green smaller">
{{#if isDirty}}
<button type="submit" class="button-green smaller save-strain">
Save
</button>
{{/if}}

View file

@ -0,0 +1,76 @@
import Ember from 'ember';
import { module, test } from 'qunit';
import startApp from '../helpers/start-app';
import { authenticateSession } from '../helpers/ember-simple-auth';
module('Acceptance | strains', {
beforeEach: function() {
this.application = startApp();
authenticateSession(this.application, {
access_token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJiYWN0ZGIiLCJzdWIiOiIxIiwiZXhwIjoxNDQ2NTAyMjI2LCJpYXQiOjE0NDY0OTg2MjZ9.vIjKHAsp2TkCV505EbtCo2xQT-2oQkB-Nv5y0b6E7Mg"
});
server.create('users', { role: 'A', canEdit: true });
},
afterEach: function() {
Ember.run(this.application, 'destroy');
}
});
test('visiting /strains', function(assert) {
const species = server.create('species');
const strains = server.createList('strains', 20, { species: species.id });
visit('/strains');
andThen(function() {
assert.equal(currentURL(), '/strains');
assert.equal(find(".flakes-table > tbody > tr").length, strains.length);
assert.equal(find("#total-strains").text(), "Total strains: 20");
});
});
test('visiting /strains/:id', function(assert) {
const species = server.create('species');
const strain = server.create('strains', { species: species.id });
visit(`/strains/${strain.id}`);
andThen(function() {
assert.equal(currentURL(), `/strains/${strain.id}`);
const typeStrain = strain.typeStrain ? 'T' : '';
assert.equal(find(".flakes-information-box > legend").text().trim(), `${strain.strainName}${typeStrain}`);
});
});
test('editing /strains/:id/edit', function(assert) {
const species = server.create('species');
const strain = server.create('strains', { canEdit: true , species: species.id });
visit(`/strains/${strain.id}/edit`);
andThen(function() {
assert.equal(currentURL(), `/strains/${strain.id}/edit`);
fillIn('.strain-name', 'Revised Strain Name');
click('.save-strain');
andThen(function() {
assert.equal(currentURL(), `/strains/${strain.id}`);
const typeStrain = strain.typeStrain ? 'T' : '';
assert.equal(find(".flakes-information-box > legend").text().trim(), `Revised Strain Name${typeStrain}`);
});
});
});
test('creating /strains/new', function(assert) {
visit(`/strains/new`);
andThen(function() {
assert.equal(currentURL(), `/strains/new`);
fillIn('.strain-name', 'New Strain Name');
click('.save-strain');
andThen(function() {
assert.equal(find(".flakes-information-box > legend").text().trim(), `New Strain Name`);
assert.equal(server.db.strains.length, 1);
});
});
});