Commit c9f77a31 authored by Florian Hübsch's avatar Florian Hübsch
Browse files

Update Collection API, add treeview for shared collections. In

more detail:

- add authentication to API
- fetch collections in API by current_user.id
- fetch roots and shared_roots separately
parent 641f8081
...@@ -16,20 +16,24 @@ ...@@ -16,20 +16,24 @@
# Available Seeds # Available Seeds
A user is seeded with email `test@ninjaconcept.com` and password `ninjaconcept`. Currently 3 users are seeded with respective email `test@ninjaconcept.com`, `hattori@ninjaconcept.com`, `momochi@ninjaconcept.com`, and password `ninjaconcept` (for all 3 the same).
# API (v1) # API (v1)
## Collections ## Collections
* Get serialized collection roots * Get serialized, unshared collection roots for current user
`/api/v1/collections/roots` `/api/v1/collections/roots`
* Get serialized, shared collection roots for current user
`/api/v1/collections/shared_roots`
* Get serialized samples by collection id * Get serialized samples by collection id
`/api/v1/collections/:collection_id/samples` `/api/v1/collections/:collection_id/samples`
* Get serialized sample by id * Get serialized sample by id
`/api/v1/samples/:id` `/api/v1/samples/:id`
...@@ -4,6 +4,26 @@ class API < Grape::API ...@@ -4,6 +4,26 @@ class API < Grape::API
format :json format :json
formatter :json, Grape::Formatter::ActiveModelSerializers formatter :json, Grape::Formatter::ActiveModelSerializers
# TODO needs to be tested,
# source: http://funonrails.com/2014/03/api-authentication-using-devise-token/
helpers do
def warden
env['warden']
end
def current_user
@current_user = warden.user
end
def authenticate!
error!('401 Unauthorized', 401) unless current_user
end
end
before do
authenticate!
end
mount Chemotion::CollectionAPI mount Chemotion::CollectionAPI
mount Chemotion::SampleAPI mount Chemotion::SampleAPI
end end
module Chemotion module Chemotion
class CollectionAPI < Grape::API class CollectionAPI < Grape::API
resource :collections do resource :collections do
desc "Return all serialized collection roots" desc "Return all unshared serialized collection roots"
get :roots do get :roots do
Collection.roots current_user.collections.unshared.roots
end end
desc "Return all shared serialized collection roots"
get :shared_roots do
current_user.collections.shared.roots
end
# TODO add authorization/authentication, e.g. is current_user allowed
# to fetch this samples?
desc "Return serialized samples for given collection id" desc "Return serialized samples for given collection id"
params do params do
requires :id, type: Integer, desc: "Collection id" requires :id, type: Integer, desc: "Collection id"
......
...@@ -14,7 +14,7 @@ export default class CollectionTree extends React.Component { ...@@ -14,7 +14,7 @@ export default class CollectionTree extends React.Component {
componentDidMount() { componentDidMount() {
CollectionStore.listen(this.onChange.bind(this)); CollectionStore.listen(this.onChange.bind(this));
CollectionActions.fetchCollections(); CollectionActions.fetchUnsharedCollectionRoots();
} }
componentWillUnmount() { componentWillUnmount() {
...@@ -39,8 +39,14 @@ export default class CollectionTree extends React.Component { ...@@ -39,8 +39,14 @@ export default class CollectionTree extends React.Component {
render() { render() {
return ( return (
<div className="tree-wrapper"> <div>
{this.subtrees()} <div className="tree-wrapper">
{this.subtrees()}
</div>
Shared
<div className="tree-wrapper">
{this.subtrees()}
</div>
</div> </div>
) )
} }
......
...@@ -2,10 +2,17 @@ import alt from '../alt'; ...@@ -2,10 +2,17 @@ import alt from '../alt';
import CollectionsFetcher from '../fetchers/CollectionsFetcher'; import CollectionsFetcher from '../fetchers/CollectionsFetcher';
class CollectionActions { class CollectionActions {
// TODO #1 unify naming: fetchCollections or fetchCollectionRoots?
// or do we need both?
// TODO #2...centralized error handling maybe ErrorActions? // TODO #2...centralized error handling maybe ErrorActions?
fetchCollections() { fetchUnsharedCollectionRoots() {
CollectionsFetcher.fetchRoots()
.then((roots) => {
this.dispatch(roots);
}).catch((errorMessage) => {
console.log(errorMessage);
});
}
fetchSharedCollectionRoots() {
CollectionsFetcher.fetchRoots() CollectionsFetcher.fetchRoots()
.then((roots) => { .then((roots) => {
this.dispatch(roots); this.dispatch(roots);
......
...@@ -2,7 +2,9 @@ import 'whatwg-fetch'; ...@@ -2,7 +2,9 @@ import 'whatwg-fetch';
export default class CollectionsFetcher { export default class CollectionsFetcher {
static fetchRoots() { static fetchRoots() {
let promise = fetch('/api/v1/collections/roots.json') let promise = fetch('/api/v1/collections/roots.json', {
credentials: 'same-origin'
})
.then((response) => { .then((response) => {
return response.json() return response.json()
}).then((json) => { }).then((json) => {
......
...@@ -3,7 +3,9 @@ import 'whatwg-fetch'; ...@@ -3,7 +3,9 @@ import 'whatwg-fetch';
// TODO: SamplesFetcher also updates Samples and so on...naming? // TODO: SamplesFetcher also updates Samples and so on...naming?
export default class SamplesFetcher { export default class SamplesFetcher {
static fetchById(id) { static fetchById(id) {
let promise = fetch('/api/v1/samples/' + id + '.json') let promise = fetch('/api/v1/samples/' + id + '.json', {
credentials: 'same-origin'
})
.then((response) => { .then((response) => {
return response.json() return response.json()
}).then((json) => { }).then((json) => {
...@@ -16,7 +18,9 @@ export default class SamplesFetcher { ...@@ -16,7 +18,9 @@ export default class SamplesFetcher {
} }
static fetchByCollectionId(id) { static fetchByCollectionId(id) {
let promise = fetch('/api/v1/collections/' + id + '/samples.json') let promise = fetch('/api/v1/collections/' + id + '/samples.json', {
credentials: 'same-origin'
})
.then((response) => { .then((response) => {
return response.json() return response.json()
}).then((json) => { }).then((json) => {
...@@ -30,6 +34,7 @@ export default class SamplesFetcher { ...@@ -30,6 +34,7 @@ export default class SamplesFetcher {
static update(paramObj) { static update(paramObj) {
let promise = fetch('/api/v1/samples/' + paramObj.id, { let promise = fetch('/api/v1/samples/' + paramObj.id, {
credentials: 'same-origin',
method: 'put', method: 'put',
headers: { headers: {
'Accept': 'application/json', 'Accept': 'application/json',
......
...@@ -8,7 +8,7 @@ class CollectionStore { ...@@ -8,7 +8,7 @@ class CollectionStore {
}; };
this.bindListeners({ this.bindListeners({
handleFetchCollections: CollectionActions.fetchCollections handleFetchCollections: CollectionActions.fetchUnsharedCollectionRoots
}) })
} }
......
...@@ -5,6 +5,7 @@ class Collection < ActiveRecord::Base ...@@ -5,6 +5,7 @@ class Collection < ActiveRecord::Base
has_many :collections_samples has_many :collections_samples
has_many :samples, through: :collections_samples has_many :samples, through: :collections_samples
scope :unshared, -> { where(is_shared: false) }
scope :shared, -> { where(is_shared: true) } scope :shared, -> { where(is_shared: true) }
def is_all_collection? def is_all_collection?
......
...@@ -3,4 +3,6 @@ class User < ActiveRecord::Base ...@@ -3,4 +3,6 @@ class User < ActiveRecord::Base
# :confirmable, :lockable, :timeoutable and :omniauthable # :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable, devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable :recoverable, :rememberable, :trackable, :validatable
has_many :collections
end end
# create initial test user # create initial test user
u = User.create!(email: 'test@ninjaconcept.com', password: 'ninjaconcept', password_confirmation: 'ninjaconcept') u = User.create!(email: 'test@ninjaconcept.com', password: 'ninjaconcept', password_confirmation: 'ninjaconcept')
hattori = User.create!(email: 'hattori@ninjaconcept.com', password: 'ninjaconcept', password_confirmation: 'ninjaconcept')
momochi = User.create!(email: 'momochi@ninjaconcept.com', password: 'ninjaconcept', password_confirmation: 'ninjaconcept')
# create some collections # create some collections
all_collection = Collection.create!(label: 'All', user_id: u.id) all_collection = Collection.create!(label: 'All', user_id: u.id)
...@@ -15,6 +17,10 @@ reaction_4 = Reaction.create(name: 'Reaction 4') ...@@ -15,6 +17,10 @@ reaction_4 = Reaction.create(name: 'Reaction 4')
reaction_5 = Reaction.create(name: 'Reaction 5') reaction_5 = Reaction.create(name: 'Reaction 5')
reaction_6 = Reaction.create(name: 'Reaction 6') reaction_6 = Reaction.create(name: 'Reaction 6')
# create some shared collections
shared_collection_1 = Collection.create!(is_shared: true, label: 'My project with Hattori', user_id: hattori.id)
shared_collection_2 = Collection.create!(is_shared: true, label: 'My project with Momochi', user_id: momochi.id)
# create some samples # create some samples
sample_1 = Sample.create!(name: 'Sample 1') sample_1 = Sample.create!(name: 'Sample 1')
sample_2 = Sample.create!(name: 'Sample 2') sample_2 = Sample.create!(name: 'Sample 2')
...@@ -46,6 +52,10 @@ CollectionsSample.create!(sample: sample_1, collection: grand_child) ...@@ -46,6 +52,10 @@ CollectionsSample.create!(sample: sample_1, collection: grand_child)
CollectionsSample.create!(sample: sample_7, collection: grand_child) CollectionsSample.create!(sample: sample_7, collection: grand_child)
CollectionsSample.create!(sample: sample_8, collection: grand_child) CollectionsSample.create!(sample: sample_8, collection: grand_child)
CollectionsSample.create!(sample: sample_1, collection: shared_collection_1)
CollectionsSample.create!(sample: sample_2, collection: shared_collection_2)
CollectionsSample.create!(sample: sample_3, collection: shared_collection_2)
# associate samples with reactions # associate samples with reactions
CollectionsReaction.create!(reaction: reaction_1, collection: collection_1) CollectionsReaction.create!(reaction: reaction_1, collection: collection_1)
CollectionsReaction.create!(reaction: reaction_2, collection: collection_1) CollectionsReaction.create!(reaction: reaction_2, collection: collection_1)
...@@ -53,3 +63,4 @@ CollectionsReaction.create!(reaction: reaction_3, collection: collection_1) ...@@ -53,3 +63,4 @@ CollectionsReaction.create!(reaction: reaction_3, collection: collection_1)
CollectionsReaction.create!(reaction: reaction_4, collection: grand_child) CollectionsReaction.create!(reaction: reaction_4, collection: grand_child)
CollectionsReaction.create!(reaction: reaction_5, collection: subcollection_1) CollectionsReaction.create!(reaction: reaction_5, collection: subcollection_1)
CollectionsReaction.create!(reaction: reaction_6, collection: subcollection_1) CollectionsReaction.create!(reaction: reaction_6, collection: subcollection_1)
...@@ -10,13 +10,19 @@ RSpec.describe Collection, type: :model do ...@@ -10,13 +10,19 @@ RSpec.describe Collection, type: :model do
end end
describe 'scopes' do describe 'scopes' do
describe 'shared scope' do let(:collection_1) { create(:collection, is_shared: false) }
let(:collection_1) { create(:collection, is_shared: false) } let(:collection_2) { create(:collection, is_shared: true) }
let(:collection_2) { create(:collection, is_shared: true) }
describe 'shared scope' do
it 'returns shared collections' do it 'returns shared collections' do
expect(Collection.shared).to match_array [collection_2] expect(Collection.shared).to match_array [collection_2]
end end
end end
describe 'unshared scope' do
it 'returns unshared collections' do
expect(Collection.unshared).to match_array [collection_1]
end
end
end end
end end
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment