From db591f9c7ffe8b0f4d6d19f57954bdd57cf35d15 Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Tue, 11 Dec 2018 15:03:54 +1000 Subject: [PATCH 01/11] Added citizen science models and migrations --- app/models/dataset.rb | 1 + app/models/dataset_item.rb | 1 + app/models/question.rb | 15 + app/models/response.rb | 18 ++ app/models/study.rb | 20 ++ db/migrate/20181210052707_create_studies.rb | 17 ++ db/migrate/20181210052725_create_questions.rb | 22 ++ db/migrate/20181210052735_create_responses.rb | 19 ++ db/structure.sql | 256 +++++++++++++++++- spec/factories/questions.rb | 5 + spec/factories/responses.rb | 5 + spec/factories/studies.rb | 5 + spec/models/question_spec.rb | 5 + spec/models/response_spec.rb | 5 + spec/models/study_spec.rb | 5 + 15 files changed, 397 insertions(+), 2 deletions(-) create mode 100644 app/models/question.rb create mode 100644 app/models/response.rb create mode 100644 app/models/study.rb create mode 100644 db/migrate/20181210052707_create_studies.rb create mode 100644 db/migrate/20181210052725_create_questions.rb create mode 100644 db/migrate/20181210052735_create_responses.rb create mode 100644 spec/factories/questions.rb create mode 100644 spec/factories/responses.rb create mode 100644 spec/factories/studies.rb create mode 100644 spec/models/question_spec.rb create mode 100644 spec/models/response_spec.rb create mode 100644 spec/models/study_spec.rb diff --git a/app/models/dataset.rb b/app/models/dataset.rb index 95f6a6f6..abb2b10b 100644 --- a/app/models/dataset.rb +++ b/app/models/dataset.rb @@ -7,6 +7,7 @@ class Dataset < ActiveRecord::Base belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_datasets belongs_to :updater, class_name: 'User', foreign_key: :updater_id, inverse_of: :updated_datasets has_many :dataset_items + has_many :study # We have not enabled soft deletes yet since we do not support deleting datasets # This may change in the future diff --git a/app/models/dataset_item.rb b/app/models/dataset_item.rb index 928118cd..1ff12762 100644 --- a/app/models/dataset_item.rb +++ b/app/models/dataset_item.rb @@ -8,6 +8,7 @@ class DatasetItem < ActiveRecord::Base belongs_to :audio_recording, inverse_of: :dataset_items belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_dataset_items has_many :progress_events, inverse_of: :dataset_item, dependent: :destroy + has_many :responses # We have not enabled soft deletes yet since we do not support deleting dataset items # This may change in the future diff --git a/app/models/question.rb b/app/models/question.rb new file mode 100644 index 00000000..8e780bfa --- /dev/null +++ b/app/models/question.rb @@ -0,0 +1,15 @@ +class Question < ActiveRecord::Base + + # ensures that creator_id, updater_id, deleter_id are set + include UserChange + + #relationships + belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_questions + belongs_to :updater, class_name: 'User', foreign_key: :updater_id, inverse_of: :updated_questions + has_and_belongs_to_many :study + has_many :responses + + # association validations + validates :creator, existence: true + +end diff --git a/app/models/response.rb b/app/models/response.rb new file mode 100644 index 00000000..176fed18 --- /dev/null +++ b/app/models/response.rb @@ -0,0 +1,18 @@ +class Response < ActiveRecord::Base + + # ensures that creator_id, updater_id, deleter_id are set + include UserChange + + #relationships + belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_responses + belongs_to :question + belongs_to :study + belongs_to :dataset_item + + # association validations + validates :creator, existence: true + validates :question, existence: true + validates :study, existence: true + validates :dataset_item, existence: true + +end diff --git a/app/models/study.rb b/app/models/study.rb new file mode 100644 index 00000000..45ce3436 --- /dev/null +++ b/app/models/study.rb @@ -0,0 +1,20 @@ +class Study < ActiveRecord::Base + + # ensures that creator_id, updater_id, deleter_id are set + include UserChange + + #relationships + belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_studies + belongs_to :updater, class_name: 'User', foreign_key: :updater_id, inverse_of: :updated_studies + has_and_belongs_to_many :questions + belongs_to :dataset + has_many :responses + + # association validations + validates :creator, existence: true + validates :question, existence: true + validates :study, existence: true + validates :dataset_item, existence: true + + +end diff --git a/db/migrate/20181210052707_create_studies.rb b/db/migrate/20181210052707_create_studies.rb new file mode 100644 index 00000000..a0fcfa3a --- /dev/null +++ b/db/migrate/20181210052707_create_studies.rb @@ -0,0 +1,17 @@ +class CreateStudies < ActiveRecord::Migration + def change + create_table :studies do |t| + + t.integer :creator_id + t.integer :updater_id + t.integer :dataset_id + t.string :name + t.timestamps null: false + end + + add_foreign_key :studies, :datasets + add_foreign_key :studies, :users, column: :creator_id + add_foreign_key :studies, :users, column: :updater_id + + end +end diff --git a/db/migrate/20181210052725_create_questions.rb b/db/migrate/20181210052725_create_questions.rb new file mode 100644 index 00000000..5096595f --- /dev/null +++ b/db/migrate/20181210052725_create_questions.rb @@ -0,0 +1,22 @@ +class CreateQuestions < ActiveRecord::Migration + def change + create_table :questions do |t| + t.integer :creator_id + t.integer :updater_id + t.text :text + t.text :data + t.timestamps null: false + end + + create_table :questions_studies, id: false do |t| + t.integer :question_id, :null => false + t.integer :study_id, :null => false + end + + add_foreign_key :questions, :users, column: :creator_id + add_foreign_key :questions, :users, column: :updater_id + add_foreign_key :questions_studies, :questions + add_foreign_key :questions_studies, :studies + + end +end diff --git a/db/migrate/20181210052735_create_responses.rb b/db/migrate/20181210052735_create_responses.rb new file mode 100644 index 00000000..b7738f50 --- /dev/null +++ b/db/migrate/20181210052735_create_responses.rb @@ -0,0 +1,19 @@ +class CreateResponses < ActiveRecord::Migration + def change + create_table :responses do |t| + + t.integer :creator_id + t.integer :dataset_item_id + t.integer :question_id + t.integer :study_id + + t.text :data + t.timestamps null: false + end + + add_foreign_key :responses, :dataset_items + add_foreign_key :responses, :questions + add_foreign_key :responses, :studies + add_foreign_key :responses, :users, column: :creator_id + end +end diff --git a/db/structure.sql b/db/structure.sql index 29a770ca..978eae25 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -2,8 +2,8 @@ -- PostgreSQL database dump -- --- Dumped from database version 9.3.20 --- Dumped by pg_dump version 10.1 +-- Dumped from database version 9.3.21 +-- Dumped by pg_dump version 10.2 (Ubuntu 10.2-1.pgdg14.04+1) SET statement_timeout = 0; SET lock_timeout = 0; @@ -520,6 +520,85 @@ CREATE TABLE projects_sites ( ); +-- +-- Name: questions; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE questions ( + id integer NOT NULL, + creator_id integer, + updater_id integer, + text text, + data text, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: questions_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE questions_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: questions_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE questions_id_seq OWNED BY questions.id; + + +-- +-- Name: questions_studies; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE questions_studies ( + question_id integer NOT NULL, + study_id integer NOT NULL +); + + +-- +-- Name: responses; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE responses ( + id integer NOT NULL, + creator_id integer, + dataset_item_id integer, + question_id integer, + study_id integer, + data text, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: responses_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE responses_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: responses_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE responses_id_seq OWNED BY responses.id; + + -- -- Name: saved_searches; Type: TABLE; Schema: public; Owner: - -- @@ -649,6 +728,40 @@ CREATE SEQUENCE sites_id_seq ALTER SEQUENCE sites_id_seq OWNED BY sites.id; +-- +-- Name: studies; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE studies ( + id integer NOT NULL, + creator_id integer, + updater_id integer, + dataset_id integer, + name character varying, + created_at timestamp without time zone NOT NULL, + updated_at timestamp without time zone NOT NULL +); + + +-- +-- Name: studies_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE studies_id_seq + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: studies_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE studies_id_seq OWNED BY studies.id; + + -- -- Name: tag_groups; Type: TABLE; Schema: public; Owner: - -- @@ -861,6 +974,20 @@ ALTER TABLE ONLY progress_events ALTER COLUMN id SET DEFAULT nextval('progress_e ALTER TABLE ONLY projects ALTER COLUMN id SET DEFAULT nextval('projects_id_seq'::regclass); +-- +-- Name: questions id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY questions ALTER COLUMN id SET DEFAULT nextval('questions_id_seq'::regclass); + + +-- +-- Name: responses id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY responses ALTER COLUMN id SET DEFAULT nextval('responses_id_seq'::regclass); + + -- -- Name: saved_searches id; Type: DEFAULT; Schema: public; Owner: - -- @@ -882,6 +1009,13 @@ ALTER TABLE ONLY scripts ALTER COLUMN id SET DEFAULT nextval('scripts_id_seq'::r ALTER TABLE ONLY sites ALTER COLUMN id SET DEFAULT nextval('sites_id_seq'::regclass); +-- +-- Name: studies id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY studies ALTER COLUMN id SET DEFAULT nextval('studies_id_seq'::regclass); + + -- -- Name: tag_groups id; Type: DEFAULT; Schema: public; Owner: - -- @@ -999,6 +1133,22 @@ ALTER TABLE ONLY projects ADD CONSTRAINT projects_pkey PRIMARY KEY (id); +-- +-- Name: questions questions_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY questions + ADD CONSTRAINT questions_pkey PRIMARY KEY (id); + + +-- +-- Name: responses responses_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY responses + ADD CONSTRAINT responses_pkey PRIMARY KEY (id); + + -- -- Name: saved_searches saved_searches_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1023,6 +1173,14 @@ ALTER TABLE ONLY sites ADD CONSTRAINT sites_pkey PRIMARY KEY (id); +-- +-- Name: studies studies_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY studies + ADD CONSTRAINT studies_pkey PRIMARY KEY (id); + + -- -- Name: tag_groups tag_groups_pkey; Type: CONSTRAINT; Schema: public; Owner: - -- @@ -1767,6 +1925,14 @@ ALTER TABLE ONLY progress_events ADD CONSTRAINT fk_rails_15ea2f07e1 FOREIGN KEY (dataset_item_id) REFERENCES dataset_items(id); +-- +-- Name: questions fk_rails_1b78df6070; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY questions + ADD CONSTRAINT fk_rails_1b78df6070 FOREIGN KEY (updater_id) REFERENCES users(id); + + -- -- Name: tag_groups fk_rails_1ba11222e1; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1775,6 +1941,46 @@ ALTER TABLE ONLY tag_groups ADD CONSTRAINT fk_rails_1ba11222e1 FOREIGN KEY (tag_id) REFERENCES tags(id); +-- +-- Name: questions fk_rails_21f8d26270; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY questions + ADD CONSTRAINT fk_rails_21f8d26270 FOREIGN KEY (creator_id) REFERENCES users(id); + + +-- +-- Name: responses fk_rails_325af149a3; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY responses + ADD CONSTRAINT fk_rails_325af149a3 FOREIGN KEY (question_id) REFERENCES questions(id); + + +-- +-- Name: studies fk_rails_41770507e5; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY studies + ADD CONSTRAINT fk_rails_41770507e5 FOREIGN KEY (dataset_id) REFERENCES datasets(id); + + +-- +-- Name: studies fk_rails_4362b81edd; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY studies + ADD CONSTRAINT fk_rails_4362b81edd FOREIGN KEY (updater_id) REFERENCES users(id); + + +-- +-- Name: responses fk_rails_51009e83c9; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY responses + ADD CONSTRAINT fk_rails_51009e83c9 FOREIGN KEY (study_id) REFERENCES studies(id); + + -- -- Name: analysis_jobs_items fk_rails_522df5cc92; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1791,6 +1997,22 @@ ALTER TABLE ONLY dataset_items ADD CONSTRAINT fk_rails_5bf6548424 FOREIGN KEY (creator_id) REFERENCES users(id); +-- +-- Name: questions_studies fk_rails_6a5ffa3b4f; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY questions_studies + ADD CONSTRAINT fk_rails_6a5ffa3b4f FOREIGN KEY (study_id) REFERENCES studies(id); + + +-- +-- Name: responses fk_rails_7a62c4269f; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY responses + ADD CONSTRAINT fk_rails_7a62c4269f FOREIGN KEY (dataset_item_id) REFERENCES dataset_items(id); + + -- -- Name: dataset_items fk_rails_81ed124069; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1807,6 +2029,22 @@ ALTER TABLE ONLY analysis_jobs_items ADD CONSTRAINT fk_rails_86f75840f2 FOREIGN KEY (analysis_job_id) REFERENCES analysis_jobs(id); +-- +-- Name: responses fk_rails_a7a3c29a3c; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY responses + ADD CONSTRAINT fk_rails_a7a3c29a3c FOREIGN KEY (creator_id) REFERENCES users(id); + + +-- +-- Name: studies fk_rails_a94a68aa0b; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY studies + ADD CONSTRAINT fk_rails_a94a68aa0b FOREIGN KEY (creator_id) REFERENCES users(id); + + -- -- Name: datasets fk_rails_c2337cbe35; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -1815,6 +2053,14 @@ ALTER TABLE ONLY datasets ADD CONSTRAINT fk_rails_c2337cbe35 FOREIGN KEY (updater_id) REFERENCES users(id); +-- +-- Name: questions_studies fk_rails_c7ae81b3ab; Type: FK CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY questions_studies + ADD CONSTRAINT fk_rails_c7ae81b3ab FOREIGN KEY (question_id) REFERENCES questions(id); + + -- -- Name: dataset_items fk_rails_c97bdfad35; Type: FK CONSTRAINT; Schema: public; Owner: - -- @@ -2101,3 +2347,9 @@ INSERT INTO schema_migrations (version) VALUES ('20160726014747'); INSERT INTO schema_migrations (version) VALUES ('20180118002015'); +INSERT INTO schema_migrations (version) VALUES ('20181210052707'); + +INSERT INTO schema_migrations (version) VALUES ('20181210052725'); + +INSERT INTO schema_migrations (version) VALUES ('20181210052735'); + diff --git a/spec/factories/questions.rb b/spec/factories/questions.rb new file mode 100644 index 00000000..571d4190 --- /dev/null +++ b/spec/factories/questions.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :question do + + end +end diff --git a/spec/factories/responses.rb b/spec/factories/responses.rb new file mode 100644 index 00000000..bfe3171f --- /dev/null +++ b/spec/factories/responses.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :response do + + end +end diff --git a/spec/factories/studies.rb b/spec/factories/studies.rb new file mode 100644 index 00000000..d7551b1d --- /dev/null +++ b/spec/factories/studies.rb @@ -0,0 +1,5 @@ +FactoryGirl.define do + factory :study do + + end +end diff --git a/spec/models/question_spec.rb b/spec/models/question_spec.rb new file mode 100644 index 00000000..5e95bcf8 --- /dev/null +++ b/spec/models/question_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Question, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/response_spec.rb b/spec/models/response_spec.rb new file mode 100644 index 00000000..aeb51d18 --- /dev/null +++ b/spec/models/response_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Response, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/spec/models/study_spec.rb b/spec/models/study_spec.rb new file mode 100644 index 00000000..fc30bb69 --- /dev/null +++ b/spec/models/study_spec.rb @@ -0,0 +1,5 @@ +require 'rails_helper' + +RSpec.describe Study, type: :model do + pending "add some examples to (or delete) #{__FILE__}" +end From 5bc312d9e1e49fcb5193b1bb615de47e1adf4a7c Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Wed, 12 Dec 2018 16:00:26 +1000 Subject: [PATCH 02/11] Added controllers for studies, questions and responses, and started specs --- app/controllers/questions_controller.rb | 100 +++++++++++++ app/controllers/responses_controller.rb | 69 +++++++++ app/controllers/studies_controller.rb | 88 +++++++++++ app/models/ability.rb | 46 ++++++ app/models/dataset.rb | 1 + app/models/question.rb | 53 ++++++- app/models/response.rb | 41 +++++ app/models/study.rb | 50 ++++++- app/models/user.rb | 6 + config/routes.rb | 14 ++ lib/modules/access/by_permission.rb | 18 +++ spec/controllers/questions_controller_spec.rb | 52 +++++++ spec/controllers/responses_controller_spec.rb | 40 +++++ spec/controllers/studies_controller_spec.rb | 47 ++++++ spec/factories/question_factory.rb | 58 +++++++ spec/factories/questions.rb | 5 - spec/factories/response_factory.rb | 11 ++ spec/factories/responses.rb | 5 - spec/factories/studies.rb | 5 - spec/factories/study_factory.rb | 9 ++ spec/lib/creation.rb | 37 +++++ spec/lib/creation_spec.rb | 5 + spec/requests/questions_spec.rb | 141 ++++++++++++++++++ spec/requests/studies_spec.rb | 107 +++++++++++++ 24 files changed, 986 insertions(+), 22 deletions(-) create mode 100644 app/controllers/questions_controller.rb create mode 100644 app/controllers/responses_controller.rb create mode 100644 app/controllers/studies_controller.rb create mode 100644 spec/controllers/questions_controller_spec.rb create mode 100644 spec/controllers/responses_controller_spec.rb create mode 100644 spec/controllers/studies_controller_spec.rb create mode 100644 spec/factories/question_factory.rb delete mode 100644 spec/factories/questions.rb create mode 100644 spec/factories/response_factory.rb delete mode 100644 spec/factories/responses.rb delete mode 100644 spec/factories/studies.rb create mode 100644 spec/factories/study_factory.rb create mode 100644 spec/requests/questions_spec.rb create mode 100644 spec/requests/studies_spec.rb diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb new file mode 100644 index 00000000..33bab112 --- /dev/null +++ b/app/controllers/questions_controller.rb @@ -0,0 +1,100 @@ +class QuestionsController < ApplicationController + + include Api::ControllerHelper + + #skip_authorization_check + + # GET /questions + # GET /studies/:study_id/questions + def index + do_authorize_class + + query = Question.all + + if params[:study_id] + # todo: + # check if this can be done better. We don't need to join + # all the way to study, only to the join table. + query = query.belonging_to_study(params[:study_id]) + end + + @questions, opts = Settings.api_response.response_advanced( + api_filter_params, + query, + Question, + Question.filter_settings + ) + respond_index(opts) + end + + # GET /questions/:id + def show + do_load_resource + do_authorize_instance + respond_show + end + + # GET /questions/filter + def filter + do_authorize_class + + filter_response, opts = Settings.api_response.response_advanced( + api_filter_params, + Question.all, + Question, + Question.filter_settings + ) + respond_filter(filter_response, opts) + end + + # GET /questions/new + def new + do_new_resource + do_set_attributes + do_authorize_instance + respond_show + end + + # POST /questions + def create + do_new_resource + do_set_attributes(question_params) + do_authorize_instance + + if @question.save + respond_create_success(question_path(@question)) + else + respond_change_fail + end + end + + # PUT /questions/:id + def update + do_load_resource + do_authorize_instance + + if @question.update_attributes(question_params) + respond_show + else + respond_change_fail + end + end + + # DELETE /questions/:id + def destroy + do_load_resource + do_authorize_instance + @question.destroy + respond_destroy + end + + private + + def question_params + #params[:question] = params[:question] || {} + #params[:question][:study_ids] = params[:study_ids] + params.require(:question).permit({:study_ids => []}, :text, :data) + end + + +end diff --git a/app/controllers/responses_controller.rb b/app/controllers/responses_controller.rb new file mode 100644 index 00000000..2417a9f5 --- /dev/null +++ b/app/controllers/responses_controller.rb @@ -0,0 +1,69 @@ +class ResponsesController < ApplicationController + + include Api::ControllerHelper + + # GET /responses + # GET /studies/:study_id/responses + def index + do_authorize_class + + @responses, opts = Settings.api_response.response_advanced( + api_filter_params, + Access::ByPermission.responses(current_user, params[:study_id]), + Response, + Response.filter_settings + ) + respond_index(opts) + end + + # GET /responses/:id + def show + do_load_resource + do_authorize_instance + respond_show + end + + # GET /responses/filter + def filter + do_authorize_class + + filter_response, opts = Settings.api_response.response_advanced( + api_filter_params, + Access::ByPermission.responses(current_user, nil), + Response, + Response.filter_settings + ) + respond_filter(filter_response, opts) + end + + # GET /responses/new + def new + do_new_resource + do_set_attributes + do_authorize_instance + respond_show + end + + # POST /responses + def create + do_new_resource + do_set_attributes(response_params) + do_authorize_instance + + if @response.save + respond_create_success(response_path(@response)) + else + respond_change_fail + end + end + + private + + def response_params + params[:response] = params[:response] || {} + params[:response][:study_id] = params[:study_id] + params.require(:response).permit(:study_id, :text, :data) + end + + +end diff --git a/app/controllers/studies_controller.rb b/app/controllers/studies_controller.rb new file mode 100644 index 00000000..9d389a84 --- /dev/null +++ b/app/controllers/studies_controller.rb @@ -0,0 +1,88 @@ +class StudiesController < ApplicationController + + include Api::ControllerHelper + + # GET /studies + def index + do_authorize_class + + @studies, opts = Settings.api_response.response_advanced( + api_filter_params, + Study.all, + Study, + Study.filter_settings + ) + respond_index(opts) + end + + # GET /studies/:id + def show + do_load_resource + do_authorize_instance + respond_show + end + + # GET /studies/filter + def filter + do_authorize_class + + filter_response, opts = Settings.api_response.response_advanced( + api_filter_params, + Study.all, + Study, + Study.filter_settings + ) + respond_filter(filter_response, opts) + end + + def new + do_new_resource + do_set_attributes + do_authorize_instance + + respond_show + end + + # POST /studies/ + def create + do_new_resource + do_set_attributes(study_params) + do_authorize_instance + + if @study.save + respond_create_success(study_path(@study)) + else + respond_change_fail + end + end + + # PUT /studies/:id + def update + do_load_resource + do_authorize_instance + + if @study.update_attributes(study_params) + respond_show + else + respond_change_fail + end + end + + # DELETE /studies/:id + def destroy + do_load_resource + do_authorize_instance + @study.destroy + respond_destroy + end + + private + + def study_params + # params[:study] = params[:study] || {} + # params[:study][:dataset_id] = params[:dataset_id] + params.require(:study).permit(:dataset_id, :name) + end + + +end diff --git a/app/models/ability.rb b/app/models/ability.rb index 25624bb1..48c9fd71 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -107,6 +107,10 @@ def initialize(user) to_error(user, is_guest) to_public(user, is_guest) + to_study(user, is_guest) + to_question(user, is_guest) + to_response(user, is_guest) + else fail ArgumentError, "Permissions are not defined for user '#{user.id}': #{user.role_symbols}" @@ -637,4 +641,46 @@ def to_public(user, is_guest) ], :public end + def to_study(user, is_guest) + + # only admin can create, update, delete + + # all users including guest can access any get request + can [:new, :index, :filter, :view], Study + + end + + def to_question(user, is_guest) + + can [:new], Question + + # only admin create, update, delete + + # only logged in users can view questions + can [:index, :filter, :view], Question unless is_guest + + end + + def to_response(user, is_guest) + + can [:new], Response + + # must have read permission on dataset item to create a response for it + can [:create], Response do |response| + check_model(response) + if response.dataset_item + Access::Core.can_any?(user, :reader, response.dataset_item.audio_recording.site.projects) + else + false + end + end + + # users can only view their own responses + can [:index, :filter, :view], Response, creator_id: user.id + + # only admin can update or delete responses + + end + + end \ No newline at end of file diff --git a/app/models/dataset.rb b/app/models/dataset.rb index abb2b10b..7646eb78 100644 --- a/app/models/dataset.rb +++ b/app/models/dataset.rb @@ -24,6 +24,7 @@ class Dataset < ActiveRecord::Base # This will potentially be hit very often, maybe multiple times per request # and therefore is a possible avenue for future optimization if necessary def self.default_dataset_id + # note: this may cause db:create and db:migrate to fail Dataset.where(name: 'default').first.id end diff --git a/app/models/question.rb b/app/models/question.rb index 8e780bfa..e965258f 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -6,10 +6,59 @@ class Question < ActiveRecord::Base #relationships belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_questions belongs_to :updater, class_name: 'User', foreign_key: :updater_id, inverse_of: :updated_questions - has_and_belongs_to_many :study - has_many :responses + has_and_belongs_to_many :studies, -> { uniq } + has_many :responses, dependent: :destroy # association validations validates :creator, existence: true + # todo: validate that question is associated with at least one study + + # Define filter api settings + def self.filter_settings + { + valid_fields: [:id, :text, :data, :created_at, :creator_id, :updated_at, :updater_id, :study_ids], + render_fields: [:id, :text, :data, :created_at, :creator_id, :updated_at, :updater_id], + new_spec_fields: lambda { |user| + { + text: nil, + data: nil + } + }, + controller: :questions, + action: :filter, + defaults: { + order_by: :created_at, + direction: :desc + }, + valid_associations: [ + { + join: Response, + on: Question.arel_table[:id].eq(Response.arel_table[:question_id]), + available: true + }, + { + join: Arel::Table.new(:questions_studies), + on: Question.arel_table[:id].eq(Arel::Table.new(:questions_studies)[:question_id]), + available: false, + associations: [ + { + join: Study, + on: Arel::Table.new(:questions_studies)[:study_id].eq(Study.arel_table[:id]), + available: true + } + ] + + } + ] + } + end + + # limit the results to questions associated with a study of the given id + scope :belonging_to_study, lambda { |study_id| + joins(:questions_studies).where('questions_studies.study_id = ?', study_id) + } + + + end diff --git a/app/models/response.rb b/app/models/response.rb index 176fed18..f8987fb7 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -15,4 +15,45 @@ class Response < ActiveRecord::Base validates :study, existence: true validates :dataset_item, existence: true + + # Define filter api settings + def self.filter_settings + { + valid_fields: [:id, :data, :created_at, :creator_id, :study_id, :question_id, :dataset_item_id], + render_fields: [:id, :data, :created_at, :creator_id, :study_id, :question_id, :dataset_item_id], + new_spec_fields: lambda { |user| + { + text: nil, + data: nil + } + }, + controller: :questions, + action: :filter, + defaults: { + order_by: :created_at, + direction: :desc + }, + valid_associations: [ + { + join: Response, + on: Question.arel_table[:id].eq(Response.arel_table[:question_id]), + available: true + }, + { + join: Arel::Table.new(:questions_studies), + on: Question.arel_table[:id].eq(Arel::Table.new(:questions_studies)[:question_id]), + available: false, + associations: [ + { + join: Study, + on: Arel::Table.new(:questions_studies)[:study_id].eq(Study.arel_table[:id]), + available: true + } + ] + + } + ] + } + end + end diff --git a/app/models/study.rb b/app/models/study.rb index 45ce3436..a8f50980 100644 --- a/app/models/study.rb +++ b/app/models/study.rb @@ -6,15 +6,55 @@ class Study < ActiveRecord::Base #relationships belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_studies belongs_to :updater, class_name: 'User', foreign_key: :updater_id, inverse_of: :updated_studies - has_and_belongs_to_many :questions + has_and_belongs_to_many :questions, -> { uniq } belongs_to :dataset - has_many :responses + has_many :responses, dependent: :destroy # association validations validates :creator, existence: true - validates :question, existence: true - validates :study, existence: true - validates :dataset_item, existence: true + validates :dataset, existence: true + + + # Define filter api settings + def self.filter_settings + { + valid_fields: [:id, :dataset_id, :name, :created_at, :creator_id, :updated_at, :updater_id], + render_fields: [:id, :dataset_id, :name, :created_at, :creator_id, :updated_at, :updater_id], + new_spec_fields: lambda { |user| + { + name: nil + } + }, + controller: :studies, + action: :filter, + defaults: { + order_by: :created_at, + direction: :desc + }, + valid_associations: [ + { + join: Dataset, + on: Study.arel_table[:dataset_id].eq(Dataset.arel_table[:id]), + available: true + }, + { + join: Arel::Table.new(:questions_studies), + on: Study.arel_table[:id].eq(Arel::Table.new(:questions_studies)[:study_id]), + available: false, + associations: [ + { + join: Question, + on: Arel::Table.new(:questions_studies)[:question_id].eq(Question.arel_table[:id]), + available: true + } + ] + + }, + ] + } + end + + end diff --git a/app/models/user.rb b/app/models/user.rb index 890385b2..a3e899d0 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -95,6 +95,12 @@ def login has_many :created_dataset_items, class_name: 'DatasetItem', foreign_key: :creator_id, inverse_of: :creator has_many :created_progress_events, class_name: 'ProgressEvent', foreign_key: :creator_id, inverse_of: :creator + has_many :created_studies, class_name: 'Study', foreign_key: :creator_id, inverse_of: :creator + has_many :created_questions, class_name: 'Question', foreign_key: :creator_id, inverse_of: :creator + has_many :created_responses, class_name: 'Response', foreign_key: :creator_id, inverse_of: :creator + has_many :updated_studies, class_name: 'Study', foreign_key: :updater_id, inverse_of: :updater + has_many :updated_questions, class_name: 'Question', foreign_key: :updater_id, inverse_of: :updater + # scopes scope :users, -> { where(roles_mask: 2) } diff --git a/config/routes.rb b/config/routes.rb index c6b7a8bd..e3d000d6 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,6 +1,7 @@ require 'resque/server' Rails.application.routes.draw do + # The priority is based upon order of creation: first created -> highest priority. # See how all your routes lay out with "rake routes". @@ -278,6 +279,19 @@ resources :items, controller: 'dataset_items', defaults: {format: 'json'} end + + # studies, questions, responses + match 'studies/filter' => 'studies#filter', via: [:get, :post], defaults: {format: 'json'} + match 'questions/filter' => 'questions#filter', via: [:get, :post], defaults: {format: 'json'} + match 'responses/filter' => 'responses#filter', via: [:get, :post], defaults: {format: 'json'} + resources :studies, defaults: {format: 'json'} + resources :questions, defaults: {format: 'json'} + resources :responses, except: :update, defaults: {format: 'json'} + get '/studies/:study_id/questions' => 'questions#index', defaults: {format: 'json'} + get '/studies/:study_id/responses' => 'responses#index', defaults: {format: 'json'} + post '/studies/:study_id/questions/:question_id/responses' => 'responses#create', defaults: {format: 'json'} + + # progress events match 'progress_events/filter' => 'progress_events#filter', via: [:get, :post], defaults: {format: 'json'} resources :progress_events, defaults: {format: 'json'} diff --git a/lib/modules/access/by_permission.rb b/lib/modules/access/by_permission.rb index 13d5ad80..6f15efe1 100644 --- a/lib/modules/access/by_permission.rb +++ b/lib/modules/access/by_permission.rb @@ -205,6 +205,24 @@ def progress_events(user, dataset_item_id = nil, levels = Access::Core.levels) end + # Get all responses for which this user has these access levels + # @param [User] user + # @param [Int] study_id + # @param [Symbol, Array] levels + # @return [ActiveRecord::Relation] responses + def responses(user, study_id = nil, levels = Access::Core.levels) + + query = Response + .joins(dataset_item: {audio_recording: :site}) + + if study_id + query = query.where(study_id: study_id) + end + + permission_sites(user, levels, query) + end + + private def permission_admin(user, levels, query) diff --git a/spec/controllers/questions_controller_spec.rb b/spec/controllers/questions_controller_spec.rb new file mode 100644 index 00000000..e274024a --- /dev/null +++ b/spec/controllers/questions_controller_spec.rb @@ -0,0 +1,52 @@ +require 'rails_helper' + +describe QuestionsController, type: :controller do + + # before { + # allow(CanCan::ControllerResource).to receive(:load_and_authorize_resource){ nil } + # } + + describe "GET #index" do + it "returns http success" do + get :index + expect(response).to have_http_status(401) + end + end + + describe "GET #show" do + it "returns http success" do + get :show, { :id => 1} + expect(response).to have_http_status(404) + end + end + + describe "GET #filter" do + it "returns http success" do + get :filter + expect(response).to have_http_status(401) + end + end + + describe "GET #new" do + it "returns http success" do + get :new + expect(response).to have_http_status(:success) + end + end + + describe "POST #create" do + it "returns http success" do + # is this how it is done for HABTM associations? + post :create, { :study_ids => [1] } + expect(response).to have_http_status(404) + end + end + + describe "PUT #update" do + it "returns http success" do + put :update, { :id => 1, :data => "something" } + expect(response).to have_http_status(404) + end + end + +end diff --git a/spec/controllers/responses_controller_spec.rb b/spec/controllers/responses_controller_spec.rb new file mode 100644 index 00000000..71eceb34 --- /dev/null +++ b/spec/controllers/responses_controller_spec.rb @@ -0,0 +1,40 @@ +require 'rails_helper' + +describe ResponsesController, type: :controller do + + describe "GET #index" do + it "returns http success" do + get :index + expect(response).to have_http_status(200) + end + end + + describe "GET #show" do + it "returns http success" do + get :show, {:id => 1} + expect(response).to have_http_status(404) + end + end + + describe "GET #filter" do + it "returns http success" do + get :filter + expect(response).to have_http_status(200) + end + end + + describe "GET #new" do + it "returns http success" do + get :new + expect(response).to have_http_status(:success) + end + end + + describe "POST #create" do + it "returns http success" do + post :create, { :study_id => 1 } + expect(response).to have_http_status(401) + end + end + +end diff --git a/spec/controllers/studies_controller_spec.rb b/spec/controllers/studies_controller_spec.rb new file mode 100644 index 00000000..ec637567 --- /dev/null +++ b/spec/controllers/studies_controller_spec.rb @@ -0,0 +1,47 @@ +require 'rails_helper' + +describe StudiesController, type: :controller do + + describe "GET #index" do + it "returns http success" do + get :index + expect(response).to have_http_status(:success) + end + end + + describe "GET #show" do + it "returns http success" do + get :show, { :id => 1} + expect(response).to have_http_status(404) + end + end + + describe "GET #filter" do + it "returns http success" do + get :filter + expect(response).to have_http_status(:success) + end + end + + describe "GET #new" do + it "returns http success" do + get :new + expect(response).to have_http_status(:success) + end + end + + describe "POST #create" do + it "returns http success" do + post :create, { :dataset_id => 1 } + expect(response).to have_http_status(401) + end + end + + describe "PUT #update" do + it "returns http success" do + put :update, { :id => 1, :dataset_id => 1, :name => "something" } + expect(response).to have_http_status(404) + end + end + +end diff --git a/spec/factories/question_factory.rb b/spec/factories/question_factory.rb new file mode 100644 index 00000000..fbd6dac6 --- /dev/null +++ b/spec/factories/question_factory.rb @@ -0,0 +1,58 @@ +FactoryGirl.define do + factory :question do + sequence(:text) { |n| "test question text #{n}" } + # some realistic data for a question. Annotation ids + # are not real (not based on factory-generated audio events) + data_json = <<~JSON + {"labels": [ + { + "id": "1", + "name": "Eastern Bristlebird click", + "tags": [ + "ebb", + "type 1" + ], + "examples": [ + { + "annotationId": 124730, + "image":"eb01.jpg" + }, + { + "annotationId": 124727, + "image":"eb01.jpg" + } + ] + }, + { + "id": "2", + "name": "Eastern Bristlebird whistle", + "tags": [ + "ebb", + "type 2" + ], + "examples": [ + { + "annotationId": 124622 + } + ] + }, + { + "id": "3", + "name": "Ground Parrot", + "tags": [ + "ground parrot", + "type 1" + ], + "examples": [ + { + "annotationId": 124623, + "image":"eb03.jpg" + } + ] + } + ]} + JSON + data data_json + creator + end +end diff --git a/spec/factories/questions.rb b/spec/factories/questions.rb deleted file mode 100644 index 571d4190..00000000 --- a/spec/factories/questions.rb +++ /dev/null @@ -1,5 +0,0 @@ -FactoryGirl.define do - factory :question do - - end -end diff --git a/spec/factories/response_factory.rb b/spec/factories/response_factory.rb new file mode 100644 index 00000000..477c0338 --- /dev/null +++ b/spec/factories/response_factory.rb @@ -0,0 +1,11 @@ +FactoryGirl.define do + factory :response do + data_json = <<~JSON + {"labels_present": [1,2]} + JSON + data data_json + creator + study + dataset_item + end +end diff --git a/spec/factories/responses.rb b/spec/factories/responses.rb deleted file mode 100644 index bfe3171f..00000000 --- a/spec/factories/responses.rb +++ /dev/null @@ -1,5 +0,0 @@ -FactoryGirl.define do - factory :response do - - end -end diff --git a/spec/factories/studies.rb b/spec/factories/studies.rb deleted file mode 100644 index d7551b1d..00000000 --- a/spec/factories/studies.rb +++ /dev/null @@ -1,5 +0,0 @@ -FactoryGirl.define do - factory :study do - - end -end diff --git a/spec/factories/study_factory.rb b/spec/factories/study_factory.rb new file mode 100644 index 00000000..b726f54f --- /dev/null +++ b/spec/factories/study_factory.rb @@ -0,0 +1,9 @@ +FactoryGirl.define do + + factory :study do + sequence(:name) { |n| "test study #{n}" } + creator + dataset + + end +end diff --git a/spec/lib/creation.rb b/spec/lib/creation.rb index 4722b23f..7db5e8af 100644 --- a/spec/lib/creation.rb +++ b/spec/lib/creation.rb @@ -99,6 +99,28 @@ def create_audio_recordings_hierarchy prepare_audio_recording end + # create study, question, response hierarchy + def create_study_hierarchy + + prepare_study + prepare_question + prepare_user_response + + end + + def prepare_study + let!(:study) { Common.create_study(admin_user, dataset) } + end + + def prepare_question + let!(:question) { Common.create_question(admin_user, study) } + end + + def prepare_user_response + let!(:user_response) { Common.create_user_response(reader_user, dataset_item, study, question) } + end + + def prepare_users # these 7 user types must be used to test every endpoint: let!(:admin_user) { User.where(user_name: 'Admin').first } @@ -350,6 +372,21 @@ def create_progress_event_full(creator, dataset_item, activity) FactoryGirl.create(:progress_event, creator: creator, dataset_item: dataset_item, activity: activity) end + def create_study(creator, dataset) + FactoryGirl.create(:study, creator: creator, dataset: dataset) + end + + def create_question(creator, study) + question = FactoryGirl.create(:question, creator: creator) + question.studies << study + question.save! + question + end + + def create_user_response(creator, dataset_item, study, question) + FactoryGirl.create(:response, creator: creator, dataset_item: dataset_item, study: study, question: question) + end + end end end \ No newline at end of file diff --git a/spec/lib/creation_spec.rb b/spec/lib/creation_spec.rb index 1935720b..602e3287 100644 --- a/spec/lib/creation_spec.rb +++ b/spec/lib/creation_spec.rb @@ -2,6 +2,7 @@ describe 'creation helper' do create_entire_hierarchy + create_study_hierarchy it 'correctly creates one of everything' do expect(User.count).to eq(6) @@ -59,5 +60,9 @@ expect(DatasetItem.count).to eq(2) expect(Dataset.all[1].id).to eq(DatasetItem.first.dataset_id) + expect(Study.count).to eq(1) + expect(Question.count).to eq(1) + expect(Response.count).to eq(1) + end end \ No newline at end of file diff --git a/spec/requests/questions_spec.rb b/spec/requests/questions_spec.rb new file mode 100644 index 00000000..958c1be6 --- /dev/null +++ b/spec/requests/questions_spec.rb @@ -0,0 +1,141 @@ +require 'rails_helper' +require 'rspec/mocks' + +def question_url(question_id = nil, study_id = nil) + + url = "/questions" + url = url + "/" + question_id.to_s if question_id + url = "/studies/#{study_id.to_s}" + url if study_id + return url + +end + +describe "Questions" do + create_entire_hierarchy + create_study_hierarchy + + let(:question_attributes) { + FactoryGirl.attributes_for(:question) + } + + let(:update_question_attributes) { + {text: ("updated question text") } + } + + before(:each) do + @env ||= {} + @env['HTTP_AUTHORIZATION'] = admin_token + @env['CONTENT_TYPE'] = "application/json" + end + + describe 'index,filter,show questions' do + + describe 'index' do + + it 'finds all (1) questions as admin' do + get question_url, nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(1) + expect(parsed_response['data'][0]['text']).to eq(question['text']) + end + + it 'finds all (1) questions for the given study as admin' do + get question_url(nil, study.id), nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(1) + expect(parsed_response['data'][0]['text']).to eq(question['text']) + end + + end + + describe 'filter' do + + it 'finds all (1) questions as admin' do + get question_url + "/filter", nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(1) + expect(parsed_response['data'][0]['text']).to eq(question['text']) + end + + end + + describe 'show' do + + it 'show question as admin' do + get question_url(question.id), nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].to_json).to eq(question.to_json) + end + + end + + end + + describe 'create and update' do + + describe 'create question' do + + it 'creates an orphan question' do + post question_url, question_attributes.to_json, @env + parsed_response = JSON.parse(response.body) + expect(response).to have_http_status(201) + expect(parsed_response['data'].symbolize_keys.slice(:text, :data)).to eq(question_attributes) + expect(parsed_response['data'].keys.sort).to eq(%w(id creator_id updater_id text + data created_at updated_at).sort) + expect(Question.all.count).to eq(2) + end + + it 'creates an question for a study' do + params = { question: question_attributes } + params[:question][:study_ids] = [study.id] + post question_url, params.to_json, @env + parsed_response = JSON.parse(response.body) + expect(response).to have_http_status(201) + expect(parsed_response['data'].symbolize_keys.slice(:text, :data)).to eq(question_attributes.slice(:text, :data)) + expect(parsed_response['data'].keys.sort).to eq(%w(id creator_id updater_id text + data created_at updated_at).sort) + expect(Question.all.count).to eq(2) + # check that the newly created question is associated with exactly one study + # and that the associated study has the correct id + joined_active_record = Question.where(id: parsed_response['data']['id']).includes(:studies)[0] + expect(joined_active_record.studies.count).to eq(1) + expect(joined_active_record.studies.first.id).to eq(study.id) + end + + end + + describe 'update question' do + + it 'updates a question' do + params = {question: {text: 'modified question text'}}.to_json + put question_url(question.id), params, @env + parsed_response = JSON.parse(response.body) + expect(response).to have_http_status(200) + expect(parsed_response['data']['text']).to eq('modified question text') + end + + end + + end + + describe 'delete' do + + it 'deletes a question and child responses' do + + delete question_url(question.id), nil, @env + expect(response).to have_http_status(204) + expect(Question.all.count).to eq(0) + expect(Response.all.count).to eq(0) + + end + + end + +end + + + diff --git a/spec/requests/studies_spec.rb b/spec/requests/studies_spec.rb new file mode 100644 index 00000000..844fe31f --- /dev/null +++ b/spec/requests/studies_spec.rb @@ -0,0 +1,107 @@ +require 'rails_helper' +require 'rspec/mocks' + +describe "Studies" do + create_entire_hierarchy + create_study_hierarchy + + let(:study_attributes) { + FactoryGirl.attributes_for(:study, {name: "test study", dataset_id: dataset.id}) + } + + let(:update_study_attributes) { + {name: ("updated study name") } + } + + before(:each) do + @env ||= {} + @env['HTTP_AUTHORIZATION'] = admin_token + @env['CONTENT_TYPE'] = "application/json" + + @study_url = "/studies" + @study_url_with_id = "/studies/#{study.id}" + end + + describe 'index,filter,show studies' do + + describe 'index' do + + it 'finds all (1) study as admin' do + get @study_url, nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(1) + expect(parsed_response['data'][0]['name']).to eq(study['name']) + end + + end + + describe 'filter' do + + it 'finds all (1) study as admin' do + get "#{@study_url}/filter", nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(1) + expect(parsed_response['data'][0]['name']).to eq(study['name']) + end + + end + + describe 'show' do + + it 'show study as admin' do + get @study_url_with_id, nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].to_json).to eq(study.to_json) + end + + end + + end + + describe 'create and update' do + + describe 'create study' do + + it 'creates a study' do + post @study_url, study_attributes.to_json, @env + parsed_response = JSON.parse(response.body) + expect(response).to have_http_status(201) + expect(parsed_response['data']['dataset_id']).to eq(dataset.id) + end + + end + + describe 'update study' do + + it 'updates a study' do + params = {study: {name: 'modified study name'}}.to_json + put @study_url_with_id, params, @env + parsed_response = JSON.parse(response.body) + expect(response).to have_http_status(200) + expect(parsed_response['data']['name']).to eq('modified study name') + end + + end + + end + + describe 'delete' do + + it 'deletes a study and child responses' do + + delete @study_url_with_id, nil, @env + expect(response).to have_http_status(204) + expect(Study.all.count).to eq(0) + expect(Response.all.count).to eq(0) + + end + + end + +end + + + From 48f994740e267f29915927233639990f8c08a888 Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Thu, 10 Jan 2019 15:37:17 +1000 Subject: [PATCH 03/11] spec for associating existing question with existing study --- spec/lib/citizen_science_creation.rb | 174 +++++++++++++++++++++++++++ spec/rails_helper.rb | 3 + spec/requests/dataset_items_spec.rb | 80 +----------- spec/requests/questions_spec.rb | 43 +++++++ 4 files changed, 221 insertions(+), 79 deletions(-) create mode 100644 spec/lib/citizen_science_creation.rb diff --git a/spec/lib/citizen_science_creation.rb b/spec/lib/citizen_science_creation.rb new file mode 100644 index 00000000..f118a2dc --- /dev/null +++ b/spec/lib/citizen_science_creation.rb @@ -0,0 +1,174 @@ +module CitizenScienceCreation + + # creates 12 dataset items. Every 3rd dataset item has a progress event created by the writer user + # and every 2nd has a progress event created by the reader user. + # Half the items are with one audio_recording and the other half with another (alternating between them) + # Resulting in dataset items 2,4,6,8,10,12 viewed by reader and + # 3,6,9,12 viewed by writer, and 6,12 viewed by both and 1,5,7,11 not viewed + # Adding these to the 1 dataset item already created in the create_entire_hierarchy, which + # has a progress event, there are 13 dataset items, 4 of which are not viewed + def create_many_dataset_items + + let(:many_dataset_items_some_with_events) { + + # start with the 2 dataset items created in the entire hierarchy + results = [ + {dataset_item: dataset_item, progress_events: [progress_event_for_no_access_user]}, + {dataset_item: default_dataset_item, progress_events: [progress_event]} + ] + + # create a progress event for the writer user on every 3rd dataset item + # and a progress event for the reader user on every 2nd dataset item. + # some dataset items will have no progress events, some by writer only, some by + # reader only and some by both + progress_event_creators = [ + {creator: writer_user, view_every: 3}, + {creator: reader_user, view_every: 2}, + ] + + # make more than 25 to test paging + num_dataset_items = 32 + + # create another audio recording so we can make sure the order is not affected by the audio recording id + another_audio_recording = FactoryGirl.create( + :audio_recording, + :status_ready, + creator: writer_user, + uploader: writer_user, + site: site, + sample_rate_hertz: 22050) + + audio_recordings = [audio_recording, another_audio_recording] + + # random number generator with seed + my_rand = Random.new(99) + + # create the dataset items one at a time + for d in 1..num_dataset_items do + + # create a dataset item with alternating audio recording id + # So that we can test the audio recording does not affect the order + dataset_item = FactoryGirl.create(:dataset_item, + creator: admin_user, + dataset: dataset, + audio_recording: audio_recordings[d % 2], + start_time_seconds: d, + end_time_seconds: d+10, + order: my_rand.rand * 10) + dataset_item.save! + + current_data = {dataset_item: dataset_item, progress_events: [], progress_event_count: 0 } + + # for this dataset item, add a progress even for zero or more of the users + # If this dataset item is the nth created, add progress events for those users + # who's view_every value is a factor of n. + for c in progress_event_creators do + progress_event = nil + if d % c[:view_every] == 0 + progress_event = FactoryGirl.create( + :progress_event, + creator: c[:creator], + dataset_item: dataset_item, + activity: "viewed", + created_at: "2017-01-01 12:34:56") + + current_data[:progress_events].push(progress_event) + end + end + + results.push(current_data) + end + + results + + } + + end + + # creates and saves many studies, questions and responses + # allowing proper testing of filter and index + # Ensures that there are a range of relationships between records + # So that filtering by (e.g.) questions of a particular study can be properly tested + def create_many_studies + + let(:many_studies) { + + results = { + studies: [], + questions: [], + # question_studies: {}, + # study_questions: {}, + responses: [] + } + + + # only admins can create studies and questions + + response_creators = [ + {creator: writer_user}, + {creator: reader_user} + ] + + num_studies = 5 + num_questions = 6 + num_responses = 32 + + + # #random number generator with seed + # my_rand = Random.new(99) + + for s in 1..num_studies do + + study = FactoryGirl.create(:study, + creator: admin_user, + dataset: dataset, + name: "Test Study #{s}") + study.save! + # initialize for later when questions are created + # results[:study_questions][study.id] = [] + results[:studies].push(study) + + end + + for q in 1..num_questions do + + # number of studies to associate question to is q mod max number + # pick these at random + number_related_studies = (q % num_studies) + 1 + study_ids = results[:studies].map(&:id).sample(number_related_studies, random:Random.new(q)) + + # study_ids = [results[:studies][0].id, + # results[:studies][2].id, + # results[:studies][3].id] + + question = FactoryGirl.create(:question, + creator: admin_user, + study_ids: study_ids, + text: "test question text #{q}", + data: {}) + + question.save! + + # # the returned Question object does not have the study_ids + # # so keep track of the relationships here + # results[:question_studies][question.id] = study_ids + # study_ids.each do |study_id| + # # append the question id to the array of ids for + # results[:study_questions][study_id].push(question.id) + # end + + + results[:questions].push(question) + + + end + + results + + } + + + end + + +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 8885ae90..edbe7bb7 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -115,6 +115,9 @@ config.include Creation::Example config.extend Creation::ExampleGroup + require File.join(File.dirname(File.expand_path(__FILE__)), 'lib', 'citizen_science_creation.rb') + config.extend CitizenScienceCreation + require 'enumerize/integrations/rspec' extend Enumerize::Integrations::RSpec diff --git a/spec/requests/dataset_items_spec.rb b/spec/requests/dataset_items_spec.rb index e8dc7813..0ca35f48 100644 --- a/spec/requests/dataset_items_spec.rb +++ b/spec/requests/dataset_items_spec.rb @@ -192,86 +192,8 @@ def filter_and_sort_for_comparison (items, user, page = nil, limit = nil) @dataset_item_next_for_me_url = "/datasets/#{dataset_item.dataset_id}/dataset_items/next_for_me" end - # creates 12 dataset items. Every 3rd dataset item has a progress event created by the writer user - # and every 2nd has a progress event created by the reader user. - # Half the items are with one audio_recording and the other half with another (alternating between them) - # Resulting in dataset items 2,4,6,8,10,12 viewed by reader and - # 3,6,9,12 viewed by writer, and 6,12 viewed by both and 1,5,7,11 not viewed - # Adding these to the 1 dataset item already created in the create_entire_hierarchy, which - # has a progress event, there are 13 dataset items, 4 of which are not viewed - let(:many_dataset_items_some_with_events) { - - # start with the 2 dataset items created in the entire hierarchy - results = [ - {dataset_item: dataset_item, progress_events: [progress_event_for_no_access_user]}, - {dataset_item: default_dataset_item, progress_events: [progress_event]} - ] - - # create a progress event for the writer user on every 3rd dataset item - # and a progress event for the reader user on every 2nd dataset item. - # some dataset items will have no progress events, some by writer only, some by - # reader only and some by both - progress_event_creators = [ - {creator: writer_user, view_every: 3}, - {creator: reader_user, view_every: 2}, - ] - - # make more than 25 to test paging - num_dataset_items = 32 - - # create another audio recording so we can make sure the order is not affected by the audio recording id - another_audio_recording = FactoryGirl.create( - :audio_recording, - :status_ready, - creator: writer_user, - uploader: writer_user, - site: site, - sample_rate_hertz: 22050) - - audio_recordings = [audio_recording, another_audio_recording] - - # random number generator with seed - my_rand = Random.new(99) - - # create the dataset items one at a time - for d in 1..num_dataset_items do - - # create a dataset item with alternating audio recording id - # So that we can test the audio recording does not affect the order - dataset_item = FactoryGirl.create(:dataset_item, - creator: admin_user, - dataset: dataset, - audio_recording: audio_recordings[d % 2], - start_time_seconds: d, - end_time_seconds: d+10, - order: my_rand.rand * 10) - dataset_item.save! - - current_data = {dataset_item: dataset_item, progress_events: [], progress_event_count: 0 } - - # for this dataset item, add a progress even for zero or more of the users - # If this dataset item is the nth created, add progress events for those users - # who's view_every value is a factor of n. - for c in progress_event_creators do - progress_event = nil - if d % c[:view_every] == 0 - progress_event = FactoryGirl.create( - :progress_event, - creator: c[:creator], - dataset_item: dataset_item, - activity: "viewed", - created_at: "2017-01-01 12:34:56") - - current_data[:progress_events].push(progress_event) - end - end - - results.push(current_data) - end - - results + create_many_dataset_items - } let(:raw_post) { { diff --git a/spec/requests/questions_spec.rb b/spec/requests/questions_spec.rb index 958c1be6..9908744d 100644 --- a/spec/requests/questions_spec.rb +++ b/spec/requests/questions_spec.rb @@ -1,6 +1,9 @@ require 'rails_helper' require 'rspec/mocks' + + + def question_url(question_id = nil, study_id = nil) url = "/questions" @@ -10,10 +13,14 @@ def question_url(question_id = nil, study_id = nil) end + describe "Questions" do create_entire_hierarchy create_study_hierarchy + # create a bunch of studies, questions and responses to work with + create_many_studies + let(:question_attributes) { FactoryGirl.attributes_for(:question) } @@ -48,6 +55,17 @@ def question_url(question_id = nil, study_id = nil) expect(parsed_response['data'][0]['text']).to eq(question['text']) end + # it 'finds all questions for the given study as admin' do + # + # available_records = many_studies + # + # + # + # + # + # end + + end describe 'filter' do @@ -118,6 +136,31 @@ def question_url(question_id = nil, study_id = nil) expect(parsed_response['data']['text']).to eq('modified question text') end + it 'adds an existing question to an existing study' do + + available_records = many_studies + # find an existing question that is not already associated with all studies + existing_question = (available_records[:questions].select { + |s| s.studies.length < available_records[:studies].length + })[0] + # find an existing study id that is not already associated with the question + available_study_ids = available_records[:studies].map(&:id) - existing_question.study_ids + study_id_to_add = available_study_ids[0] + + study_ids = existing_question.study_ids + study_ids.push(study_id_to_add) + + # adding a question to a study requires updating either the question or the study record with + # all the associated ids including the new one. + params = {question: {study_ids: study_ids}}.to_json + + put question_url(existing_question.id), params, @env + #parsed_response = JSON.parse(response.body) + expect(response).to have_http_status(200) + expect(Question.find(existing_question.id).study_ids.sort).to eq(study_ids.sort) + + end + end end From 745cc766745c20bd211b10b0d4c01428dd69d2f1 Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Fri, 11 Jan 2019 15:33:27 +1000 Subject: [PATCH 04/11] specs to filter questions by study id --- spec/requests/questions_spec.rb | 54 +++++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/spec/requests/questions_spec.rb b/spec/requests/questions_spec.rb index 9908744d..5e68c25d 100644 --- a/spec/requests/questions_spec.rb +++ b/spec/requests/questions_spec.rb @@ -55,16 +55,23 @@ def question_url(question_id = nil, study_id = nil) expect(parsed_response['data'][0]['text']).to eq(question['text']) end - # it 'finds all questions for the given study as admin' do - # - # available_records = many_studies - # - # - # - # - # - # end + it 'finds the correct questions for the given study as admin' do + available_records = many_studies + + study_id = available_records[:studies][0].id + get question_url(nil, study_id), nil, @env + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(available_records[:studies][0].question_ids.count) + expect((parsed_response['data'].map { |q| q['id'] }).sort).to eq(available_records[:studies][0].question_ids.sort) + + study_id = available_records[:studies][1].id + get question_url(nil, study_id), nil, @env + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(available_records[:studies][1].question_ids.count) + expect((parsed_response['data'].map { |q| q['id'] }).sort).to eq(available_records[:studies][1].question_ids.sort) + + end end @@ -78,6 +85,23 @@ def question_url(question_id = nil, study_id = nil) expect(parsed_response['data'][0]['text']).to eq(question['text']) end + it 'finds the correct questions for the given study as admin' do + + available_records = many_studies + + url = question_url + "/filter" + + study = available_records[:studies][0] + # there might be a more efficient way to do this query, without + # joining all the way to studies. + filter_params = { filter: { 'studies.id'=> {in: [study.id] } }} + post url, filter_params.to_json, @env + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(available_records[:studies][0].question_ids.count) + expect((parsed_response['data'].map { |q| q['id'] }).sort).to eq(available_records[:studies][0].question_ids.sort) + + end + end describe 'show' do @@ -161,6 +185,18 @@ def question_url(question_id = nil, study_id = nil) end + it 'does not allow a question with no studies' do + + # doesn't work because require/permit fails with study_ids = nil + + params = {question: {study_ids: []}}.to_json + put question_url(question.id), params, @env + parsed_response = JSON.parse(response.body) + expect(response).to have_http_status(415) + + end + + end end From 04b2d4ab33bbbcb522424365b321d6ff7f16bdcc Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Tue, 15 Jan 2019 15:02:20 +1000 Subject: [PATCH 05/11] validate associations and add model specs for studies and questions --- app/controllers/questions_controller.rb | 13 +++--- app/models/question.rb | 9 ++--- app/models/study.rb | 6 ++- spec/lib/creation.rb | 2 +- spec/models/question_spec.rb | 44 +++++++++++++++++++- spec/models/study_spec.rb | 54 ++++++++++++++++++++++++- spec/requests/questions_spec.rb | 34 ++++++---------- 7 files changed, 126 insertions(+), 36 deletions(-) diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 33bab112..96648c68 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -13,7 +13,7 @@ def index if params[:study_id] # todo: - # check if this can be done better. We don't need to join + # check if this can be done better. We shouln't need to join # all the way to study, only to the join table. query = query.belonging_to_study(params[:study_id]) end @@ -91,10 +91,13 @@ def destroy private def question_params - #params[:question] = params[:question] || {} - #params[:question][:study_ids] = params[:study_ids] - params.require(:question).permit({:study_ids => []}, :text, :data) + # empty array is replaced with nil by rails. Revert to empty array + # to avoid errors with strong parameters + if params.has_key?(:question) and params[:question].has_key?(:study_ids) and params[:question][:study_ids].nil? + params[:question][:study_ids] = [] + end + permitted = [{study_ids: []}, :text, :data] + params.require(:question).permit(permitted) end - end diff --git a/app/models/question.rb b/app/models/question.rb index e965258f..0a00bd1f 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -10,9 +10,10 @@ class Question < ActiveRecord::Base has_many :responses, dependent: :destroy # association validations - validates :creator, existence: true - - # todo: validate that question is associated with at least one study + validates :creator, presence: true + # questions must be associated with at least one study + validates :studies, presence: true + validates_associated :studies # Define filter api settings def self.filter_settings @@ -59,6 +60,4 @@ def self.filter_settings joins(:questions_studies).where('questions_studies.study_id = ?', study_id) } - - end diff --git a/app/models/study.rb b/app/models/study.rb index a8f50980..d037a713 100644 --- a/app/models/study.rb +++ b/app/models/study.rb @@ -11,8 +11,10 @@ class Study < ActiveRecord::Base has_many :responses, dependent: :destroy # association validations - validates :creator, existence: true - validates :dataset, existence: true + validates :creator, presence: true + validates :dataset, presence: true + validates_associated :dataset + validates_associated :questions # Define filter api settings diff --git a/spec/lib/creation.rb b/spec/lib/creation.rb index 7db5e8af..27f8408c 100644 --- a/spec/lib/creation.rb +++ b/spec/lib/creation.rb @@ -377,7 +377,7 @@ def create_study(creator, dataset) end def create_question(creator, study) - question = FactoryGirl.create(:question, creator: creator) + question = FactoryGirl.build(:question, creator: creator) question.studies << study question.save! question diff --git a/spec/models/question_spec.rb b/spec/models/question_spec.rb index 5e95bcf8..a6ca452a 100644 --- a/spec/models/question_spec.rb +++ b/spec/models/question_spec.rb @@ -1,5 +1,47 @@ require 'rails_helper' RSpec.describe Question, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + + let(:study) { + FactoryGirl.create(:study) + } + + it 'has a valid factory' do + expect(create(:question, studies: [study])).to be_valid + end + + it 'created_at should be set by rails' do + item = create(:question, studies: [study]) + expect(item.created_at).to_not be_blank + expect(item.valid?).to be true + item.reload + expect(item.created_at).to_not be_blank + expect(item.valid?).to be true + end + + describe 'associations' do + it { is_expected.to have_and_belong_to_many(:studies) } + it { is_expected.to have_many(:responses) } + it { is_expected.to belong_to(:updater).with_foreign_key(:updater_id) } + it { is_expected.to belong_to(:creator).with_foreign_key(:creator_id) } + end + + describe 'validations' do + it { is_expected.to validate_presence_of(:studies) } + it 'cannot be created with no studies' do + expect { + create(:question, studies: []) + }.to raise_error(ActiveRecord::RecordInvalid) + end + it { is_expected.to validate_presence_of(:creator) } + + it 'can not create a question associated with a nonexistent study' do + expect { + # array with both an existing and nonexistent study + create(:question, study_ids: [study.id, 12345]) + }.to raise_error(ActiveRecord::RecordNotFound) + end + + end + end diff --git a/spec/models/study_spec.rb b/spec/models/study_spec.rb index fc30bb69..11a4f64d 100644 --- a/spec/models/study_spec.rb +++ b/spec/models/study_spec.rb @@ -1,5 +1,57 @@ require 'rails_helper' RSpec.describe Study, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + + let(:dataset) { + FactoryGirl.create(:dataset) + } + + it 'has a valid factory' do + expect(create(:study, dataset: dataset)).to be_valid + end + + it 'created_at should be set by rails' do + item = create(:study, dataset: dataset) + expect(item.created_at).to_not be_blank + expect(item.valid?).to be true + item.reload + expect(item.created_at).to_not be_blank + expect(item.valid?).to be true + end + + describe 'associations' do + it { is_expected.to have_and_belong_to_many(:questions) } + it { is_expected.to have_many(:responses) } + it { is_expected.to belong_to(:dataset) } + it { is_expected.to belong_to(:updater).with_foreign_key(:updater_id) } + it { is_expected.to belong_to(:creator).with_foreign_key(:creator_id) } + end + + describe 'validations' do + + it { is_expected.to validate_presence_of(:dataset) } + + it 'cannot be created without a dataset' do + expect { + create(:study, dataset: nil) + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it { is_expected.to validate_presence_of(:creator) } + + it 'can not create a study associated with a nonexistent dataset' do + expect { + create(:study, dataset_id: 12345) + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it 'can not create a study with a nonexistent question' do + expect { + create(:study, question_ids: [12345]) + # not sure why the error is different for the two associations + }.to raise_error(ActiveRecord::RecordNotFound) + end + + end + end diff --git a/spec/requests/questions_spec.rb b/spec/requests/questions_spec.rb index 5e68c25d..62a820b6 100644 --- a/spec/requests/questions_spec.rb +++ b/spec/requests/questions_spec.rb @@ -1,9 +1,6 @@ require 'rails_helper' require 'rspec/mocks' - - - def question_url(question_id = nil, study_id = nil) url = "/questions" @@ -13,7 +10,6 @@ def question_url(question_id = nil, study_id = nil) end - describe "Questions" do create_entire_hierarchy create_study_hierarchy @@ -121,17 +117,7 @@ def question_url(question_id = nil, study_id = nil) describe 'create question' do - it 'creates an orphan question' do - post question_url, question_attributes.to_json, @env - parsed_response = JSON.parse(response.body) - expect(response).to have_http_status(201) - expect(parsed_response['data'].symbolize_keys.slice(:text, :data)).to eq(question_attributes) - expect(parsed_response['data'].keys.sort).to eq(%w(id creator_id updater_id text - data created_at updated_at).sort) - expect(Question.all.count).to eq(2) - end - - it 'creates an question for a study' do + it 'creates a question for a study' do params = { question: question_attributes } params[:question][:study_ids] = [study.id] post question_url, params.to_json, @env @@ -148,6 +134,14 @@ def question_url(question_id = nil, study_id = nil) expect(joined_active_record.studies.first.id).to eq(study.id) end + it 'can not create an orphan question' do + post question_url, question_attributes.to_json, @env + parsed_response = JSON.parse(response.body) + expect(response).to have_http_status(422) + expect(Question.all.count).to eq(1) + expect(parsed_response["meta"]["error"]["info"]["studies"]).to eq(["can't be blank"]) + end + end describe 'update question' do @@ -186,17 +180,15 @@ def question_url(question_id = nil, study_id = nil) end it 'does not allow a question with no studies' do - - # doesn't work because require/permit fails with study_ids = nil - + current_study_ids = question.study_ids.dup params = {question: {study_ids: []}}.to_json put question_url(question.id), params, @env parsed_response = JSON.parse(response.body) - expect(response).to have_http_status(415) + expect(response).to have_http_status(422) + expect(Question.find(question.id).study_ids).to eq(current_study_ids) end - - + end end From 6b483339d40c5c39310f52501b0d8aab2bc25f36 Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Thu, 17 Jan 2019 16:57:32 +1000 Subject: [PATCH 06/11] response model and request specs --- app/controllers/responses_controller.rb | 7 +- app/models/question.rb | 2 +- app/models/response.rb | 49 +-- app/models/study.rb | 3 - db/migrate/20181210052735_create_responses.rb | 3 +- db/structure.sql | 5 +- spec/lib/citizen_science_creation.rb | 105 +++++-- spec/lib/creation.rb | 1 + spec/models/response_spec.rb | 87 +++++- spec/requests/questions_spec.rb | 3 +- spec/requests/responses_spec.rb | 279 ++++++++++++++++++ 11 files changed, 491 insertions(+), 53 deletions(-) create mode 100644 spec/requests/responses_spec.rb diff --git a/app/controllers/responses_controller.rb b/app/controllers/responses_controller.rb index 2417a9f5..2948d20d 100644 --- a/app/controllers/responses_controller.rb +++ b/app/controllers/responses_controller.rb @@ -45,6 +45,7 @@ def new end # POST /responses + # POST /study/:study_id/questions/:question_id/responses def create do_new_resource do_set_attributes(response_params) @@ -60,9 +61,9 @@ def create private def response_params - params[:response] = params[:response] || {} - params[:response][:study_id] = params[:study_id] - params.require(:response).permit(:study_id, :text, :data) + # params[:response] = params[:response] || {} + # params[:response][:study_id] = params[:study_id] + params.require(:response).permit(:study_id, :question_id, :dataset_item_id, :text, :data) end diff --git a/app/models/question.rb b/app/models/question.rb index 0a00bd1f..dbf0ac94 100644 --- a/app/models/question.rb +++ b/app/models/question.rb @@ -13,7 +13,7 @@ class Question < ActiveRecord::Base validates :creator, presence: true # questions must be associated with at least one study validates :studies, presence: true - validates_associated :studies + # Define filter api settings def self.filter_settings diff --git a/app/models/response.rb b/app/models/response.rb index f8987fb7..ec258a7a 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -10,10 +10,17 @@ class Response < ActiveRecord::Base belongs_to :dataset_item # association validations - validates :creator, existence: true - validates :question, existence: true - validates :study, existence: true - validates :dataset_item, existence: true + validates :creator, presence: true + validates :question, presence: true + validates :study, presence: true + validates :dataset_item, presence: true + validates :data, presence: true + + # Response is associated with study directly and also via question + # validate that the associated study and question are associated with each other + + validate :consistent_associations + # Define filter api settings @@ -27,7 +34,7 @@ def self.filter_settings data: nil } }, - controller: :questions, + controller: :responses, action: :filter, defaults: { order_by: :created_at, @@ -35,25 +42,33 @@ def self.filter_settings }, valid_associations: [ { - join: Response, + join: Question, on: Question.arel_table[:id].eq(Response.arel_table[:question_id]), available: true }, { - join: Arel::Table.new(:questions_studies), - on: Question.arel_table[:id].eq(Arel::Table.new(:questions_studies)[:question_id]), - available: false, - associations: [ - { - join: Study, - on: Arel::Table.new(:questions_studies)[:study_id].eq(Study.arel_table[:id]), - available: true - } - ] - + join: Study, + on: Study.arel_table[:id].eq(Response.arel_table[:study_id]), + available: true + }, + { + join: DatasetItem, + on: DatasetItem.arel_table[:id].eq(Response.arel_table[:dataset_item_id]), + available: true } ] } end + private + + def consistent_associations + if !study.question_ids.include?(question.id) + errors.add(:question_id, "parent question is not associated with parent study") + end + if study.dataset_id != dataset_item.dataset_id + errors.add(:dataset_item_id, "dataset item and study must belong to the same dataset") + end + end + end diff --git a/app/models/study.rb b/app/models/study.rb index d037a713..14fc54c4 100644 --- a/app/models/study.rb +++ b/app/models/study.rb @@ -13,9 +13,6 @@ class Study < ActiveRecord::Base # association validations validates :creator, presence: true validates :dataset, presence: true - validates_associated :dataset - validates_associated :questions - # Define filter api settings def self.filter_settings diff --git a/db/migrate/20181210052735_create_responses.rb b/db/migrate/20181210052735_create_responses.rb index b7738f50..c1bb961e 100644 --- a/db/migrate/20181210052735_create_responses.rb +++ b/db/migrate/20181210052735_create_responses.rb @@ -6,9 +6,10 @@ def change t.integer :dataset_item_id t.integer :question_id t.integer :study_id + t.datetime :created_at t.text :data - t.timestamps null: false + #t.timestamps null: false end add_foreign_key :responses, :dataset_items diff --git a/db/structure.sql b/db/structure.sql index 978eae25..a253cd30 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -574,9 +574,8 @@ CREATE TABLE responses ( dataset_item_id integer, question_id integer, study_id integer, - data text, - created_at timestamp without time zone NOT NULL, - updated_at timestamp without time zone NOT NULL + created_at timestamp without time zone, + data text ); diff --git a/spec/lib/citizen_science_creation.rb b/spec/lib/citizen_science_creation.rb index f118a2dc..66357a21 100644 --- a/spec/lib/citizen_science_creation.rb +++ b/spec/lib/citizen_science_creation.rb @@ -96,8 +96,6 @@ def create_many_studies results = { studies: [], questions: [], - # question_studies: {}, - # study_questions: {}, responses: [] } @@ -109,10 +107,23 @@ def create_many_studies {creator: reader_user} ] - num_studies = 5 - num_questions = 6 - num_responses = 32 + # for each question, which study should it be associated with? + # number represent study index, not id (nothing is created yet) + question_study_map = [ + [0], + [1], + [1,2], + [2] + ] + + num_studies = question_study_map.flatten.max + 1 + # number of responses per study per question per user + num_responses = 3 + response_creators = [writer_user, reader_user] + + total_num_respones = question_study_map.flatten.count * response_creators.count + puts "creating #{total_num_respones} responses in total" # #random number generator with seed # my_rand = Random.new(99) @@ -130,45 +141,93 @@ def create_many_studies end - for q in 1..num_questions do - - # number of studies to associate question to is q mod max number - # pick these at random - number_related_studies = (q % num_studies) + 1 - study_ids = results[:studies].map(&:id).sample(number_related_studies, random:Random.new(q)) + question_count = 1 + question_study_map.each do |q| - # study_ids = [results[:studies][0].id, - # results[:studies][2].id, - # results[:studies][3].id] + # # each question is related to one more study than the previous question + # # i.e. number of studies is q mod max number + # # pick these at random, seeded by q for consistency + # number_related_studies = (q % num_studies) + 1 + # study_ids = results[:studies].map(&:id).sample(number_related_studies, random:Random.new(q)) + study_ids = results[:studies].values_at(*q).map(&:id) question = FactoryGirl.create(:question, creator: admin_user, study_ids: study_ids, - text: "test question text #{q}", + text: "test question text #{question_count}", data: {}) question.save! - # # the returned Question object does not have the study_ids - # # so keep track of the relationships here - # results[:question_studies][question.id] = study_ids - # study_ids.each do |study_id| - # # append the question id to the array of ids for - # results[:study_questions][study_id].push(question.id) - # end + results[:questions].push(question) + question_count += 1 + end - results[:questions].push(question) + for r in 1..num_responses do + results[:studies].each do |s| + s.question_ids.each do |q_id| + response_creators.each do |creator| + # create a response for this study, for this question, for this user + data_value = "for study #{s.id}, question #{q_id}, dataset_item #{dataset_item.id}, user #{creator.id}" + response = FactoryGirl.create(:response, + creator: creator, + study_id: s.id, + question_id: q_id, + dataset_item: dataset_item, + data: { some_key: data_value }.to_json) + + response.save! + results[:responses].push(response) + + end + end + end end results } + end + + + # creates a related network of + # dataset, dataset_item, progress_event, study, question, response + def create_citizen_science_hierarchies (number) + + let(:citizen_science_hierarchies) { + + hierarchies = [] + + for i in 1..number do + + records = {} + records[:dataset] = FactoryGirl.create(:dataset) + records[:study] = FactoryGirl.create(:study, + creator: admin_user, + dataset: records[:dataset]) + records[:question] = FactoryGirl.create(:question, + creator: admin_user, + studies: [records[:study]]) + records[:dataset_item] = FactoryGirl.create(:dataset_item, dataset: records[:dataset]) + records[:user_response] = FactoryGirl.create(:response, + creator: reader_user, + study: records[:study], + question: records[:question], + dataset_item: records[:dataset_item]) + records[:progress_event] = FactoryGirl.create(:progress_event, dataset_item: records[:dataset_item]) + hierarchies.push(records) + + end + hierarchies + } end + + end diff --git a/spec/lib/creation.rb b/spec/lib/creation.rb index 27f8408c..6f249987 100644 --- a/spec/lib/creation.rb +++ b/spec/lib/creation.rb @@ -117,6 +117,7 @@ def prepare_question end def prepare_user_response + # named to avoid name collision with rspec 'response' let!(:user_response) { Common.create_user_response(reader_user, dataset_item, study, question) } end diff --git a/spec/models/response_spec.rb b/spec/models/response_spec.rb index aeb51d18..d067ebbe 100644 --- a/spec/models/response_spec.rb +++ b/spec/models/response_spec.rb @@ -1,5 +1,90 @@ require 'rails_helper' RSpec.describe Response, type: :model do - pending "add some examples to (or delete) #{__FILE__}" + + let(:dataset_item) { + FactoryGirl.create(:dataset_item) + } + let(:study) { + FactoryGirl.create(:study) + } + let(:question) { + FactoryGirl.create(:question, studies: [study]) + } + + it 'has a valid factory' do + expect(create(:response, question: question, study: study, dataset_item: dataset_item)).to be_valid + expect(create(:response, question_id: question.id, study_id: study.id, dataset_item_id: dataset_item.id)).to be_valid + end + + it 'created_at should be set by rails' do + item = create(:response, question: question, study: study, dataset_item: dataset_item) + expect(item.created_at).to_not be_blank + expect(item.valid?).to be true + item.reload + expect(item.created_at).to_not be_blank + expect(item.valid?).to be true + end + + describe 'associations' do + it { is_expected.to belong_to(:question) } + it { is_expected.to belong_to(:study) } + it { is_expected.to belong_to(:dataset_item) } + it { is_expected.to belong_to(:creator).with_foreign_key(:creator_id) } + end + + describe 'validations' do + + it { is_expected.to validate_presence_of(:dataset_item) } + it 'cannot be created without a dataset_item' do + expect { + create(:response, question: question, study: study, dataset_item: nil) + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it { is_expected.to validate_presence_of(:question) } + it 'cannot be created without a question' do + expect { + create(:response, question: nil, study: study, dataset_item: dataset_item) + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it { is_expected.to validate_presence_of(:study) } + it 'cannot be created without a study' do + expect { + create(:response, question: question, study: nil, dataset_item: dataset_item) + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it { is_expected.to validate_presence_of(:creator) } + + it 'can not be associated with a nonexistent dataset item' do + expect { + create(:response, question: question, study: study, dataset_item_id: 12345) + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it 'can not be associated with a nonexistent question' do + expect { + create(:response, question: question, study_id: 12345, dataset_item: dataset_item) + # not sure why the error is different for the two associations + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it 'can not be associated with a nonexistent study' do + expect { + create(:response, question_id: 12345, study: study, dataset_item: dataset_item) + # not sure why the error is different for the two associations + }.to raise_error(ActiveRecord::RecordInvalid) + end + + it 'does not allow unrelated parent question and parent study' do + other_study = create(:study) + expect { + create(:response, question_id: question.id, study_id: other_study.id, dataset_item: dataset_item) + }.to raise_error(ActiveRecord::RecordInvalid) + end + + end + end diff --git a/spec/requests/questions_spec.rb b/spec/requests/questions_spec.rb index 62a820b6..57ceb195 100644 --- a/spec/requests/questions_spec.rb +++ b/spec/requests/questions_spec.rb @@ -142,6 +142,7 @@ def question_url(question_id = nil, study_id = nil) expect(parsed_response["meta"]["error"]["info"]["studies"]).to eq(["can't be blank"]) end + end describe 'update question' do @@ -188,7 +189,7 @@ def question_url(question_id = nil, study_id = nil) expect(Question.find(question.id).study_ids).to eq(current_study_ids) end - + end end diff --git a/spec/requests/responses_spec.rb b/spec/requests/responses_spec.rb new file mode 100644 index 00000000..b21a3b6f --- /dev/null +++ b/spec/requests/responses_spec.rb @@ -0,0 +1,279 @@ +require 'rails_helper' +require 'rspec/mocks' + +def response_url(response_id = nil, study_id = nil) + + url = "/responses" + url = url + "/" + response_id.to_s if response_id + url = "/studies/#{study_id.to_s}" + url if study_id + return url + +end + +describe "responses" do + create_entire_hierarchy + create_study_hierarchy + + # create a bunch of studies, questions and responses to work with + create_many_studies + + # create two cs hierarchies + create_citizen_science_hierarchies(2) + + let(:response_attributes) { + FactoryGirl.attributes_for(:response) + } + + let(:update_response_attributes) { + {data: {some_key: 'updated response data'}.to_json } + } + + before(:each) do + @env ||= {} + @env['HTTP_AUTHORIZATION'] = admin_token + @env['CONTENT_TYPE'] = "application/json" + end + + describe 'index,filter,show responses' do + + describe 'index' do + + it 'finds all (1) responses as admin' do + get response_url, nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(1) + expect(parsed_response['data'][0]['data']).to eq(user_response['data']) + end + + it 'finds all (1) responses for the given study as admin' do + get response_url(nil, study.id), nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(1) + expect(parsed_response['data'][0]['data']).to eq(user_response['data']) + end + + it 'finds the correct responses for the given study as admin' do + + available_records = many_studies + + # find responses for the first study in many_studies + study_id = available_records[:studies][0].id + get response_url(nil, study_id), nil, @env + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(available_records[:studies][0].response_ids.count) + expect((parsed_response['data'].map { |q| q['id'] }).sort).to eq(available_records[:studies][0].response_ids.sort) + + study_id = available_records[:studies][1].id + get response_url(nil, study_id), nil, @env + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(available_records[:studies][1].response_ids.count) + expect((parsed_response['data'].map { |q| q['id'] }).sort).to eq(available_records[:studies][1].response_ids.sort) + + end + + end + + describe 'filter' do + + it 'finds all (1) responses as admin' do + get response_url + "/filter", nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(1) + expect(parsed_response['data'][0]['data']).to eq(response['data']) + end + + it 'finds responses to the study0 and study1 using studies.id' do + + available_records = many_studies + + url = response_url + "/filter" + + study0 = available_records[:studies][0] + study1 = available_records[:studies][1] + expected_response_ids = (study0.response_ids + study1.response_ids).uniq + filter_params = { filter: { 'studies.id' => { in: [study0.id, study1.id] }}} + post url, filter_params.to_json, @env + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(expected_response_ids.count) + expect((parsed_response['data'].map { |q| q['id'] }).sort).to eq(expected_response_ids.sort) + + end + + it 'finds responses to the study0 and study1 using study_id' do + + available_records = many_studies + + url = response_url + "/filter" + + study0 = available_records[:studies][0] + study1 = available_records[:studies][1] + expected_response_ids = (study0.response_ids + study1.response_ids).uniq + filter_params = { filter: { 'study_id' => { in: [study0.id, study1.id] }}} + post url, filter_params.to_json, @env + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].count).to eq(expected_response_ids.count) + expect((parsed_response['data'].map { |q| q['id'] }).sort).to eq(expected_response_ids.sort) + + end + + + end + + describe 'show' do + + it 'show response as admin' do + get response_url(user_response.id), nil, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response['data'].to_json).to eq(user_response.to_json) + end + + end + + end + + describe 'create and update' do + + describe 'create response' do + + it 'creates a response' do + params = { response: response_attributes } + params[:response][:study_id] = study.id + params[:response][:question_id] = question.id + params[:response][:dataset_item_id] = dataset_item.id + + post response_url, params.to_json, @env + parsed_response = JSON.parse(response.body) + expect(response).to have_http_status(201) + expect(parsed_response['data'].symbolize_keys.slice(*params[:response].keys)).to eq(params[:response]) + expected_keys = params[:response].keys.map(&:to_s) + %w(id creator_id created_at) + expect(parsed_response['data'].keys.sort).to eq(expected_keys.sort) + expect(Response.all.count).to eq(2) + end + + describe 'missing foreign key' do + + it 'cannot create a response with no study' do + params = { response: response_attributes } + params[:response][:question_id] = question.id + params[:response][:dataset_item_id] = dataset_item.id + post response_url, params.to_json, @env + expect(response).to have_http_status(422) + expect(Response.all.count).to eq(1) + end + + it 'cannot create a response with no dataset_item' do + params = { response: response_attributes } + params[:response][:question_id] = question.id + params[:response][:study_id] = study.id + post response_url, params.to_json, @env + expect(response).to have_http_status(422) + expect(Response.all.count).to eq(1) + end + + it 'cannot create a response with no question' do + params = { response: response_attributes } + params[:response][:dataset_item_id] = dataset_item.id + params[:response][:study_id] = study.id + post response_url, params.to_json, @env + expect(response).to have_http_status(422) + expect(Response.all.count).to eq(1) + end + + end + + it 'cannot create a response with no data' do + params = { response: {} } + params[:response][:dataset_item_id] = dataset_item.id + params[:response][:study_id] = study.id + params[:response][:question_id] = question.id + post response_url, params.to_json, @env + expect(response).to have_http_status(422) + expect(Response.all.count).to eq(1) + end + + + + # todo: + # These checks will slow down writing so removing this would be an option for speeding + # things up if necessary in the future + describe 'incompatible dependencies' do + + it 'ensures parent study and question are associated with each other' do + + # elements of citizen_science_hierarchies are not related to each other (except through audio_recording) + study_id = citizen_science_hierarchies[0][:study].id + question_id = citizen_science_hierarchies[1][:question].id + dataset_item_id = citizen_science_hierarchies[0][:dataset_item].id + + params = { response: response_attributes } + params[:response][:dataset_item_id] = dataset_item_id + params[:response][:study_id] = study_id + params[:response][:question_id] = question_id + count_before = Response.all.count + post response_url, params.to_json, @env + expect(response).to have_http_status(422) + expect(Response.all.count).to eq(count_before) + parsed_response = JSON.parse(response.body) + expect(parsed_response['meta']['error']['info']).to eq({"question_id"=>["parent question is not associated with parent study"]}) + + end + + it 'ensures parent study dataset item are associated with each other through dataset' do + + study_id = citizen_science_hierarchies[0][:study].id + question_id = citizen_science_hierarchies[0][:question].id + dataset_item_id = citizen_science_hierarchies[1][:dataset_item].id + + params = { response: response_attributes } + params[:response][:dataset_item_id] = dataset_item_id + params[:response][:study_id] = study_id + params[:response][:question_id] = question_id + count_before = Response.all.count + post response_url, params.to_json, @env + expect(response).to have_http_status(422) + expect(Response.all.count).to eq(count_before) + parsed_response = JSON.parse(response.body) + expect(parsed_response['meta']['error']['info']).to eq({"dataset_item_id"=>["dataset item and study must belong to the same dataset"]}) + + end + + end + + + end + + describe 'update response' do + + it 'cannot update a response' do + # params = {response: {text: 'modified response text'}}.to_json + # put response_url(response.id), params, @env + # parsed_response = JSON.parse(response.body) + # expect(response).to have_http_status(200) + # expect(parsed_response['data']['text']).to eq('modified response text') + end + + end + + end + + describe 'delete' do + + it 'deletes a response' do + + delete response_url(response.id), nil, @env + expect(response).to have_http_status(204) + expect(Response.all.count).to eq(0) + expect(Response.all.count).to eq(0) + + end + + end + +end + + + From b960810e8b733f99aae44efcc2477675261c1206 Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Fri, 18 Jan 2019 15:53:18 +1000 Subject: [PATCH 07/11] routing specs for studies, questions, responses --- app/controllers/responses_controller.rb | 9 + spec/acceptance/datasets_spec.rb | 140 +++---- spec/acceptance/studies_spec.rb | 492 ++++++++++++++++++++++++ spec/requests/responses_spec.rb | 15 +- spec/routing/questions_routing_spec.rb | 17 + spec/routing/responses_routing_spec.rb | 17 + spec/routing/studies_routing_spec.rb | 16 + 7 files changed, 631 insertions(+), 75 deletions(-) create mode 100644 spec/acceptance/studies_spec.rb create mode 100644 spec/routing/questions_routing_spec.rb create mode 100644 spec/routing/responses_routing_spec.rb create mode 100644 spec/routing/studies_routing_spec.rb diff --git a/app/controllers/responses_controller.rb b/app/controllers/responses_controller.rb index 2948d20d..33b61561 100644 --- a/app/controllers/responses_controller.rb +++ b/app/controllers/responses_controller.rb @@ -58,6 +58,15 @@ def create end end + # DELETE /responses/:id + def destroy + do_load_resource + do_authorize_instance + @response.destroy + respond_destroy + end + + private def response_params diff --git a/spec/acceptance/datasets_spec.rb b/spec/acceptance/datasets_spec.rb index fdcda085..4681ee25 100644 --- a/spec/acceptance/datasets_spec.rb +++ b/spec/acceptance/datasets_spec.rb @@ -94,75 +94,79 @@ def body_params # CREATE ################################ - post '/datasets' do - body_params - let(:raw_post) { {'dataset' => post_attributes}.to_json } - let(:authentication_token) { admin_token } - standard_request_options( - :post, - 'CREATE (as admin)', - :created, - {expected_json_path: 'data/name', response_body_content: 'New Dataset name'} - ) - end - - post '/datasets' do - body_params - let(:raw_post) { {'dataset' => post_attributes}.to_json } - let(:authentication_token) { reader_token } - standard_request_options( - :post, - 'CREATE (as reader user)', - :created, - {expected_json_path: 'data/name', response_body_content: 'New Dataset name'} - ) - end - - post '/datasets' do - body_params - let(:raw_post) { {'dataset' => post_attributes}.to_json } - let(:authentication_token) { no_access_token } - standard_request_options( - :post, - 'CREATE (as no access user)', - :created, - {expected_json_path: 'data/name', response_body_content: 'New Dataset name'} - ) - end + describe 'create' do + + post '/datasets' do + body_params + let(:raw_post) { {'dataset' => post_attributes}.to_json } + let(:authentication_token) { admin_token } + standard_request_options( + :post, + 'CREATE (as admin)', + :created, + {expected_json_path: 'data/name', response_body_content: 'New Dataset name'} + ) + end + + post '/datasets' do + body_params + let(:raw_post) { {'dataset' => post_attributes}.to_json } + let(:authentication_token) { reader_token } + standard_request_options( + :post, + 'CREATE (as reader user)', + :created, + {expected_json_path: 'data/name', response_body_content: 'New Dataset name'} + ) + end + + post '/datasets' do + body_params + let(:raw_post) { {'dataset' => post_attributes}.to_json } + let(:authentication_token) { no_access_token } + standard_request_options( + :post, + 'CREATE (as no access user)', + :created, + {expected_json_path: 'data/name', response_body_content: 'New Dataset name'} + ) + end + + post '/datasets' do + body_params + let(:raw_post) { {'dataset' => post_attributes}.to_json } + let(:authentication_token) { invalid_token } + standard_request_options( + :post, + 'CREATE (invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/datasets' do + body_params + let(:raw_post) { {'dataset' => post_attributes}.to_json } + standard_request_options( + :post, + 'CREATE (as anonymous user)', + :unauthorized, + {remove_auth: true, expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/datasets' do + body_params + let(:raw_post) { {'dataset' => post_attributes}.to_json } + let(:authentication_token) { harvester_token } + standard_request_options( + :post, + 'CREATE (as harvester user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end - post '/datasets' do - body_params - let(:raw_post) { {'dataset' => post_attributes}.to_json } - let(:authentication_token) { invalid_token } - standard_request_options( - :post, - 'CREATE (invalid token)', - :unauthorized, - {expected_json_path: get_json_error_path(:sign_up)} - ) - end - - post '/datasets' do - body_params - let(:raw_post) { {'dataset' => post_attributes}.to_json } - standard_request_options( - :post, - 'CREATE (as anonymous user)', - :unauthorized, - {remove_auth: true, expected_json_path: get_json_error_path(:sign_up)} - ) - end - - post '/datasets' do - body_params - let(:raw_post) { {'dataset' => post_attributes}.to_json } - let(:authentication_token) { harvester_token } - standard_request_options( - :post, - 'CREATE (as harvester user)', - :forbidden, - {expected_json_path: get_json_error_path(:permissions)} - ) end ################################ diff --git a/spec/acceptance/studies_spec.rb b/spec/acceptance/studies_spec.rb new file mode 100644 index 00000000..b6021478 --- /dev/null +++ b/spec/acceptance/studies_spec.rb @@ -0,0 +1,492 @@ +require 'rails_helper' +require 'rspec_api_documentation/dsl' +require 'helpers/acceptance_spec_helper' + +def id_params + parameter :id, 'Study id in request url', required: true +end + +def body_params + parameter :name, 'Name of study', scope: :study, :required => true + parameter :description, 'Description of study', scope: :study +end + +# https://github.com/zipmark/rspec_api_documentation +resource 'Studies' do + + header 'Accept', 'application/json' + header 'Content-Type', 'application/json' + header 'Authorization', :authentication_token + + let(:format) { 'json' } + + create_entire_hierarchy + create_study_hierarchy + + # Create post parameters from factory + let(:post_attributes) { FactoryGirl.attributes_for(:study, name: 'New Study name') } + + ################################ + # INDEX + ################################ + + describe 'index' do + + get '/studies' do + let(:authentication_token) { admin_token } + standard_request_options( + :get, + 'INDEX (as admin)', + :ok, + {expected_json_path: 'data/0/name', data_item_count: 1} + ) + end + + # writer and owner user don't need tests because studies don't derive permissions from projects + + get '/studies' do + let(:authentication_token) { reader_token } + standard_request_options( + :get, + 'INDEX (as reader user)', + :ok, + {expected_json_path: 'data/0/name', data_item_count: 1} + ) + end + + get '/studies' do + let(:authentication_token) { invalid_token } + standard_request_options( + :get, + 'INDEX (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/studies' do + standard_request_options( + :get, + 'INDEX (as anonymous user)', + :ok, + {remove_auth: true, expected_json_path: 'data/0/name', data_item_count: 1} + ) + end + + + get '/studies' do + let(:authentication_token) { harvester_token } + standard_request_options( + :get, + 'INDEX (as harvester)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + ################################ + # CREATE + ################################ + + describe 'create studies' do + + post '/studies' do + body_params + let(:raw_post) { {'study' => post_attributes}.to_json } + let(:authentication_token) { admin_token } + standard_request_options( + :post, + 'CREATE (as admin)', + :created, + {expected_json_path: 'data/name', response_body_content: 'New Study name'} + ) + end + + post '/studies' do + body_params + let(:raw_post) { {'study' => post_attributes}.to_json } + let(:authentication_token) { reader_token } + standard_request_options( + :post, + 'CREATE (as reader user)', + :created, + {expected_json_path: 'data/name', response_body_content: 'New Study name'} + ) + end + + post '/studies' do + body_params + let(:raw_post) { {'study' => post_attributes}.to_json } + let(:authentication_token) { no_access_token } + standard_request_options( + :post, + 'CREATE (as no access user)', + :created, + {expected_json_path: 'data/name', response_body_content: 'New Study name'} + ) + end + + post '/studies' do + body_params + let(:raw_post) { {'study' => post_attributes}.to_json } + let(:authentication_token) { invalid_token } + standard_request_options( + :post, + 'CREATE (invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/studies' do + body_params + let(:raw_post) { {'study' => post_attributes}.to_json } + standard_request_options( + :post, + 'CREATE (as anonymous user)', + :unauthorized, + {remove_auth: true, expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/studies' do + body_params + let(:raw_post) { {'study' => post_attributes}.to_json } + let(:authentication_token) { harvester_token } + standard_request_options( + :post, + 'CREATE (as harvester user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + # ################################ + # # NEW + # ################################ + # + # get '/studies/new' do + # let(:authentication_token) { admin_token } + # standard_request_options( + # :get, + # 'NEW (as admin)', + # :ok, + # {expected_json_path: 'data/name/'} + # ) + # end + # + # get '/studies/new' do + # let(:authentication_token) { no_access_token } + # standard_request_options( + # :get, + # 'NEW (as non admin user)', + # :ok, + # {expected_json_path: 'data/name'} + # ) + # end + # + # get '/studies/new' do + # let(:authentication_token) { invalid_token } + # standard_request_options( + # :get, + # 'NEW (with invalid token)', + # :unauthorized, + # {expected_json_path: get_json_error_path(:sign_up)} + # ) + # end + # + # get '/studies/new' do + # standard_request_options( + # :get, + # 'NEW (as anonymous user)', + # :ok, + # {expected_json_path: 'data/name'} + # ) + # end + # + # get '/studies/new' do + # let(:authentication_token) { harvester_token } + # standard_request_options( + # :get, + # 'NEW (as harvester user)', + # :forbidden, + # {expected_json_path: get_json_error_path(:permissions)} + # ) + # end + # + # ################################ + # # SHOW + # ################################ + # + # get '/studies/:id' do + # id_params + # let(:id) { study.id } + # let(:authentication_token) { admin_token } + # standard_request_options( + # :get, + # 'SHOW (as admin)', + # :ok, + # {expected_json_path: 'data/name'} + # ) + # end + # + # get '/studies/:id' do + # id_params + # let(:id) { study.id } + # let(:authentication_token) { no_access_token } + # standard_request_options( + # :get, + # 'SHOW (as no access user)', + # :ok, + # {expected_json_path: 'data/name'} + # ) + # end + # + # get '/studies/:id' do + # id_params + # let(:id) { study.id } + # let(:authentication_token) { reader_token } + # standard_request_options( + # :get, + # 'SHOW (as reader user)', + # :ok, + # {expected_json_path: 'data/name'} + # ) + # end + # + # get '/studies/:id' do + # id_params + # let(:id) { study.id } + # let(:authentication_token) { invalid_token } + # standard_request_options( + # :get, + # 'SHOW (with invalid token)', + # :unauthorized, + # {expected_json_path: get_json_error_path(:sign_up)} + # ) + # end + # + # get '/studies/:id' do + # id_params + # let(:id) { study.id } + # standard_request_options( + # :get, + # 'SHOW (an anonymous user)', + # :ok, + # {expected_json_path: 'data/name'} + # ) + # end + # + # + # ################################ + # # UPDATE + # ################################ + # + # + # # add tests for creator + # + # put '/studies/:id' do + # body_params + # let(:id) { study.id } + # let(:raw_post) { {study: post_attributes}.to_json } + # let(:authentication_token) { admin_token } + # standard_request_options( + # :put, + # 'UPDATE (as admin)', + # :ok, + # {expected_json_path: 'data/name', response_body_content: 'New Study name'} + # ) + # end + # + # # owner user is the user who is the creator of the study + # # can therefore update the study + # put '/studies/:id' do + # body_params + # let(:id) { study.id } + # let(:raw_post) { {study: post_attributes}.to_json } + # let(:authentication_token) { owner_token } + # standard_request_options( + # :put, + # 'UPDATE (as owner user)', + # :ok, + # {expected_json_path: 'data/name', response_body_content: 'New Study name'} + # ) + # end + # + # + # put '/studies/:id' do + # body_params + # let(:id) { study.id } + # let(:raw_post) { {study: post_attributes}.to_json } + # let(:authentication_token) { reader_token } + # standard_request_options( + # :put, + # 'UPDATE (as reader user)', + # :forbidden, + # {expected_json_path: get_json_error_path(:permissions)} + # ) + # end + # + # put '/studies/:id' do + # body_params + # let(:id) { study.id } + # let(:raw_post) { {study: post_attributes}.to_json } + # let(:authentication_token) { no_access_token } + # standard_request_options( + # :put, + # 'UPDATE (as no access user)', + # :forbidden, + # {expected_json_path: get_json_error_path(:permissions)} + # ) + # end + # + # put '/studies/:id' do + # body_params + # let(:id) { study.id } + # let(:raw_post) { {study: post_attributes}.to_json } + # let(:authentication_token) { invalid_token } + # standard_request_options( + # :put, + # 'UPDATE (with invalid token)', + # :unauthorized, + # {expected_json_path: get_json_error_path(:sign_up)} + # ) + # end + # + # put '/studies/:id' do + # body_params + # let(:id) { study.id } + # let(:raw_post) { {study: post_attributes}.to_json } + # standard_request_options( + # :put, + # 'UPDATE (as anonymous user)', + # :unauthorized, + # {remove_auth: true, expected_json_path: get_json_error_path(:sign_in)} + # ) + # end + # + # put '/studies/:id' do + # body_params + # let(:id) { study.id } + # let(:raw_post) { {study: post_attributes}.to_json } + # let(:authentication_token) { harvester_token } + # standard_request_options( + # :put, + # 'UPDATE (with harvester token)', + # :forbidden, + # {expected_json_path: get_json_error_path(:permissions)} + # ) + # end + # + # ################################ + # # DESTROY + # ################################ + # + # # Destroy is not implemented, so just one test for expected 404 + # delete '/studies/:id' do + # body_params + # let(:id) { study.id } + # let(:authentication_token) { admin_token } + # standard_request_options( + # :delete, + # 'DESTROY (as admin)', + # :not_found, + # {expected_json_path: 'meta/error/info/original_route', response_body_content: 'Could not find'} + # ) + # end + # + # ################################ + # # FILTER + # ################################ + # + # + # # Basic filter with no conditions is expected count is 2 because of the 'default study'. + # # The 'default study' exists as seed data in a clean install + # # for no filter constraints, use the following opts + # basic_filter_opts = { + # response_body_content: ['The default study', 'gen_study'], + # expected_json_path: 'data/0/name', + # data_item_count: 2 + # } + # + # post '/studies/filter' do + # let(:authentication_token) { admin_token } + # standard_request_options(:post,'FILTER (as admin)',:ok, basic_filter_opts) + # end + # + # post '/studies/filter' do + # let(:authentication_token) { owner_token } + # standard_request_options(:post,'FILTER (as owner user)',:ok, basic_filter_opts) + # end + # + # post '/studies/filter' do + # let(:authentication_token) { writer_token } + # standard_request_options(:post,'FILTER (as writer user)',:ok, basic_filter_opts) + # end + # + # post '/studies/filter' do + # let(:authentication_token) { reader_token } + # standard_request_options(:post,'FILTER (as reader user)',:ok, basic_filter_opts) + # end + # + # post '/studies/filter' do + # let(:authentication_token) { no_access_token } + # standard_request_options(:post,'FILTER (as no access user)',:ok, basic_filter_opts) + # end + # + # post '/studies/filter' do + # let(:authentication_token) { invalid_token } + # standard_request_options( + # :post, + # 'FILTER (with invalid token)', + # :unauthorized, + # {expected_json_path: get_json_error_path(:sign_up)} + # ) + # end + # + # post '/studies/filter' do + # standard_request_options(:post,'FILTER (as anonymous user)',:ok, basic_filter_opts) + # end + # + # post '/studies/filter' do + # let(:authentication_token) { harvester_token } + # standard_request_options( + # :post, + # 'FILTER (with harvester token)', + # :forbidden, + # {response_body_content: ['"data":null'], expected_json_path: get_json_error_path(:permissions)} + # ) + # end + # + # post '/studies/filter' do + # let(:raw_post) { + # { + # filter: { + # name: { + # eq: 'default' + # } + # } + # # projection: { + # # include: [:name, :description] + # # } + # }.to_json + # } + # let(:authentication_token) { reader_token } + # standard_request_options( + # :post, + # 'FILTER (with admin token: filter by name with projection)', + # :ok, + # { + # response_body_content: ['The default study'], + # expected_json_path: 'data/0/name', + # data_item_count: 1 + # } + # ) + # end + + +end \ No newline at end of file diff --git a/spec/requests/responses_spec.rb b/spec/requests/responses_spec.rb index b21a3b6f..3e43ee3e 100644 --- a/spec/requests/responses_spec.rb +++ b/spec/requests/responses_spec.rb @@ -32,6 +32,7 @@ def response_url(response_id = nil, study_id = nil) @env ||= {} @env['HTTP_AUTHORIZATION'] = admin_token @env['CONTENT_TYPE'] = "application/json" + @env['ACCEPT'] = "application/json" end describe 'index,filter,show responses' do @@ -249,11 +250,12 @@ def response_url(response_id = nil, study_id = nil) describe 'update response' do it 'cannot update a response' do - # params = {response: {text: 'modified response text'}}.to_json - # put response_url(response.id), params, @env - # parsed_response = JSON.parse(response.body) - # expect(response).to have_http_status(200) - # expect(parsed_response['data']['text']).to eq('modified response text') + data = {some_answer:'modified response text'}.to_json + params = {response: {data: data}}.to_json + put response_url(user_response.id), params, @env + expect(response).to have_http_status(404) + parsed_response = JSON.parse(response.body) + expect(parsed_response['meta']['message']).to eq('Not Found') end end @@ -264,10 +266,9 @@ def response_url(response_id = nil, study_id = nil) it 'deletes a response' do - delete response_url(response.id), nil, @env + delete response_url(user_response.id), nil, @env expect(response).to have_http_status(204) expect(Response.all.count).to eq(0) - expect(Response.all.count).to eq(0) end diff --git a/spec/routing/questions_routing_spec.rb b/spec/routing/questions_routing_spec.rb new file mode 100644 index 00000000..b3a389ce --- /dev/null +++ b/spec/routing/questions_routing_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe QuestionsController, :type => :routing do + describe :routing do + + it { expect(get('/questions')).to route_to('questions#index', format: 'json') } + it { expect(get('studies/1/questions')).to route_to('questions#index', study_id: '1', format: 'json') } + it { expect(get('/questions/1')).to route_to('questions#show', id: '1', format: 'json') } + it { expect(get('/questions/new')).to route_to('questions#new', format: 'json') } + it { expect(get('/questions/filter')).to route_to('questions#filter', format: 'json') } + it { expect(post('/questions/filter')).to route_to('questions#filter', format: 'json') } + it { expect(post('/questions')).to route_to('questions#create', format: 'json') } + it { expect(put('/questions/1')).to route_to('questions#update', id: '1', format: 'json') } + it { expect(delete('/questions/1')).to route_to('questions#destroy', id: '1', format: 'json') } + + end +end \ No newline at end of file diff --git a/spec/routing/responses_routing_spec.rb b/spec/routing/responses_routing_spec.rb new file mode 100644 index 00000000..5c16f393 --- /dev/null +++ b/spec/routing/responses_routing_spec.rb @@ -0,0 +1,17 @@ +require 'rails_helper' + +describe ResponsesController, :type => :routing do + describe :routing do + + it { expect(get('/responses')).to route_to('responses#index', format: 'json') } + it { expect(get('studies/1/responses')).to route_to('responses#index', study_id: '1', format: 'json') } + it { expect(get('/responses/1')).to route_to('responses#show', id: '1', format: 'json') } + it { expect(get('/responses/new')).to route_to('responses#new', format: 'json') } + it { expect(get('/responses/filter')).to route_to('responses#filter', format: 'json') } + it { expect(post('/responses/filter')).to route_to('responses#filter', format: 'json') } + it { expect(post('/responses')).to route_to('responses#create', format: 'json') } + it { expect(put('/responses/1')).to route_to('errors#route_error', requested_route: 'responses/1') } + it { expect(delete('/responses/1')).to route_to('responses#destroy', id: '1', format: 'json') } + + end +end \ No newline at end of file diff --git a/spec/routing/studies_routing_spec.rb b/spec/routing/studies_routing_spec.rb new file mode 100644 index 00000000..6b3831b7 --- /dev/null +++ b/spec/routing/studies_routing_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +describe StudiesController, :type => :routing do + describe :routing do + + it { expect(get('/studies')).to route_to('studies#index', format: 'json') } + it { expect(get('/studies/1')).to route_to('studies#show', id: '1', format: 'json') } + it { expect(get('/studies/new')).to route_to('studies#new', format: 'json') } + it { expect(get('/studies/filter')).to route_to('studies#filter', format: 'json') } + it { expect(post('/studies/filter')).to route_to('studies#filter', format: 'json') } + it { expect(post('/studies')).to route_to('studies#create', format: 'json') } + it { expect(put('/studies/1')).to route_to('studies#update', id: '1', format: 'json') } + it { expect(delete('/studies/1')).to route_to('studies#destroy', id: '1', format: 'json') } + + end +end \ No newline at end of file From c41f5f0f25f98c0a501e50de82921d5b2125d221 Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Thu, 24 Jan 2019 16:43:56 +1000 Subject: [PATCH 08/11] acceptance specs for studies and questions --- app/models/ability.rb | 7 +- spec/acceptance/datasets_spec.rb | 9 +- spec/acceptance/questions_spec.rb | 619 ++++++++++++++++++++++++ spec/acceptance/studies_spec.rb | 780 +++++++++++++++++------------- spec/lib/creation.rb | 2 + spec/requests/datasets_spec.rb | 25 + spec/requests/studies_spec.rb | 20 + 7 files changed, 1124 insertions(+), 338 deletions(-) create mode 100644 spec/acceptance/questions_spec.rb diff --git a/app/models/ability.rb b/app/models/ability.rb index 48c9fd71..d2842cac 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -646,7 +646,7 @@ def to_study(user, is_guest) # only admin can create, update, delete # all users including guest can access any get request - can [:new, :index, :filter, :view], Study + can [:new, :index, :filter, :show], Study end @@ -657,7 +657,7 @@ def to_question(user, is_guest) # only admin create, update, delete # only logged in users can view questions - can [:index, :filter, :view], Question unless is_guest + can [:index, :filter, :show], Question unless is_guest end @@ -676,7 +676,8 @@ def to_response(user, is_guest) end # users can only view their own responses - can [:index, :filter, :view], Response, creator_id: user.id + # therefore guest can not view any responses + can [:index, :filter, :show], Response, creator_id: user.id unless is_guest # only admin can update or delete responses diff --git a/spec/acceptance/datasets_spec.rb b/spec/acceptance/datasets_spec.rb index 4681ee25..e47e128c 100644 --- a/spec/acceptance/datasets_spec.rb +++ b/spec/acceptance/datasets_spec.rb @@ -473,10 +473,10 @@ def body_params name: { eq: 'default' } + }, + projection: { + include: ["name", "description"] } - # projection: { - # include: [:name, :description] - # } }.to_json } let(:authentication_token) { reader_token } @@ -485,12 +485,11 @@ def body_params 'FILTER (with admin token: filter by name with projection)', :ok, { - response_body_content: ['The default dataset'], + #response_body_content: ['The default dataset'], expected_json_path: 'data/0/name', data_item_count: 1 } ) end - end \ No newline at end of file diff --git a/spec/acceptance/questions_spec.rb b/spec/acceptance/questions_spec.rb new file mode 100644 index 00000000..b5cb6bd6 --- /dev/null +++ b/spec/acceptance/questions_spec.rb @@ -0,0 +1,619 @@ +require 'rails_helper' +require 'rspec_api_documentation/dsl' +require 'helpers/acceptance_spec_helper' + +def id_params + parameter :id, 'Question id in request url', required: true +end + +def body_params + parameter :text, 'Name of question', scope: :question, :required => true + parameter :data, 'Description of question', scope: :question + parameter :study_ids, 'IDs of studies', scope: :question, :required => true +end + +def basic_filter_opts + { + #response_body_content: ['test question'], + expected_json_path: ['data/0/text', 'data/0/data'], + data_item_count: 1 + } +end + +# https://github.com/zipmark/rspec_api_documentation +resource 'Questions' do + + header 'Accept', 'application/json' + header 'Content-Type', 'application/json' + header 'Authorization', :authentication_token + + let(:format) { 'json' } + + create_entire_hierarchy + create_study_hierarchy + + # Create post parameters from factory + let(:post_attributes) { FactoryGirl.attributes_for(:question, text: 'New Question text', study_ids: [study.id]) } + + + # reader, writer and owner should all act the same, because questions don't derive permissions from projects + shared_examples_for 'Questions results' do |current_user| + + let(:current_user) { current_user } + + def token(target) + target.send((current_user.to_s + '_token').to_sym) + end + + # INDEX + get '/questions' do + let(:authentication_token) { token(self) } + standard_request_options( + :get, + "Any user, including #{current_user.to_s}, can access", + :ok, + basic_filter_opts + ) + end + + # CREATE + post '/questions' do + body_params + let(:raw_post) { {'question' => post_attributes}.to_json } + let(:authentication_token) { token(self) } + standard_request_options( + :post, + "Non-admin, including #{current_user.to_s}, cannot create", + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + # NEW + get '/questions/new' do + let(:authentication_token) { token(self) } + standard_request_options( + :get, + "Any user, including #{current_user.to_s}, can access", + :ok, + {expected_json_path: ['data/text', 'data/data']} + ) + end + + # SHOW + get '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { token(self) } + standard_request_options( + :get, + "Any user, including #{current_user.to_s}, can access", + :ok, + {expected_json_path: ['data/text', 'data/data']} + ) + end + + # UPDATE + put '/questions/:id' do + body_params + let(:id) { question.id } + let(:raw_post) { {question: post_attributes}.to_json } + let(:authentication_token) { token(self) } + standard_request_options( + :put, + "Non-admin, including #{current_user.to_s}, cannot update", + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + # FILTER + + post '/questions/filter' do + let(:authentication_token) { token(self) } + standard_request_options( + :post, + "Any user, including #{current_user.to_s}, can access", + :ok, + basic_filter_opts) + end + + + + + end + + + + + ################################ + # INDEX + ################################ + + describe 'index' do + + get '/questions' do + let(:authentication_token) { admin_token } + standard_request_options( + :get, + 'INDEX (as admin)', + :ok, + basic_filter_opts + ) + end + + get '/questions' do + let(:authentication_token) { invalid_token } + standard_request_options( + :get, + 'INDEX (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/questions' do + standard_request_options( + :get, + 'INDEX (as anonymous user)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + + get '/questions' do + let(:authentication_token) { harvester_token } + standard_request_options( + :get, + 'INDEX (as harvester)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + ################################ + # CREATE + ################################ + + describe 'create questions' do + + post '/questions' do + body_params + let(:raw_post) { {'question' => post_attributes}.to_json } + let(:authentication_token) { admin_token } + standard_request_options( + :post, + 'CREATE (as admin)', + :created, + {expected_json_path: ['data/text','data/data'], response_body_content: 'New Question text'} + ) + end + + post '/questions' do + body_params + let(:raw_post) { {'question' => post_attributes}.to_json } + let(:dataset_id) { dataset.id} + let(:authentication_token) { no_access_token } + standard_request_options( + :post, + 'CREATE (as no access user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + post '/questions' do + body_params + let(:raw_post) { {'question' => post_attributes}.to_json } + let(:authentication_token) { invalid_token } + standard_request_options( + :post, + 'CREATE (invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/questions' do + body_params + let(:raw_post) { {'question' => post_attributes}.to_json } + standard_request_options( + :post, + 'CREATE (as anonymous user)', + :unauthorized, + {remove_auth: true, expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/questions' do + body_params + let(:raw_post) { {'question' => post_attributes}.to_json } + let(:authentication_token) { harvester_token } + standard_request_options( + :post, + 'CREATE (as harvester user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + # ################################ + # # NEW + # ################################ + + describe 'new' do + + get '/questions/new' do + let(:authentication_token) { admin_token } + standard_request_options( + :get, + 'NEW (as admin)', + :ok, + {expected_json_path: ['data/text','data/data']} + ) + end + + get '/questions/new' do + let(:authentication_token) { no_access_token } + standard_request_options( + :get, + 'NEW (as non admin user)', + :ok, + {expected_json_path: ['data/text','data/data']} + ) + end + + get '/questions/new' do + let(:authentication_token) { invalid_token } + standard_request_options( + :get, + 'NEW (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/questions/new' do + standard_request_options( + :get, + 'NEW (as anonymous user)', + :ok, + {expected_json_path: ['data/text','data/data']} + ) + end + + get '/questions/new' do + let(:authentication_token) { harvester_token } + standard_request_options( + :get, + 'NEW (as harvester user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + # ################################ + # # SHOW + # ################################ + + describe 'show' do + + get '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { admin_token } + standard_request_options( + :get, + 'SHOW (as admin)', + :ok, + {expected_json_path: ['data/text','data/data']} + ) + end + + get '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { no_access_token } + standard_request_options( + :get, + 'SHOW (as no access user)', + :ok, + {expected_json_path: ['data/text','data/data']} + ) + end + + get '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { invalid_token } + standard_request_options( + :get, + 'SHOW (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/questions/:id' do + id_params + let(:id) { question.id } + standard_request_options( + :get, + 'SHOW (as anonymous user)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { harvester_token } + standard_request_options( + :get, + 'SHOW (as harvester)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + # + # ################################ + # # UPDATE + # ################################ + + describe 'update' do + + put '/questions/:id' do + body_params + let(:id) { question.id } + let(:raw_post) { {question: post_attributes}.to_json } + let(:authentication_token) { admin_token } + standard_request_options( + :put, + 'UPDATE (as admin)', + :ok, + {expected_json_path: ['data/text','data/data'], response_body_content: 'New Question text'} + ) + end + + put '/questions/:id' do + body_params + let(:id) { question.id } + let(:raw_post) { {question: post_attributes}.to_json } + let(:authentication_token) { no_access_token } + standard_request_options( + :put, + 'UPDATE (as no access user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + put '/questions/:id' do + body_params + let(:id) { question.id } + let(:raw_post) { {question: post_attributes}.to_json } + let(:authentication_token) { invalid_token } + standard_request_options( + :put, + 'UPDATE (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + put '/questions/:id' do + body_params + let(:id) { question.id } + let(:raw_post) { {question: post_attributes}.to_json } + standard_request_options( + :put, + 'UPDATE (as anonymous user)', + :unauthorized, + {remove_auth: true, expected_json_path: get_json_error_path(:sign_in)} + ) + end + + put '/questions/:id' do + body_params + let(:id) { question.id } + let(:raw_post) { {question: post_attributes}.to_json } + let(:authentication_token) { harvester_token } + standard_request_options( + :put, + 'UPDATE (with harvester token)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + # ################################ + # # DESTROY + # ################################ + + describe 'destroy' do + + delete '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { admin_token } + standard_request_options( + :delete, + 'DESTROY (as admin)', + :no_content, + {expected_response_has_content: false, expected_response_content_type: nil} + ) + end + + delete '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { owner_token } + standard_request_options( + :delete, + 'DESTROY (as owner user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + delete '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { reader_token } + standard_request_options( + :delete, + 'DESTROY (as reader user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + delete '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { no_access_token } + standard_request_options( + :delete, + 'DESTROY (as no access user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + delete '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { invalid_token } + standard_request_options( + :delete, + 'DESTROY (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + delete '/questions/:id' do + id_params + let(:id) { question.id } + let(:raw_post) { {question: post_attributes}.to_json } + standard_request_options( + :delete, + 'DESTROY (as anonymous user)', + :unauthorized, + {remove_auth: true, expected_json_path: get_json_error_path(:sign_in)} + ) + end + + delete '/questions/:id' do + id_params + let(:id) { question.id } + let(:authentication_token) { harvester_token } + standard_request_options( + :delete, + 'DESTROY (with harvester token)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + # ################################ + # # FILTER + # ################################ + + + describe 'filter' do + + post '/questions/filter' do + let(:authentication_token) { admin_token } + standard_request_options(:post,'FILTER (as admin)',:ok, basic_filter_opts) + end + + post '/questions/filter' do + let(:authentication_token) { no_access_token } + standard_request_options(:post,'FILTER (as no access user)',:ok, basic_filter_opts) + end + + post '/questions/filter' do + let(:authentication_token) { invalid_token } + standard_request_options( + :post, + 'FILTER (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/questions/filter' do + standard_request_options( + :post, + 'FILTER (as anonymous user)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)}) + end + + post '/questions/filter' do + let(:authentication_token) { harvester_token } + standard_request_options( + :post, + 'FILTER (with harvester token)', + :forbidden, + {response_body_content: ['"data":null'], expected_json_path: get_json_error_path(:permissions)} + ) + end + + post '/questions/filter' do + let(:raw_post) { + { + filter: { + text: { + starts_with: 'test question' + } + }, + projection: { + include: [:text] + } + }.to_json + } + let(:authentication_token) { reader_token } + standard_request_options( + :post, + 'FILTER (with admin token: filter by name with projection)', + :ok, + { + #response_body_content: ['Test question'], + expected_json_path: 'data/0/text', + data_item_count: 1 + } + ) + end + + end + + + describe 'Owner user' do + it_should_behave_like 'Questions results', :owner + end + + describe 'Writer user' do + it_should_behave_like 'Questions results', :writer + end + + describe 'Reader user' do + it_should_behave_like 'Questions results', :reader + end + +end \ No newline at end of file diff --git a/spec/acceptance/studies_spec.rb b/spec/acceptance/studies_spec.rb index b6021478..26c14b8e 100644 --- a/spec/acceptance/studies_spec.rb +++ b/spec/acceptance/studies_spec.rb @@ -9,6 +9,15 @@ def id_params def body_params parameter :name, 'Name of study', scope: :study, :required => true parameter :description, 'Description of study', scope: :study + parameter :dataset_id, 'ID of dataset', scope: :study, :required => true +end + +def basic_filter_opts + { + #response_body_content: ['test study'], + expected_json_path: 'data/0/name', + data_item_count: 1 + } end # https://github.com/zipmark/rspec_api_documentation @@ -24,31 +33,110 @@ def body_params create_study_hierarchy # Create post parameters from factory - let(:post_attributes) { FactoryGirl.attributes_for(:study, name: 'New Study name') } + let(:post_attributes) { FactoryGirl.attributes_for(:study, name: 'New Study name', dataset_id: dataset.id) } - ################################ - # INDEX - ################################ - describe 'index' do + # reader, writer and owner should all act the same, because studies don't derive permissions from projects + shared_examples_for 'Studies results' do |current_user| + let(:current_user) { current_user } + + def token(target) + target.send((current_user.to_s + '_token').to_sym) + end + + # INDEX get '/studies' do - let(:authentication_token) { admin_token } + let(:authentication_token) { token(self) } standard_request_options( :get, - 'INDEX (as admin)', + "Any user, including #{current_user.to_s}, can access", :ok, {expected_json_path: 'data/0/name', data_item_count: 1} ) end - # writer and owner user don't need tests because studies don't derive permissions from projects + # CREATE + post '/studies' do + body_params + let(:raw_post) { {'study' => post_attributes}.to_json } + let(:authentication_token) { token(self) } + standard_request_options( + :post, + "Non-admin, including #{current_user.to_s}, cannot create", + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + # NEW + get '/studies/new' do + let(:authentication_token) { token(self) } + standard_request_options( + :get, + "Any user, including #{current_user.to_s}, can access", + :ok, + {expected_json_path: 'data/name'} + ) + end + + # SHOW + get '/studies/:id' do + id_params + let(:id) { study.id } + let(:authentication_token) { token(self) } + standard_request_options( + :get, + "Any user, including #{current_user.to_s}, can access", + :ok, + {expected_json_path: 'data/name'} + ) + end + + # UPDATE + put '/studies/:id' do + body_params + let(:id) { study.id } + let(:raw_post) { {study: post_attributes}.to_json } + let(:authentication_token) { token(self) } + standard_request_options( + :put, + "Non-admin, including #{current_user.to_s}, cannot update", + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + # FILTER + + post '/studies/filter' do + let(:authentication_token) { token(self) } + standard_request_options( + :post, + "Any user, including #{current_user.to_s}, can access", + :ok, + basic_filter_opts) + end + + + + + end + + + + + ################################ + # INDEX + ################################ + + describe 'index' do get '/studies' do - let(:authentication_token) { reader_token } + let(:authentication_token) { admin_token } standard_request_options( :get, - 'INDEX (as reader user)', + 'INDEX (as admin)', :ok, {expected_json_path: 'data/0/name', data_item_count: 1} ) @@ -107,24 +195,13 @@ def body_params post '/studies' do body_params let(:raw_post) { {'study' => post_attributes}.to_json } - let(:authentication_token) { reader_token } - standard_request_options( - :post, - 'CREATE (as reader user)', - :created, - {expected_json_path: 'data/name', response_body_content: 'New Study name'} - ) - end - - post '/studies' do - body_params - let(:raw_post) { {'study' => post_attributes}.to_json } + let(:dataset_id) { dataset.id} let(:authentication_token) { no_access_token } standard_request_options( :post, 'CREATE (as no access user)', - :created, - {expected_json_path: 'data/name', response_body_content: 'New Study name'} + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} ) end @@ -168,325 +245,368 @@ def body_params # ################################ # # NEW # ################################ - # - # get '/studies/new' do - # let(:authentication_token) { admin_token } - # standard_request_options( - # :get, - # 'NEW (as admin)', - # :ok, - # {expected_json_path: 'data/name/'} - # ) - # end - # - # get '/studies/new' do - # let(:authentication_token) { no_access_token } - # standard_request_options( - # :get, - # 'NEW (as non admin user)', - # :ok, - # {expected_json_path: 'data/name'} - # ) - # end - # - # get '/studies/new' do - # let(:authentication_token) { invalid_token } - # standard_request_options( - # :get, - # 'NEW (with invalid token)', - # :unauthorized, - # {expected_json_path: get_json_error_path(:sign_up)} - # ) - # end - # - # get '/studies/new' do - # standard_request_options( - # :get, - # 'NEW (as anonymous user)', - # :ok, - # {expected_json_path: 'data/name'} - # ) - # end - # - # get '/studies/new' do - # let(:authentication_token) { harvester_token } - # standard_request_options( - # :get, - # 'NEW (as harvester user)', - # :forbidden, - # {expected_json_path: get_json_error_path(:permissions)} - # ) - # end - # + + describe 'new' do + + get '/studies/new' do + let(:authentication_token) { admin_token } + standard_request_options( + :get, + 'NEW (as admin)', + :ok, + {expected_json_path: 'data/name/'} + ) + end + + get '/studies/new' do + let(:authentication_token) { no_access_token } + standard_request_options( + :get, + 'NEW (as non admin user)', + :ok, + {expected_json_path: 'data/name'} + ) + end + + get '/studies/new' do + let(:authentication_token) { invalid_token } + standard_request_options( + :get, + 'NEW (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/studies/new' do + standard_request_options( + :get, + 'NEW (as anonymous user)', + :ok, + {expected_json_path: 'data/name'} + ) + end + + get '/studies/new' do + let(:authentication_token) { harvester_token } + standard_request_options( + :get, + 'NEW (as harvester user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + # ################################ # # SHOW # ################################ - # - # get '/studies/:id' do - # id_params - # let(:id) { study.id } - # let(:authentication_token) { admin_token } - # standard_request_options( - # :get, - # 'SHOW (as admin)', - # :ok, - # {expected_json_path: 'data/name'} - # ) - # end - # - # get '/studies/:id' do - # id_params - # let(:id) { study.id } - # let(:authentication_token) { no_access_token } - # standard_request_options( - # :get, - # 'SHOW (as no access user)', - # :ok, - # {expected_json_path: 'data/name'} - # ) - # end - # - # get '/studies/:id' do - # id_params - # let(:id) { study.id } - # let(:authentication_token) { reader_token } - # standard_request_options( - # :get, - # 'SHOW (as reader user)', - # :ok, - # {expected_json_path: 'data/name'} - # ) - # end - # - # get '/studies/:id' do - # id_params - # let(:id) { study.id } - # let(:authentication_token) { invalid_token } - # standard_request_options( - # :get, - # 'SHOW (with invalid token)', - # :unauthorized, - # {expected_json_path: get_json_error_path(:sign_up)} - # ) - # end - # - # get '/studies/:id' do - # id_params - # let(:id) { study.id } - # standard_request_options( - # :get, - # 'SHOW (an anonymous user)', - # :ok, - # {expected_json_path: 'data/name'} - # ) - # end - # + + describe 'show' do + + get '/studies/:id' do + id_params + let(:id) { study.id } + let(:authentication_token) { admin_token } + standard_request_options( + :get, + 'SHOW (as admin)', + :ok, + {expected_json_path: 'data/name'} + ) + end + + get '/studies/:id' do + id_params + let(:id) { study.id } + let(:authentication_token) { no_access_token } + standard_request_options( + :get, + 'SHOW (as no access user)', + :ok, + {expected_json_path: 'data/name'} + ) + end + + + + get '/studies/:id' do + id_params + let(:id) { study.id } + let(:authentication_token) { invalid_token } + standard_request_options( + :get, + 'SHOW (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/studies/:id' do + id_params + let(:id) { study.id } + standard_request_options( + :get, + 'SHOW (an anonymous user)', + :ok, + {expected_json_path: 'data/name'} + ) + end + + end # # ################################ # # UPDATE # ################################ - # - # - # # add tests for creator - # - # put '/studies/:id' do - # body_params - # let(:id) { study.id } - # let(:raw_post) { {study: post_attributes}.to_json } - # let(:authentication_token) { admin_token } - # standard_request_options( - # :put, - # 'UPDATE (as admin)', - # :ok, - # {expected_json_path: 'data/name', response_body_content: 'New Study name'} - # ) - # end - # - # # owner user is the user who is the creator of the study - # # can therefore update the study - # put '/studies/:id' do - # body_params - # let(:id) { study.id } - # let(:raw_post) { {study: post_attributes}.to_json } - # let(:authentication_token) { owner_token } - # standard_request_options( - # :put, - # 'UPDATE (as owner user)', - # :ok, - # {expected_json_path: 'data/name', response_body_content: 'New Study name'} - # ) - # end - # - # - # put '/studies/:id' do - # body_params - # let(:id) { study.id } - # let(:raw_post) { {study: post_attributes}.to_json } - # let(:authentication_token) { reader_token } - # standard_request_options( - # :put, - # 'UPDATE (as reader user)', - # :forbidden, - # {expected_json_path: get_json_error_path(:permissions)} - # ) - # end - # - # put '/studies/:id' do - # body_params - # let(:id) { study.id } - # let(:raw_post) { {study: post_attributes}.to_json } - # let(:authentication_token) { no_access_token } - # standard_request_options( - # :put, - # 'UPDATE (as no access user)', - # :forbidden, - # {expected_json_path: get_json_error_path(:permissions)} - # ) - # end - # - # put '/studies/:id' do - # body_params - # let(:id) { study.id } - # let(:raw_post) { {study: post_attributes}.to_json } - # let(:authentication_token) { invalid_token } - # standard_request_options( - # :put, - # 'UPDATE (with invalid token)', - # :unauthorized, - # {expected_json_path: get_json_error_path(:sign_up)} - # ) - # end - # - # put '/studies/:id' do - # body_params - # let(:id) { study.id } - # let(:raw_post) { {study: post_attributes}.to_json } - # standard_request_options( - # :put, - # 'UPDATE (as anonymous user)', - # :unauthorized, - # {remove_auth: true, expected_json_path: get_json_error_path(:sign_in)} - # ) - # end - # - # put '/studies/:id' do - # body_params - # let(:id) { study.id } - # let(:raw_post) { {study: post_attributes}.to_json } - # let(:authentication_token) { harvester_token } - # standard_request_options( - # :put, - # 'UPDATE (with harvester token)', - # :forbidden, - # {expected_json_path: get_json_error_path(:permissions)} - # ) - # end - # + + describe 'update' do + + put '/studies/:id' do + body_params + let(:id) { study.id } + let(:raw_post) { {study: post_attributes}.to_json } + let(:authentication_token) { admin_token } + standard_request_options( + :put, + 'UPDATE (as admin)', + :ok, + {expected_json_path: 'data/name', response_body_content: 'New Study name'} + ) + end + + put '/studies/:id' do + body_params + let(:id) { study.id } + let(:raw_post) { {study: post_attributes}.to_json } + let(:authentication_token) { no_access_token } + standard_request_options( + :put, + 'UPDATE (as no access user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + put '/studies/:id' do + body_params + let(:id) { study.id } + let(:raw_post) { {study: post_attributes}.to_json } + let(:authentication_token) { invalid_token } + standard_request_options( + :put, + 'UPDATE (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + put '/studies/:id' do + body_params + let(:id) { study.id } + let(:raw_post) { {study: post_attributes}.to_json } + standard_request_options( + :put, + 'UPDATE (as anonymous user)', + :unauthorized, + {remove_auth: true, expected_json_path: get_json_error_path(:sign_in)} + ) + end + + put '/studies/:id' do + body_params + let(:id) { study.id } + let(:raw_post) { {study: post_attributes}.to_json } + let(:authentication_token) { harvester_token } + standard_request_options( + :put, + 'UPDATE (with harvester token)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + # ################################ # # DESTROY # ################################ - # - # # Destroy is not implemented, so just one test for expected 404 - # delete '/studies/:id' do - # body_params - # let(:id) { study.id } - # let(:authentication_token) { admin_token } - # standard_request_options( - # :delete, - # 'DESTROY (as admin)', - # :not_found, - # {expected_json_path: 'meta/error/info/original_route', response_body_content: 'Could not find'} - # ) - # end - # + + describe 'destroy' do + + delete '/studies/:id' do + id_params + let(:id) { study.id } + let(:authentication_token) { admin_token } + standard_request_options( + :delete, + 'DESTROY (as admin)', + :no_content, + {expected_response_has_content: false, expected_response_content_type: nil} + ) + end + + delete '/studies/:id' do + id_params + let(:id) { study.id } + let(:authentication_token) { owner_token } + standard_request_options( + :delete, + 'DESTROY (as owner user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + delete '/studies/:id' do + id_params + let(:id) { study.id } + let(:authentication_token) { reader_token } + standard_request_options( + :delete, + 'DESTROY (as reader user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + delete '/studies/:id' do + id_params + let(:id) { study.id } + let(:authentication_token) { no_access_token } + standard_request_options( + :delete, + 'DESTROY (as no access user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + delete '/studies/:id' do + id_params + let(:id) { study.id } + let(:authentication_token) { invalid_token } + standard_request_options( + :delete, + 'DESTROY (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + delete '/studies/:id' do + id_params + let(:id) { study.id } + let(:raw_post) { {study: post_attributes}.to_json } + standard_request_options( + :delete, + 'DESTROY (as anonymous user)', + :unauthorized, + {remove_auth: true, expected_json_path: get_json_error_path(:sign_in)} + ) + end + + delete '/studies/:id' do + id_params + let(:id) { study.id } + let(:authentication_token) { harvester_token } + standard_request_options( + :delete, + 'DESTROY (with harvester token)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + # ################################ # # FILTER # ################################ - # - # - # # Basic filter with no conditions is expected count is 2 because of the 'default study'. - # # The 'default study' exists as seed data in a clean install - # # for no filter constraints, use the following opts - # basic_filter_opts = { - # response_body_content: ['The default study', 'gen_study'], - # expected_json_path: 'data/0/name', - # data_item_count: 2 - # } - # - # post '/studies/filter' do - # let(:authentication_token) { admin_token } - # standard_request_options(:post,'FILTER (as admin)',:ok, basic_filter_opts) - # end - # - # post '/studies/filter' do - # let(:authentication_token) { owner_token } - # standard_request_options(:post,'FILTER (as owner user)',:ok, basic_filter_opts) - # end - # - # post '/studies/filter' do - # let(:authentication_token) { writer_token } - # standard_request_options(:post,'FILTER (as writer user)',:ok, basic_filter_opts) - # end - # - # post '/studies/filter' do - # let(:authentication_token) { reader_token } - # standard_request_options(:post,'FILTER (as reader user)',:ok, basic_filter_opts) - # end - # - # post '/studies/filter' do - # let(:authentication_token) { no_access_token } - # standard_request_options(:post,'FILTER (as no access user)',:ok, basic_filter_opts) - # end - # - # post '/studies/filter' do - # let(:authentication_token) { invalid_token } - # standard_request_options( - # :post, - # 'FILTER (with invalid token)', - # :unauthorized, - # {expected_json_path: get_json_error_path(:sign_up)} - # ) - # end - # - # post '/studies/filter' do - # standard_request_options(:post,'FILTER (as anonymous user)',:ok, basic_filter_opts) - # end - # - # post '/studies/filter' do - # let(:authentication_token) { harvester_token } - # standard_request_options( - # :post, - # 'FILTER (with harvester token)', - # :forbidden, - # {response_body_content: ['"data":null'], expected_json_path: get_json_error_path(:permissions)} - # ) - # end - # - # post '/studies/filter' do - # let(:raw_post) { - # { - # filter: { - # name: { - # eq: 'default' - # } - # } - # # projection: { - # # include: [:name, :description] - # # } - # }.to_json - # } - # let(:authentication_token) { reader_token } - # standard_request_options( - # :post, - # 'FILTER (with admin token: filter by name with projection)', - # :ok, - # { - # response_body_content: ['The default study'], - # expected_json_path: 'data/0/name', - # data_item_count: 1 - # } - # ) - # end + + + + + describe 'filter' do + + post '/studies/filter' do + let(:authentication_token) { admin_token } + standard_request_options(:post,'FILTER (as admin)',:ok, basic_filter_opts) + end + + post '/studies/filter' do + let(:authentication_token) { no_access_token } + standard_request_options(:post,'FILTER (as no access user)',:ok, basic_filter_opts) + end + + post '/studies/filter' do + let(:authentication_token) { invalid_token } + standard_request_options( + :post, + 'FILTER (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/studies/filter' do + standard_request_options(:post,'FILTER (as anonymous user)',:ok, basic_filter_opts) + end + + post '/studies/filter' do + let(:authentication_token) { harvester_token } + standard_request_options( + :post, + 'FILTER (with harvester token)', + :forbidden, + {response_body_content: ['"data":null'], expected_json_path: get_json_error_path(:permissions)} + ) + end + + post '/studies/filter' do + let(:raw_post) { + { + filter: { + name: { + starts_with: 'test study' + } + }, + projection: { + include: [:name] + } + }.to_json + } + let(:authentication_token) { reader_token } + standard_request_options( + :post, + 'FILTER (with admin token: filter by name with projection)', + :ok, + { + #response_body_content: ['Test study'], + expected_json_path: 'data/0/name', + data_item_count: 1 + } + ) + end + + + + + end + + + describe 'Owner user' do + it_should_behave_like 'Studies results', :owner + end + + describe 'Writer user' do + it_should_behave_like 'Studies results', :writer + end + + describe 'Reader user' do + it_should_behave_like 'Studies results', :reader + end + end \ No newline at end of file diff --git a/spec/lib/creation.rb b/spec/lib/creation.rb index 6f249987..d422d146 100644 --- a/spec/lib/creation.rb +++ b/spec/lib/creation.rb @@ -33,6 +33,8 @@ def create_entire_hierarchy prepare_progress_event + create_study_hierarchy + end # similar to create entire hierarchy diff --git a/spec/requests/datasets_spec.rb b/spec/requests/datasets_spec.rb index 06895607..7ceff17b 100644 --- a/spec/requests/datasets_spec.rb +++ b/spec/requests/datasets_spec.rb @@ -37,6 +37,31 @@ end + describe 'filter' do + + it 'can do a projection' do + + post_body = { + filter: { + name: { + eq: 'default' + } + }, + projection: { + include: [:name] + } + }.to_json + + post "/datasets/filter", post_body, @env + expect(response).to have_http_status(200) + #parsed_response = JSON.parse(response.body) + #expect(parsed_response).to include('The default dataset') + + end + + + end + end diff --git a/spec/requests/studies_spec.rb b/spec/requests/studies_spec.rb index 844fe31f..14bcd9d5 100644 --- a/spec/requests/studies_spec.rb +++ b/spec/requests/studies_spec.rb @@ -46,6 +46,26 @@ expect(parsed_response['data'][0]['name']).to eq(study['name']) end + it 'can do a projection' do + + post_body = { + filter: { + name: { + starts_with: 'test study' + } + } + }.to_json + + post "#{@study_url}/filter", post_body, @env + expect(response).to have_http_status(200) + parsed_response = JSON.parse(response.body) + expect(parsed_response).to include('test study') + + + end + + + end describe 'show' do From ac9cf09ec3ccb0b4eaab4b99c8c05318d8440add Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Fri, 25 Jan 2019 13:02:16 +1000 Subject: [PATCH 09/11] responses acceptance spec --- app/controllers/errors_controller.rb | 14 +- app/models/response.rb | 1 - config/routes.rb | 2 + lib/modules/access/by_permission.rb | 10 +- spec/acceptance/responses_spec.rb | 752 +++++++++++++++++++++++++++ 5 files changed, 776 insertions(+), 3 deletions(-) create mode 100644 spec/acceptance/responses_spec.rb diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb index 3e2ac44f..f57351d0 100644 --- a/app/controllers/errors_controller.rb +++ b/app/controllers/errors_controller.rb @@ -1,6 +1,6 @@ class ErrorsController < ApplicationController - skip_authorization_check only: [:route_error, :uncaught_error, :test_exceptions, :show] + skip_authorization_check only: [:route_error, :uncaught_error, :test_exceptions, :show, :method_not_allowed_error] # see application_controller.rb for error handling for specific exceptions. # see routes.rb for the catch-all route for routing errors. @@ -30,6 +30,9 @@ def show when 404, '404', 'not_found' status_symbol = :not_found detail_message = 'Could not find the requested page.' + when 405, '405', 'method_not_allowed' + status_symbol = :method_not_allowed + detail_message = 'HTTP method not allowed for this resource.' when 406, '406', 'not_acceptable' status_symbol = :not_acceptable detail_message = 'We cold not provide the format you asked for. Perhaps try a different file extension?' @@ -71,6 +74,15 @@ def route_error end + def method_not_allowed_error + + status_symbol = :method_not_allowed + detail_message = 'HTTP method not allowed for this resource.' + opts = {error_info: {original_route: params[:requested_route], original_http_method: request.method}} + render_error(status_symbol, detail_message, nil, nil, opts) + + end + def uncaught_error diff --git a/app/models/response.rb b/app/models/response.rb index ec258a7a..0ba20203 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -30,7 +30,6 @@ def self.filter_settings render_fields: [:id, :data, :created_at, :creator_id, :study_id, :question_id, :dataset_item_id], new_spec_fields: lambda { |user| { - text: nil, data: nil } }, diff --git a/config/routes.rb b/config/routes.rb index e3d000d6..1e0f2d8b 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -281,6 +281,8 @@ # studies, questions, responses + put 'responses/:id', to: 'errors#method_not_allowed_error' + put '/studies/:study_id/responses/:id', to: 'errors#method_not_allowed' match 'studies/filter' => 'studies#filter', via: [:get, :post], defaults: {format: 'json'} match 'questions/filter' => 'questions#filter', via: [:get, :post], defaults: {format: 'json'} match 'responses/filter' => 'responses#filter', via: [:get, :post], defaults: {format: 'json'} diff --git a/lib/modules/access/by_permission.rb b/lib/modules/access/by_permission.rb index 6f15efe1..95e4ecb4 100644 --- a/lib/modules/access/by_permission.rb +++ b/lib/modules/access/by_permission.rb @@ -215,11 +215,19 @@ def responses(user, study_id = nil, levels = Access::Core.levels) query = Response .joins(dataset_item: {audio_recording: :site}) + is_admin, query = permission_admin(user, levels, query) + if study_id query = query.where(study_id: study_id) end - permission_sites(user, levels, query) + if !is_admin + query = query.where(creator_id: user.id) + end + + query = permission_sites(user, levels, query) + + query end diff --git a/spec/acceptance/responses_spec.rb b/spec/acceptance/responses_spec.rb new file mode 100644 index 00000000..39b9cf58 --- /dev/null +++ b/spec/acceptance/responses_spec.rb @@ -0,0 +1,752 @@ +require 'rails_helper' +require 'rspec_api_documentation/dsl' +require 'helpers/acceptance_spec_helper' + +def id_params + parameter :id, 'Response id in request url', required: true +end + +def body_params + parameter :text, 'Name of response', scope: :response, :required => true + parameter :data, 'Description of response', scope: :response + parameter :study_ids, 'IDs of studies', scope: :response, :required => true +end + +def basic_filter_opts + { + #response_body_content: ['test response'], + expected_json_path: ['data/0/data'], + data_item_count: 1 + } +end + +# response body content is compared against unparsed json response, so json values +# in the response data are escaped +response_body_content = '"data":"{\"labels_present\": [1,2]}\n"' +created_response_body_content = "\"data\":\"{\\\"test_name\\\":\\\"test value\\\"}" + +# https://github.com/zipmark/rspec_api_documentation +resource 'Responses' do + + header 'Accept', 'application/json' + header 'Content-Type', 'application/json' + header 'Authorization', :authentication_token + + let(:format) { 'json' } + + create_entire_hierarchy + create_study_hierarchy + + # Create post parameters from factory + let(:post_attributes) { + FactoryGirl.attributes_for(:response, + data: {test_name:"test value"}.to_json, + study_id: study.id, + question_id: question.id, + dataset_item_id: dataset_item.id) + } + + + + + + ################################ + # INDEX + ################################ + + describe 'index' do + + get '/responses' do + let(:authentication_token) { admin_token } + standard_request_options( + :get, + 'INDEX (as admin)', + :ok, + basic_filter_opts + ) + end + + # admin or response creator can read + # reader user was the creator of the test response + + get '/responses' do + let(:authentication_token) { owner_token } + standard_request_options( + :get, + 'INDEX (as non-responder (owner))', + :ok, + {response_body_content: ['"order_by":"created_at","direction":"desc"'], data_item_count: 0} + ) + end + + get '/responses' do + let(:authentication_token) { writer_token } + standard_request_options( + :get, + 'INDEX (as non-responder (writer))', + :ok, + {response_body_content: ['"order_by":"created_at","direction":"desc"'], data_item_count: 0} + ) + end + + get '/responses' do + let(:authentication_token) { reader_token } + standard_request_options( + :get, + 'INDEX (as responder (reader))', + :ok, + basic_filter_opts + ) + end + + get '/responses' do + let(:authentication_token) { invalid_token } + standard_request_options( + :get, + 'INDEX (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/responses' do + standard_request_options( + :get, + 'INDEX (as anonymous user)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + + get '/responses' do + let(:authentication_token) { harvester_token } + standard_request_options( + :get, + 'INDEX (as harvester)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + ################################ + # CREATE + ################################ + + describe 'create responses' do + + + + post '/responses' do + body_params + let(:raw_post) { {'response' => post_attributes}.to_json } + let(:authentication_token) { admin_token } + standard_request_options( + :post, + 'CREATE (as admin)', + :created, + # json data string will be escaped because it gets compared against unparsed response body + {expected_json_path: ['data/data'], response_body_content: created_response_body_content} + ) + end + + # must have read permission on to create response. + + post '/responses' do + body_params + let(:raw_post) { {'response' => post_attributes}.to_json } + let(:authentication_token) { owner_token } + standard_request_options( + :post, + 'CREATE (as owner of project via dataset item)', + :created, + {expected_json_path: ['data/data'], response_body_content: created_response_body_content} + ) + end + + post '/responses' do + body_params + let(:raw_post) { {'response' => post_attributes}.to_json } + let(:authentication_token) { writer_token } + standard_request_options( + :post, + 'CREATE (as writer of project via dataset item)', + :created, + {expected_json_path: ['data/data'], response_body_content: created_response_body_content} + ) + end + + post '/responses' do + body_params + let(:raw_post) { {'response' => post_attributes}.to_json } + let(:authentication_token) { reader_token } + standard_request_options( + :post, + 'CREATE (as reader of project via dataset item)', + :created, + {expected_json_path: ['data/data'], response_body_content: created_response_body_content} + ) + end + + post '/responses' do + body_params + let(:raw_post) { {'response' => post_attributes}.to_json } + let(:dataset_id) { dataset.id} + let(:authentication_token) { no_access_token } + standard_request_options( + :post, + 'CREATE (as no access user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + post '/responses' do + body_params + let(:raw_post) { {'response' => post_attributes}.to_json } + let(:authentication_token) { invalid_token } + standard_request_options( + :post, + 'CREATE (invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/responses' do + body_params + let(:raw_post) { {'response' => post_attributes}.to_json } + standard_request_options( + :post, + 'CREATE (as anonymous user)', + :unauthorized, + {remove_auth: true, expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/responses' do + body_params + let(:raw_post) { {'response' => post_attributes}.to_json } + let(:authentication_token) { harvester_token } + standard_request_options( + :post, + 'CREATE (as harvester user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + # ################################ + # # NEW + # ################################ + + describe 'new' do + + get '/responses/new' do + let(:authentication_token) { admin_token } + standard_request_options( + :get, + 'NEW (as admin)', + :ok, + {expected_json_path: 'data/data'} + ) + end + + + get '/responses/new' do + let(:authentication_token) { owner_token } + standard_request_options( + :get, + 'NEW (as owner)', + :ok, + {expected_json_path: 'data/data'} + ) + end + + + get '/responses/new' do + let(:authentication_token) { writer_token } + standard_request_options( + :get, + 'NEW (as writer)', + :ok, + {expected_json_path: 'data/data'} + ) + end + + + get '/responses/new' do + let(:authentication_token) { reader_token } + standard_request_options( + :get, + 'NEW (as reader)', + :ok, + {expected_json_path: 'data/data'} + ) + end + + + get '/responses/new' do + let(:authentication_token) { no_access_token } + standard_request_options( + :get, + 'NEW (as non admin user)', + :ok, + {expected_json_path: 'data/data'} + ) + end + + get '/responses/new' do + let(:authentication_token) { invalid_token } + standard_request_options( + :get, + 'NEW (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/responses/new' do + standard_request_options( + :get, + 'NEW (as anonymous user)', + :ok, + {expected_json_path: 'data/data'} + ) + end + + get '/responses/new' do + let(:authentication_token) { harvester_token } + standard_request_options( + :get, + 'NEW (as harvester user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + # ################################ + # # SHOW + # ################################ + + describe 'show' do + + get '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { admin_token } + standard_request_options( + :get, + 'SHOW (as admin)', + :ok, + {expected_json_path: 'data/data', response_body_content: response_body_content} + ) + end + + # admin or response creator can read + # reader user was the creator of the test response + + get '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { owner_token } + standard_request_options( + :get, + 'INDEX (as non-responder (owner))', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + get '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { writer_token } + standard_request_options( + :get, + 'INDEX (as non-responder (writer))', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + get '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { reader_token } + standard_request_options( + :get, + 'INDEX (as responder (reader))', + :ok, + {expected_json_path: 'data/data', response_body_content: response_body_content} + ) + end + + get '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { no_access_token } + standard_request_options( + :get, + 'SHOW (as no access user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + get '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { invalid_token } + standard_request_options( + :get, + 'SHOW (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/responses/:id' do + id_params + let(:id) { user_response.id } + standard_request_options( + :get, + 'SHOW (as anonymous user)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + get '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { harvester_token } + standard_request_options( + :get, + 'SHOW (as harvester)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + # + # ################################ + # # UPDATE + # ################################ + + describe 'update' do + + method_not_allowed_content = 'HTTP method not allowed for this resource.' + + put '/responses/:id' do + body_params + let(:id) { user_response.id } + let(:raw_post) { {response: post_attributes}.to_json } + let(:authentication_token) { admin_token } + standard_request_options( + :put, + 'UPDATE (as admin)', + :method_not_allowed, + {response_body_content: method_not_allowed_content} + ) + end + + put '/responses/:id' do + body_params + let(:id) { user_response.id } + let(:raw_post) { {response: post_attributes}.to_json } + let(:authentication_token) { owner_token } + standard_request_options( + :put, + 'UPDATE (as owner user)', + :method_not_allowed, + {response_body_content: method_not_allowed_content} + ) + end + + put '/responses/:id' do + body_params + let(:id) { user_response.id } + let(:raw_post) { {response: post_attributes}.to_json } + let(:authentication_token) { writer_token } + standard_request_options( + :put, + 'UPDATE (as writer user)', + :method_not_allowed, + {response_body_content: method_not_allowed_content} + ) + end + + put '/responses/:id' do + body_params + let(:id) { user_response.id } + let(:raw_post) { {response: post_attributes}.to_json } + let(:authentication_token) { reader_token } + standard_request_options( + :put, + 'UPDATE (as reader user)', + :method_not_allowed, + {response_body_content: method_not_allowed_content} + ) + end + + put '/responses/:id' do + body_params + let(:id) { user_response.id } + let(:raw_post) { {response: post_attributes}.to_json } + let(:authentication_token) { no_access_token } + standard_request_options( + :put, + 'UPDATE (as no access user)', + :method_not_allowed, + {response_body_content: method_not_allowed_content} + ) + end + + put '/responses/:id' do + body_params + let(:id) { user_response.id } + let(:raw_post) { {response: post_attributes}.to_json } + let(:authentication_token) { invalid_token } + standard_request_options( + :put, + 'UPDATE (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + put '/responses/:id' do + body_params + let(:id) { user_response.id } + let(:raw_post) { {response: post_attributes}.to_json } + standard_request_options( + :put, + 'UPDATE (as anonymous user)', + :method_not_allowed, + {response_body_content: method_not_allowed_content} + ) + end + + put '/responses/:id' do + body_params + let(:id) { user_response.id } + let(:raw_post) { {response: post_attributes}.to_json } + let(:authentication_token) { harvester_token } + standard_request_options( + :put, + 'UPDATE (with harvester token)', + :method_not_allowed, + {response_body_content: method_not_allowed_content} + ) + end + + end + + # ################################ + # # DESTROY + # ################################ + + describe 'destroy' do + + delete '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { admin_token } + standard_request_options( + :delete, + 'DESTROY (as admin)', + :no_content, + {expected_response_has_content: false, expected_response_content_type: nil} + ) + end + + delete '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { owner_token } + standard_request_options( + :delete, + 'DESTROY (as owner user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + delete '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { reader_token } + standard_request_options( + :delete, + 'DESTROY (as reader user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + delete '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { no_access_token } + standard_request_options( + :delete, + 'DESTROY (as no access user)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + delete '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { invalid_token } + standard_request_options( + :delete, + 'DESTROY (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + delete '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:raw_post) { {response: post_attributes}.to_json } + standard_request_options( + :delete, + 'DESTROY (as anonymous user)', + :unauthorized, + {remove_auth: true, expected_json_path: get_json_error_path(:sign_in)} + ) + end + + delete '/responses/:id' do + id_params + let(:id) { user_response.id } + let(:authentication_token) { harvester_token } + standard_request_options( + :delete, + 'DESTROY (with harvester token)', + :forbidden, + {expected_json_path: get_json_error_path(:permissions)} + ) + end + + end + + # ################################ + # # FILTER + # ################################ + + + describe 'filter' do + + post '/responses/filter' do + let(:authentication_token) { admin_token } + standard_request_options(:post,'FILTER (as admin)',:ok, basic_filter_opts) + end + + post '/responses/filter' do + let(:authentication_token) { owner_token } + standard_request_options( + :post, + 'FILTER (as no ow user)', + :ok, + {response_body_content: ['"order_by":"created_at","direction":"desc"'], data_item_count: 0}) + end + + + post '/responses/filter' do + let(:authentication_token) { writer_token } + standard_request_options( + :post, + 'FILTER (as no owner user)', + :ok, + {response_body_content: ['"order_by":"created_at","direction":"desc"'], data_item_count: 0}) + end + + post '/responses/filter' do + let(:authentication_token) { reader_token } + standard_request_options(:post,'FILTER (as no access user)',:ok, basic_filter_opts) + end + + post '/responses/filter' do + let(:authentication_token) { no_access_token } + standard_request_options( + :post, + 'FILTER (as no access user)', + :ok, + {response_body_content: ['"order_by":"created_at","direction":"desc"'], data_item_count: 0}) + end + + + post '/responses/filter' do + let(:authentication_token) { invalid_token } + standard_request_options( + :post, + 'FILTER (with invalid token)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)} + ) + end + + post '/responses/filter' do + standard_request_options( + :post, + 'FILTER (as anonymous user)', + :unauthorized, + {expected_json_path: get_json_error_path(:sign_up)}) + end + + post '/responses/filter' do + let(:authentication_token) { harvester_token } + standard_request_options( + :post, + 'FILTER (with harvester token)', + :forbidden, + {response_body_content: ['"data":null'], expected_json_path: get_json_error_path(:permissions)} + ) + end + + + describe 'temp' do + + post '/responses/filter' do + let(:raw_post) { + { + filter: { + data: { + starts_with: '{' + } + }, + projection: { + include: [:data] + } + }.to_json + } + let(:authentication_token) { reader_token } + standard_request_options( + :post, + 'FILTER (with admin token: filter by name with projection)', + :ok, + { + #response_body_content: ['Test response'], + expected_json_path: 'data/0/data', + data_item_count: 1 + } + ) + end + + end + + end + +end \ No newline at end of file From e6ae20da4217ad09dcf88f420e7bbdb16f4d9175 Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Tue, 29 Jan 2019 16:13:20 +1000 Subject: [PATCH 10/11] fixes to specs for questions, studies and responses --- app/controllers/errors_controller.rb | 11 ++-- app/models/dataset_item.rb | 5 +- app/models/response.rb | 12 +++-- spec/acceptance/dataset_items_spec.rb | 1 + spec/controllers/questions_controller_spec.rb | 16 +++--- spec/controllers/responses_controller_spec.rb | 17 +++--- spec/controllers/studies_controller_spec.rb | 12 +++-- spec/models/response_spec.rb | 6 ++- spec/requests/dataset_items_spec.rb | 54 ++++++++++++++----- spec/requests/responses_spec.rb | 6 +-- spec/requests/studies_spec.rb | 22 +------- spec/routing/responses_routing_spec.rb | 2 +- 12 files changed, 92 insertions(+), 72 deletions(-) diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb index f57351d0..0960e956 100644 --- a/app/controllers/errors_controller.rb +++ b/app/controllers/errors_controller.rb @@ -76,10 +76,13 @@ def route_error def method_not_allowed_error - status_symbol = :method_not_allowed - detail_message = 'HTTP method not allowed for this resource.' - opts = {error_info: {original_route: params[:requested_route], original_http_method: request.method}} - render_error(status_symbol, detail_message, nil, nil, opts) + render_error( + :method_not_allowed, + 'HTTP method not allowed for this resource.2', + nil, + 'method_not_allowed_error_1', + {error_info: {original_route: params[:requested_route], original_http_method: request.method}} + ) end diff --git a/app/models/dataset_item.rb b/app/models/dataset_item.rb index 1ff12762..24664c1a 100644 --- a/app/models/dataset_item.rb +++ b/app/models/dataset_item.rb @@ -8,10 +8,7 @@ class DatasetItem < ActiveRecord::Base belongs_to :audio_recording, inverse_of: :dataset_items belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_dataset_items has_many :progress_events, inverse_of: :dataset_item, dependent: :destroy - has_many :responses - - # We have not enabled soft deletes yet since we do not support deleting dataset items - # This may change in the future + has_many :responses, dependent: :destroy # association validations validates :dataset, existence: true diff --git a/app/models/response.rb b/app/models/response.rb index 0ba20203..2a286555 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -62,11 +62,15 @@ def self.filter_settings private def consistent_associations - if !study.question_ids.include?(question.id) - errors.add(:question_id, "parent question is not associated with parent study") + if !study.nil? && !question.nil? + if !study.question_ids.include?(question.id) + errors.add(:question_id, "parent question is not associated with parent study") + end end - if study.dataset_id != dataset_item.dataset_id - errors.add(:dataset_item_id, "dataset item and study must belong to the same dataset") + if !study.nil? && !dataset_item.nil? + if study.dataset_id != dataset_item.dataset_id + errors.add(:dataset_item_id, "dataset item and study must belong to the same dataset") + end end end diff --git a/spec/acceptance/dataset_items_spec.rb b/spec/acceptance/dataset_items_spec.rb index 4f0a77e3..61bf2a86 100644 --- a/spec/acceptance/dataset_items_spec.rb +++ b/spec/acceptance/dataset_items_spec.rb @@ -584,6 +584,7 @@ def body_params # DESTROY ################################ + delete '/datasets/:dataset_id/items/:id' do dataset_id_param dataset_item_id_param diff --git a/spec/controllers/questions_controller_spec.rb b/spec/controllers/questions_controller_spec.rb index e274024a..4f4cf863 100644 --- a/spec/controllers/questions_controller_spec.rb +++ b/spec/controllers/questions_controller_spec.rb @@ -6,6 +6,8 @@ # allow(CanCan::ControllerResource).to receive(:load_and_authorize_resource){ nil } # } + create_entire_hierarchy + describe "GET #index" do it "returns http success" do get :index @@ -15,8 +17,8 @@ describe "GET #show" do it "returns http success" do - get :show, { :id => 1} - expect(response).to have_http_status(404) + get :show, { :id => question.id} + expect(response).to have_http_status(401) end end @@ -30,22 +32,22 @@ describe "GET #new" do it "returns http success" do get :new + # no permissions needed for new expect(response).to have_http_status(:success) end end describe "POST #create" do it "returns http success" do - # is this how it is done for HABTM associations? - post :create, { :study_ids => [1] } - expect(response).to have_http_status(404) + post :create, { :question => { :study_ids => [study.id] }, :format => :json} + expect(response).to have_http_status(401) end end describe "PUT #update" do it "returns http success" do - put :update, { :id => 1, :data => "something" } - expect(response).to have_http_status(404) + put :update, { :id => question.id, :data => "something" } + expect(response).to have_http_status(401) end end diff --git a/spec/controllers/responses_controller_spec.rb b/spec/controllers/responses_controller_spec.rb index 71eceb34..92f0a671 100644 --- a/spec/controllers/responses_controller_spec.rb +++ b/spec/controllers/responses_controller_spec.rb @@ -2,37 +2,40 @@ describe ResponsesController, type: :controller do + create_entire_hierarchy + describe "GET #index" do - it "returns http success" do + it "returns http success for index" do get :index - expect(response).to have_http_status(200) + expect(response).to have_http_status(401) end end describe "GET #show" do it "returns http success" do - get :show, {:id => 1} - expect(response).to have_http_status(404) + get :show, { id: user_response.id} + expect(response).to have_http_status(401) end end describe "GET #filter" do it "returns http success" do get :filter - expect(response).to have_http_status(200) + expect(response).to have_http_status(401) end end describe "GET #new" do it "returns http success" do get :new - expect(response).to have_http_status(:success) + expect(response).to have_http_status(200) end end describe "POST #create" do it "returns http success" do - post :create, { :study_id => 1 } + post :create, { response: { study_id: study.id, question_id: question.id, dataset_item_id: dataset_item.id }} + # post :create expect(response).to have_http_status(401) end end diff --git a/spec/controllers/studies_controller_spec.rb b/spec/controllers/studies_controller_spec.rb index ec637567..473cedc0 100644 --- a/spec/controllers/studies_controller_spec.rb +++ b/spec/controllers/studies_controller_spec.rb @@ -2,6 +2,8 @@ describe StudiesController, type: :controller do + create_entire_hierarchy + describe "GET #index" do it "returns http success" do get :index @@ -11,8 +13,8 @@ describe "GET #show" do it "returns http success" do - get :show, { :id => 1} - expect(response).to have_http_status(404) + get :show, { :id => study.id} + expect(response).to have_http_status(:success) end end @@ -32,15 +34,15 @@ describe "POST #create" do it "returns http success" do - post :create, { :dataset_id => 1 } + post :create, { study: { :dataset_id => dataset.id, :name => "something" }} expect(response).to have_http_status(401) end end describe "PUT #update" do it "returns http success" do - put :update, { :id => 1, :dataset_id => 1, :name => "something" } - expect(response).to have_http_status(404) + put :update, { :id => study.id, :name => "something" } + expect(response).to have_http_status(401) end end diff --git a/spec/models/response_spec.rb b/spec/models/response_spec.rb index d067ebbe..d247b108 100644 --- a/spec/models/response_spec.rb +++ b/spec/models/response_spec.rb @@ -6,7 +6,7 @@ FactoryGirl.create(:dataset_item) } let(:study) { - FactoryGirl.create(:study) + FactoryGirl.create(:study, dataset_id: dataset_item.dataset_id) } let(:question) { FactoryGirl.create(:question, studies: [study]) @@ -35,7 +35,9 @@ describe 'validations' do - it { is_expected.to validate_presence_of(:dataset_item) } + it { + is_expected.to validate_presence_of(:dataset_item) + } it 'cannot be created without a dataset_item' do expect { create(:response, question: question, study: study, dataset_item: nil) diff --git a/spec/requests/dataset_items_spec.rb b/spec/requests/dataset_items_spec.rb index 0ca35f48..64e1c6e0 100644 --- a/spec/requests/dataset_items_spec.rb +++ b/spec/requests/dataset_items_spec.rb @@ -17,8 +17,8 @@ @env ||= {} @env['HTTP_AUTHORIZATION'] = admin_token - @create_dataset_item_url = "/datasets/#{dataset.id}/items" - @update_dataset_item_url = "/datasets/#{dataset_item.dataset_id}/items/#{dataset_item.id}" + @direct_dataset_item_url = "/datasets/#{dataset.id}/items" #@direct_dataset_item_url + @nested_dataset_item_url = "/datasets/#{dataset_item.dataset_id}/items/#{dataset_item.id}" #@nested_dataset_item_url end describe 'Creating a dataset item' do @@ -26,28 +26,28 @@ it 'does not allow text/plain content-type' do @env['CONTENT_TYPE'] = "text/plain" params = {dataset_item: dataset_item_attributes}.to_json - post @create_dataset_item_url, params, @env + post @direct_dataset_item_url, params, @env expect(response).to have_http_status(415) end it 'does not allow application/x-www-form-urlencoded content-type with json data' do # use default form content type params = {dataset_item: dataset_item_attributes}.to_json - post @create_dataset_item_url, params, @env + post @direct_dataset_item_url, params, @env expect(response).to have_http_status(415) end it 'allows application/json content-type with json data' do @env['CONTENT_TYPE'] = "application/json" params = {dataset_item: dataset_item_attributes}.to_json - post @create_dataset_item_url, params, @env + post @direct_dataset_item_url, params, @env expect(response).to have_http_status(201) end it 'allows application/json content-type with unnested json data' do @env['CONTENT_TYPE'] = "application/json" params = dataset_item_attributes.to_json - post @create_dataset_item_url, params, @env + post @direct_dataset_item_url, params, @env expect(response).to have_http_status(201) end @@ -57,7 +57,7 @@ it 'does not allow empty body (nil, json)' do @env['CONTENT_TYPE'] = "application/json" params = nil - post @create_dataset_item_url, params, @env + post @direct_dataset_item_url, params, @env expect(response).to have_http_status(400) parsed_response = JSON.parse(response.body) expect(parsed_response['meta']['error']['links']).to eq({"New Resource"=>"/datasets/2/items/new"}) @@ -66,7 +66,7 @@ it 'does not allow empty body (empty string, json)' do @env['CONTENT_TYPE'] = "application/json" params = "" - post @create_dataset_item_url, params, @env + post @direct_dataset_item_url, params, @env expect(response).to have_http_status(400) expect(response.content_type).to eq "application/json" parsed_response = JSON.parse(response.body) @@ -81,7 +81,7 @@ @env['CONTENT_TYPE'] = "text/plain" params = {dataset_item: update_dataset_item_attributes}.to_json - put @update_dataset_item_url, params, @env + put @nested_dataset_item_url, params, @env expect(response).to have_http_status(415) end @@ -89,7 +89,7 @@ # use default form content type params = {dataset_item: update_dataset_item_attributes}.to_json - put @update_dataset_item_url, params, @env + put @nested_dataset_item_url, params, @env expect(response).to have_http_status(415) end @@ -97,14 +97,14 @@ @env['CONTENT_TYPE'] = "application/json" params = {dataset_item: update_dataset_item_attributes}.to_json - put @update_dataset_item_url, params, @env + put @nested_dataset_item_url, params, @env expect(response).to have_http_status(200) end it 'does not allow empty body (nil, json)' do @env['CONTENT_TYPE'] = "application/json" params = nil - put @update_dataset_item_url, params, @env + put @nested_dataset_item_url, params, @env expect(response).to have_http_status(400) parsed_response = JSON.parse(response.body) expect(parsed_response['meta']['error']['links']).to eq({"New Resource"=>"/datasets/2/items/new"}) @@ -113,7 +113,7 @@ it 'does not allow empty body (empty string, json)' do @env['CONTENT_TYPE'] = "application/json" params = "" - put @update_dataset_item_url, params, @env + put @nested_dataset_item_url, params, @env expect(response).to have_http_status(400) expect(response.content_type).to eq "application/json" parsed_response = JSON.parse(response.body) @@ -122,7 +122,33 @@ end - describe "filter" do + describe 'delete' do + + it 'deletes dataset item and any children' do + + dataset_item_count = DatasetItem.all.count + progress_event_count = ProgressEvent.all.count + response_count = Response.all.count + expected_progress_event_count = progress_event_count - ProgressEvent.where(dataset_item_id: dataset_item.id).count + expected_response_count = response_count - Response.where(dataset_item_id: dataset_item.id).count + + @env['CONTENT_TYPE'] = "application/json" + params = "" + delete @nested_dataset_item_url, params, @env + expect(response).to have_http_status(204) + #parsed_response = JSON.parse(response.body) + + # ensure dataset has been deleted + expect(DatasetItem.all.count).to eq(dataset_item_count - 1) + # ensure children are deleted + expect(ProgressEvent.all.count).to eq(expected_progress_event_count) + expect(Response.all.count).to eq(expected_response_count) + + end + + end + + describe 'filter' do it 'does not allow arbitrary string in sort column' do diff --git a/spec/requests/responses_spec.rb b/spec/requests/responses_spec.rb index 3e43ee3e..4fa71ff6 100644 --- a/spec/requests/responses_spec.rb +++ b/spec/requests/responses_spec.rb @@ -83,7 +83,7 @@ def response_url(response_id = nil, study_id = nil) expect(response).to have_http_status(200) parsed_response = JSON.parse(response.body) expect(parsed_response['data'].count).to eq(1) - expect(parsed_response['data'][0]['data']).to eq(response['data']) + expect(parsed_response['data'][0]['data']).to eq(user_response['data']) end it 'finds responses to the study0 and study1 using studies.id' do @@ -253,9 +253,9 @@ def response_url(response_id = nil, study_id = nil) data = {some_answer:'modified response text'}.to_json params = {response: {data: data}}.to_json put response_url(user_response.id), params, @env - expect(response).to have_http_status(404) + expect(response).to have_http_status(405) parsed_response = JSON.parse(response.body) - expect(parsed_response['meta']['message']).to eq('Not Found') + expect(parsed_response['meta']['message']).to eq('Method Not Allowed') end end diff --git a/spec/requests/studies_spec.rb b/spec/requests/studies_spec.rb index 14bcd9d5..d0585fa3 100644 --- a/spec/requests/studies_spec.rb +++ b/spec/requests/studies_spec.rb @@ -45,27 +45,7 @@ expect(parsed_response['data'].count).to eq(1) expect(parsed_response['data'][0]['name']).to eq(study['name']) end - - it 'can do a projection' do - - post_body = { - filter: { - name: { - starts_with: 'test study' - } - } - }.to_json - - post "#{@study_url}/filter", post_body, @env - expect(response).to have_http_status(200) - parsed_response = JSON.parse(response.body) - expect(parsed_response).to include('test study') - - - end - - - + end describe 'show' do diff --git a/spec/routing/responses_routing_spec.rb b/spec/routing/responses_routing_spec.rb index 5c16f393..f5f23534 100644 --- a/spec/routing/responses_routing_spec.rb +++ b/spec/routing/responses_routing_spec.rb @@ -10,7 +10,7 @@ it { expect(get('/responses/filter')).to route_to('responses#filter', format: 'json') } it { expect(post('/responses/filter')).to route_to('responses#filter', format: 'json') } it { expect(post('/responses')).to route_to('responses#create', format: 'json') } - it { expect(put('/responses/1')).to route_to('errors#route_error', requested_route: 'responses/1') } + it { expect(put('/responses/1')).to route_to('errors#method_not_allowed_error', id: '1') } it { expect(delete('/responses/1')).to route_to('responses#destroy', id: '1', format: 'json') } end From b0371158132cf172eb4f6afe2ebd501d45ff318d Mon Sep 17 00:00:00 2001 From: Philip Eichinski Date: Mon, 25 Feb 2019 11:13:22 +1000 Subject: [PATCH 11/11] addresses PR change requests --- app/controllers/errors_controller.rb | 4 +- app/controllers/questions_controller.rb | 1 + app/controllers/responses_controller.rb | 2 - app/models/response.rb | 2 +- spec/acceptance/responses_spec.rb | 4 +- spec/controllers/questions_controller_spec.rb | 54 ------------------- spec/controllers/responses_controller_spec.rb | 43 --------------- spec/controllers/studies_controller_spec.rb | 49 ----------------- spec/factories/dataset_item_factory.rb | 5 +- spec/requests/studies_spec.rb | 2 +- 10 files changed, 11 insertions(+), 155 deletions(-) delete mode 100644 spec/controllers/questions_controller_spec.rb delete mode 100644 spec/controllers/responses_controller_spec.rb delete mode 100644 spec/controllers/studies_controller_spec.rb diff --git a/app/controllers/errors_controller.rb b/app/controllers/errors_controller.rb index 0960e956..99599a30 100644 --- a/app/controllers/errors_controller.rb +++ b/app/controllers/errors_controller.rb @@ -78,9 +78,9 @@ def method_not_allowed_error render_error( :method_not_allowed, - 'HTTP method not allowed for this resource.2', + 'HTTP method not allowed for this resource.', nil, - 'method_not_allowed_error_1', + 'method_not_allowed_error', {error_info: {original_route: params[:requested_route], original_http_method: request.method}} ) diff --git a/app/controllers/questions_controller.rb b/app/controllers/questions_controller.rb index 96648c68..98b8bf1e 100644 --- a/app/controllers/questions_controller.rb +++ b/app/controllers/questions_controller.rb @@ -93,6 +93,7 @@ def destroy def question_params # empty array is replaced with nil by rails. Revert to empty array # to avoid errors with strong parameters + # https://github.com/rails/rails/issues/13766 if params.has_key?(:question) and params[:question].has_key?(:study_ids) and params[:question][:study_ids].nil? params[:question][:study_ids] = [] end diff --git a/app/controllers/responses_controller.rb b/app/controllers/responses_controller.rb index 33b61561..4a42f031 100644 --- a/app/controllers/responses_controller.rb +++ b/app/controllers/responses_controller.rb @@ -70,8 +70,6 @@ def destroy private def response_params - # params[:response] = params[:response] || {} - # params[:response][:study_id] = params[:study_id] params.require(:response).permit(:study_id, :question_id, :dataset_item_id, :text, :data) end diff --git a/app/models/response.rb b/app/models/response.rb index 2a286555..2cc43242 100644 --- a/app/models/response.rb +++ b/app/models/response.rb @@ -3,7 +3,7 @@ class Response < ActiveRecord::Base # ensures that creator_id, updater_id, deleter_id are set include UserChange - #relationships + # relationships belongs_to :creator, class_name: 'User', foreign_key: :creator_id, inverse_of: :created_responses belongs_to :question belongs_to :study diff --git a/spec/acceptance/responses_spec.rb b/spec/acceptance/responses_spec.rb index 39b9cf58..0b7fb068 100644 --- a/spec/acceptance/responses_spec.rb +++ b/spec/acceptance/responses_spec.rb @@ -658,7 +658,7 @@ def basic_filter_opts let(:authentication_token) { owner_token } standard_request_options( :post, - 'FILTER (as no ow user)', + 'FILTER (as owner user)', :ok, {response_body_content: ['"order_by":"created_at","direction":"desc"'], data_item_count: 0}) end @@ -668,7 +668,7 @@ def basic_filter_opts let(:authentication_token) { writer_token } standard_request_options( :post, - 'FILTER (as no owner user)', + 'FILTER (as no writer user)', :ok, {response_body_content: ['"order_by":"created_at","direction":"desc"'], data_item_count: 0}) end diff --git a/spec/controllers/questions_controller_spec.rb b/spec/controllers/questions_controller_spec.rb deleted file mode 100644 index 4f4cf863..00000000 --- a/spec/controllers/questions_controller_spec.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'rails_helper' - -describe QuestionsController, type: :controller do - - # before { - # allow(CanCan::ControllerResource).to receive(:load_and_authorize_resource){ nil } - # } - - create_entire_hierarchy - - describe "GET #index" do - it "returns http success" do - get :index - expect(response).to have_http_status(401) - end - end - - describe "GET #show" do - it "returns http success" do - get :show, { :id => question.id} - expect(response).to have_http_status(401) - end - end - - describe "GET #filter" do - it "returns http success" do - get :filter - expect(response).to have_http_status(401) - end - end - - describe "GET #new" do - it "returns http success" do - get :new - # no permissions needed for new - expect(response).to have_http_status(:success) - end - end - - describe "POST #create" do - it "returns http success" do - post :create, { :question => { :study_ids => [study.id] }, :format => :json} - expect(response).to have_http_status(401) - end - end - - describe "PUT #update" do - it "returns http success" do - put :update, { :id => question.id, :data => "something" } - expect(response).to have_http_status(401) - end - end - -end diff --git a/spec/controllers/responses_controller_spec.rb b/spec/controllers/responses_controller_spec.rb deleted file mode 100644 index 92f0a671..00000000 --- a/spec/controllers/responses_controller_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -require 'rails_helper' - -describe ResponsesController, type: :controller do - - create_entire_hierarchy - - describe "GET #index" do - it "returns http success for index" do - get :index - expect(response).to have_http_status(401) - end - end - - describe "GET #show" do - it "returns http success" do - get :show, { id: user_response.id} - expect(response).to have_http_status(401) - end - end - - describe "GET #filter" do - it "returns http success" do - get :filter - expect(response).to have_http_status(401) - end - end - - describe "GET #new" do - it "returns http success" do - get :new - expect(response).to have_http_status(200) - end - end - - describe "POST #create" do - it "returns http success" do - post :create, { response: { study_id: study.id, question_id: question.id, dataset_item_id: dataset_item.id }} - # post :create - expect(response).to have_http_status(401) - end - end - -end diff --git a/spec/controllers/studies_controller_spec.rb b/spec/controllers/studies_controller_spec.rb deleted file mode 100644 index 473cedc0..00000000 --- a/spec/controllers/studies_controller_spec.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'rails_helper' - -describe StudiesController, type: :controller do - - create_entire_hierarchy - - describe "GET #index" do - it "returns http success" do - get :index - expect(response).to have_http_status(:success) - end - end - - describe "GET #show" do - it "returns http success" do - get :show, { :id => study.id} - expect(response).to have_http_status(:success) - end - end - - describe "GET #filter" do - it "returns http success" do - get :filter - expect(response).to have_http_status(:success) - end - end - - describe "GET #new" do - it "returns http success" do - get :new - expect(response).to have_http_status(:success) - end - end - - describe "POST #create" do - it "returns http success" do - post :create, { study: { :dataset_id => dataset.id, :name => "something" }} - expect(response).to have_http_status(401) - end - end - - describe "PUT #update" do - it "returns http success" do - put :update, { :id => study.id, :name => "something" } - expect(response).to have_http_status(401) - end - end - -end diff --git a/spec/factories/dataset_item_factory.rb b/spec/factories/dataset_item_factory.rb index f16bad1d..51189473 100644 --- a/spec/factories/dataset_item_factory.rb +++ b/spec/factories/dataset_item_factory.rb @@ -20,7 +20,10 @@ end_time_seconds 1442 sequence(:order) { |n| (n+10.0)/2 } - dataset_id Dataset.default_dataset_id + # curly braces around the value to delay execution + # https://stackoverflow.com/questions/12423273/factorygirl-screws-up-rake-dbmigrate-process + dataset_id { Dataset.default_dataset_id } + audio_recording creator diff --git a/spec/requests/studies_spec.rb b/spec/requests/studies_spec.rb index d0585fa3..844fe31f 100644 --- a/spec/requests/studies_spec.rb +++ b/spec/requests/studies_spec.rb @@ -45,7 +45,7 @@ expect(parsed_response['data'].count).to eq(1) expect(parsed_response['data'][0]['name']).to eq(study['name']) end - + end describe 'show' do