diff --git a/app/controllers/researchers_controller.rb b/app/controllers/researchers_controller.rb new file mode 100644 index 000000000..f462b3785 --- /dev/null +++ b/app/controllers/researchers_controller.rb @@ -0,0 +1,136 @@ +class ResearchersController < ApplicationController + include ActionController::MimeResponds + + prepend_before_action :authenticate_user! + before_action :set_researcher, only: [:show, :destroy] + load_and_authorize_resource except: [:index, :update] + + def index + sort = case params[:sort] + when "relevance" then { "_score" => { order: 'desc' }} + when "name" then { "family_name.raw" => { order: 'asc' }} + when "-name" then { "family_name.raw" => { order: 'desc' }} + when "created" then { created_at: { order: 'asc' }} + when "-created" then { created_at: { order: 'desc' }} + else { "family_name.raw" => { order: 'asc' }} + end + + page = page_from_params(params) + + if params[:id].present? + response = Researcher.find_by_id(params[:id]) + elsif params[:ids].present? + response = Researcher.find_by_id(params[:ids], page: page, sort: sort) + else + response = Researcher.query(params[:query], page: page, sort: sort) + end + + begin + total = response.results.total + total_pages = page[:size] > 0 ? (total.to_f / page[:size]).ceil : 0 + options = {} + options[:meta] = { + total: total, + "totalPages" => total_pages, + page: page[:number] + }.compact + + options[:links] = { + self: request.original_url, + next: response.results.blank? ? nil : request.base_url + "/researchers?" + { + query: params[:query], + "page[number]" => page[:number] + 1, + "page[size]" => page[:size], + sort: params[:sort] }.compact.to_query + }.compact + options[:is_collection] = true + + fields = fields_from_params(params) + if fields + render json: ResearcherSerializer.new(response.results, options.merge(fields: fields)).serialized_json, status: :ok + else + render json: ResearcherSerializer.new(response.results, options).serialized_json, status: :ok + end + rescue Elasticsearch::Transport::Transport::Errors::BadRequest => exception + Raven.capture_exception(exception) + + message = JSON.parse(exception.message[6..-1]).to_h.dig("error", "root_cause", 0, "reason") + + render json: { "errors" => { "title" => message }}.to_json, status: :bad_request + end + end + + def show + options = {} + options[:is_collection] = false + render json: ResearcherSerializer.new(@researcher, options).serialized_json, status: :ok + end + + def create + logger = Logger.new(STDOUT) + @researcher = Researcher.new(safe_params) + authorize! :create, @researcher + + if @researcher.save + options = {} + options[:is_collection] = false + render json: ResearcherSerializer.new(@researcher, options).serialized_json, status: :created + else + logger.warn @researcher.errors.inspect + render json: serialize_errors(@researcher.errors), status: :unprocessable_entity + end + end + + def update + logger = Logger.new(STDOUT) + @researcher = Researcher.where(uid: params[:id]).first + exists = @researcher.present? + + # create researcher if it doesn't exist already + @researcher = Researcher.new(safe_params.except(:format).merge(uid: params[:id])) unless @researcher.present? + + logger.info @researcher.inspect + authorize! :update, @researcher + + if @researcher.update_attributes(safe_params) + options = {} + options[:is_collection] = false + render json: ResearcherSerializer.new(@researcher, options).serialized_json, status: exists ? :ok : :created + else + logger.warn @researcher.errors.inspect + render json: serialize_errors(@researcher.errors), status: :unprocessable_entity + end + end + + def destroy + logger = Logger.new(STDOUT) + if @researcher.destroy + head :no_content + else + logger.warn @researcher.errors.inspect + render json: serialize_errors(@researcher.errors), status: :unprocessable_entity + end + end + + protected + + def set_researcher + @researcher = Researcher.where(uid: params[:id]).first + fail ActiveRecord::RecordNotFound unless @researcher.present? + end + + private + + def safe_params + fail JSON::ParserError, "You need to provide a payload following the JSONAPI spec" unless params[:data].present? + ActiveModelSerializers::Deserialization.jsonapi_parse!( + params, + only: [ + :uid, :name, "givenNames", "familyName" + ], + keys: { + id: :uid, "givenNames" => :given_names, "familyName" => :family_name + } + ) + end +end diff --git a/app/jobs/orcid_auto_update_by_id_job.rb b/app/jobs/orcid_auto_update_by_id_job.rb new file mode 100644 index 000000000..a7d9c3502 --- /dev/null +++ b/app/jobs/orcid_auto_update_by_id_job.rb @@ -0,0 +1,73 @@ +class OrcidAutoUpdateByIdJob < ActiveJob::Base + queue_as :lupo_background + + # retry_on ActiveRecord::Deadlocked, wait: 10.seconds, attempts: 3 + # retry_on Faraday::TimeoutError, wait: 10.minutes, attempts: 3 + + # discard_on ActiveJob::DeserializationError + + def perform(id) + logger = Logger.new(STDOUT) + + orcid = orcid_from_url(id) + return {} unless orcid.present? + + # check whether ORCID ID has been registered with DataCite already + result = Researcher.find_by_id(orcid).results.first + return {} unless result.blank? + + # otherwise fetch basic ORCID metadata and store with DataCite + url = "https://pub.orcid.org/v2.1/#{orcid}/person" + # if Rails.env.production? + # url = "https://pub.orcid.org/v2.1/#{orcid}/person" + # else + # url = "https://pub.sandbox.orcid.org/v2.1/#{orcid}/person" + # end + + response = Maremma.get(url, accept: "application/vnd.orcid+json") + return {} if response.status != 200 + + message = response.body.fetch("data", {}) + attributes = parse_message(message: message) + data = { + "data" => { + "type" => "researchers", + "attributes" => attributes + } + } + url = "http://localhost/researchers/#{orcid}" + response = Maremma.put(url, accept: 'application/vnd.api+json', + content_type: 'application/vnd.api+json', + data: data.to_json, + username: ENV["ADMIN_USERNAME"], + password: ENV["ADMIN_PASSWORD"]) + + if [200, 201].include?(response.status) + logger.info "ORCID #{orcid} added." + else + puts response.body["errors"].inspect + logger.warn "[Error for ORCID #{orcid}]: " + response.body["errors"].inspect + end + end + + def parse_message(message: nil) + given_names = message.dig("name", "given-names", "value") + family_name = message.dig("name", "family-name", "value") + if message.dig("name", "credit-name", "value").present? + name = message.dig("name", "credit-name", "value") + elsif given_names.present? || family_name.present? + name = [given_names, family_name].join(" ") + else + name = nil + end + + { + "name" => name, + "givenNames" => given_names, + "familyName" => family_name }.compact + end + + def orcid_from_url(url) + Array(/\A(http|https):\/\/orcid\.org\/(.+)/.match(url)).last + end +end diff --git a/app/jobs/orcid_auto_update_job.rb b/app/jobs/orcid_auto_update_job.rb new file mode 100644 index 000000000..40474ef03 --- /dev/null +++ b/app/jobs/orcid_auto_update_job.rb @@ -0,0 +1,7 @@ +class OrcidAutoUpdateJob < ActiveJob::Base + queue_as :lupo_background + + def perform(ids) + ids.each { |id| OrcidAutoUpdateByIdJob.perform_later(id) } + end +end diff --git a/app/models/concerns/indexable.rb b/app/models/concerns/indexable.rb index 7c8b6ec69..80c7bf3f8 100644 --- a/app/models/concerns/indexable.rb +++ b/app/models/concerns/indexable.rb @@ -114,6 +114,10 @@ def query(query, options={}) from = 0 search_after = [options.dig(:page, :cursor)] sort = [{ created: { order: 'asc' }}] + elsif self.name == "Researcher" && options.dig(:page, :cursor).present? + from = 0 + search_after = [options.dig(:page, :cursor)] + sort = [{ created_at: { order: 'asc' }}] elsif options.dig(:page, :cursor).present? from = 0 search_after = [options.dig(:page, :cursor)] diff --git a/app/models/event.rb b/app/models/event.rb index 7666df154..1f4cd3759 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -331,6 +331,32 @@ def self.update_datacite_crossref(options={}) response.results.total end + def self.update_datacite_orcid_auto_update(options={}) + logger = Logger.new(STDOUT) + + size = (options[:size] || 1000).to_i + cursor = (options[:cursor] || 0).to_i + + response = Event.query(nil, source_id: "datacite-orcid-auto-update", page: { size: 1, cursor: cursor }) + logger.info "[Update] #{response.results.total} events for source datacite-orcid-auto-update." + + # walk through results using cursor + if response.results.total > 0 + while response.results.results.length > 0 do + response = Event.query(nil, source_id: "datacite-orcid-auto-update", page: { size: size, cursor: cursor }) + break unless response.results.results.length > 0 + + logger.info "[Update] Updating #{response.results.results.length} datacite-orcid-auto-update events starting with _id #{cursor + 1}." + cursor = response.results.to_a.last[:sort].first.to_i + + ids = response.results.results.map(&:obj_id).uniq + OrcidAutoUpdateJob.perform_later(ids) + end + end + + response.results.total + end + def to_param # overridden, use uuid instead of id uuid end diff --git a/app/models/researcher.rb b/app/models/researcher.rb index 8f5ab3f0a..aec33bfec 100644 --- a/app/models/researcher.rb +++ b/app/models/researcher.rb @@ -1,30 +1,75 @@ -class Researcher - def self.find_by_id(id) - orcid = orcid_from_url(id) - return {} unless orcid.present? +class Researcher < ActiveRecord::Base + # include helper module for Elasticsearch + include Indexable - url = "https://pub.orcid.org/v2.1/#{orcid}/person" - response = Maremma.get(url, accept: "application/vnd.orcid+json") + include Elasticsearch::Model - return {} if response.status != 200 + validates_presence_of :uid + validates_uniqueness_of :uid + validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\Z/i, if: :email? + + # use different index for testing + index_name Rails.env.test? ? "researchers-test" : "researchers" - message = response.body.fetch("data", {}) - data = [parse_message(id: id, message: message)] + settings index: { + analysis: { + analyzer: { + string_lowercase: { tokenizer: 'keyword', filter: %w(lowercase ascii_folding) } + }, + filter: { ascii_folding: { type: 'asciifolding', preserve_original: true } } + } + } do + mapping dynamic: 'false' do + indexes :id, type: :keyword + indexes :uid, type: :keyword + indexes :name, type: :text, fields: { keyword: { type: "keyword" }, raw: { type: "text", "analyzer": "string_lowercase", "fielddata": true }} + indexes :given_names, type: :text, fields: { keyword: { type: "keyword" }, raw: { type: "text", "analyzer": "string_lowercase", "fielddata": true }} + indexes :family_name, type: :text, fields: { keyword: { type: "keyword" }, raw: { type: "text", "analyzer": "string_lowercase", "fielddata": true }} + indexes :created_at, type: :date + indexes :updated_at, type: :date + end + end - errors = response.body.fetch("errors", nil) + # also index id as workaround for finding the correct key in associations + def as_indexed_json(options={}) + { + "id" => uid, + "uid" => uid, + "name" => name, + "given_names" => given_names, + "family_name" => family_name, + "created_at" => created_at, + "updated_at" => updated_at + } + end - { data: data, errors: errors } + def self.query_fields + ['uid^10', 'name^5', 'given_names^5', 'family_name^5', '_all'] end - def self.parse_message(id: nil, message: nil) - { - id: id, - name: message.dig("name", "credit-name", "value"), - "givenName" => message.dig("name", "given-names", "value"), - "familyName" => message.dig("name", "family-name", "value") }.compact + def self.query_aggregations + {} end - def self.orcid_from_url(url) - Array(/\A(http|https):\/\/orcid\.org\/(.+)/.match(url)).last + # return results for one or more ids + def self.find_by_id(ids, options={}) + ids = ids.split(",") if ids.is_a?(String) + + options[:page] ||= {} + options[:page][:number] ||= 1 + options[:page][:size] ||= 1000 + options[:sort] ||= { created_at: { order: "asc" }} + + __elasticsearch__.search({ + from: (options.dig(:page, :number) - 1) * options.dig(:page, :size), + size: options.dig(:page, :size), + sort: [options[:sort]], + query: { + terms: { + uid: ids + } + }, + aggregations: query_aggregations + }) end end diff --git a/app/serializers/researcher_serializer.rb b/app/serializers/researcher_serializer.rb new file mode 100644 index 000000000..225d6e163 --- /dev/null +++ b/app/serializers/researcher_serializer.rb @@ -0,0 +1,8 @@ +class ResearcherSerializer + include FastJsonapi::ObjectSerializer + set_key_transform :camel_lower + set_type :researchers + set_id :uid + + attributes :name, :given_names, :family_name, :created_at, :updated_at +end diff --git a/config/routes.rb b/config/routes.rb index ccad96033..b80c845d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -85,7 +85,7 @@ resources :prefixes, constraints: { :id => /.+/ } end resources :providers, constraints: { :id => /.+/ } - + resources :researchers resources :resource_types, path: 'resource-types', only: [:show, :index] # custom routes for maintenance tasks diff --git a/db/migrate/20190629072238_add_researchers_table.rb b/db/migrate/20190629072238_add_researchers_table.rb new file mode 100644 index 000000000..ff22d3ecf --- /dev/null +++ b/db/migrate/20190629072238_add_researchers_table.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true +class AddResearchersTable < ActiveRecord::Migration[5.2] + def up + create_table "researchers", force: :cascade do |t| + t.string "name", limit: 191 + t.string "family_name", limit: 191 + t.string "given_names", limit: 191 + t.string "email", limit: 191 + t.string "provider", limit: 255, default: "orcid" + t.string "uid", limit: 191 + t.string "authentication_token", limit: 191 + t.string "role_id", limit: 255, default: "user" + t.boolean "auto_update", default: true + t.datetime "expires_at", default: '1970-01-01 00:00:00', null: false + t.datetime "created_at", precision: 3 + t.datetime "updated_at", precision: 3 + t.text "other_names", limit: 65535 + t.string "confirmation_token", limit: 191 + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email", limit: 255 + t.string "github", limit: 191 + t.string "github_uid", limit: 191 + t.string "github_token", limit: 191 + t.string "google_uid", limit: 191 + t.string "google_token", limit: 191 + t.integer "github_put_code", limit: 4 + t.boolean "is_public", default: true + t.boolean "beta_tester", default: false + end + + add_index "researchers", ["uid"], name: "index_researchers_on_uid", unique: true, using: :btree + end + + def down + drop_table :researchers + end +end diff --git a/db/schema.rb b/db/schema.rb index 25460d010..2e434a8b3 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,9 +10,9 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2019_06_21_202343) do +ActiveRecord::Schema.define(version: 2019_06_29_072238) do - create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin", force: :cascade do |t| + create_table "active_storage_attachments", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC", force: :cascade do |t| t.string "name", limit: 191, null: false t.string "record_type", null: false t.bigint "record_id", null: false @@ -22,7 +22,7 @@ t.index ["record_type", "record_id", "name", "blob_id"], name: "index_active_storage_attachments_uniqueness", unique: true end - create_table "active_storage_blobs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin", force: :cascade do |t| + create_table "active_storage_blobs", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC", force: :cascade do |t| t.string "key", limit: 191, null: false t.string "filename", limit: 191, null: false t.string "content_type", limit: 191 @@ -33,7 +33,7 @@ t.index ["key"], name: "index_active_storage_blobs_on_key", unique: true end - create_table "allocator", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "allocator", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.string "system_email", null: false t.datetime "created" t.integer "doi_quota_allowed", null: false @@ -72,7 +72,7 @@ t.index ["symbol"], name: "symbol", unique: true end - create_table "allocator_prefixes", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "allocator_prefixes", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.bigint "allocator", null: false t.bigint "prefixes", null: false t.datetime "created_at" @@ -82,7 +82,7 @@ t.index ["prefixes"], name: "FKE7FBD674AF86A1C7" end - create_table "audits", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin", force: :cascade do |t| + create_table "audits", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin ROW_FORMAT=DYNAMIC", force: :cascade do |t| t.integer "auditable_id" t.string "auditable_type" t.integer "associated_id" @@ -104,7 +104,7 @@ t.index ["user_id", "user_type"], name: "user_index" end - create_table "datacentre", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "datacentre", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.text "comments", limit: 4294967295 t.string "contact_email", null: false t.string "contact_name", limit: 80, null: false @@ -132,7 +132,7 @@ t.index ["url"], name: "index_datacentre_on_url", length: 100 end - create_table "datacentre_prefixes", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "datacentre_prefixes", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.bigint "datacentre", null: false t.bigint "prefixes", null: false t.datetime "created_at" @@ -144,7 +144,7 @@ t.index ["prefixes"], name: "FK13A1B3BAAF86A1C7" end - create_table "dataset", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "dataset", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.datetime "created" t.string "doi", null: false t.binary "is_active", limit: 1, null: false @@ -199,7 +199,7 @@ t.index ["url"], name: "index_dataset_on_url", length: 100 end - create_table "events", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4", force: :cascade do |t| + create_table "events", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin ROW_FORMAT=DYNAMIC", force: :cascade do |t| t.text "uuid", null: false t.text "subj_id", null: false t.text "obj_id" @@ -226,7 +226,7 @@ t.index ["uuid"], name: "index_events_on_uuid", unique: true, length: 36 end - create_table "media", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "media", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.datetime "created" t.string "media_type", limit: 80 t.datetime "updated" @@ -238,7 +238,7 @@ t.index ["url"], name: "index_media_on_url", length: 100 end - create_table "metadata", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "metadata", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.datetime "created" t.integer "metadata_version" t.integer "version" @@ -250,7 +250,7 @@ t.index ["dataset"], name: "FKE52D7B2F4D3D6B1B" end - create_table "prefix", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8", force: :cascade do |t| + create_table "prefix", options: "ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPACT", force: :cascade do |t| t.datetime "created" t.string "prefix", limit: 80, null: false t.integer "version" @@ -258,6 +258,35 @@ t.index ["prefix"], name: "prefix", unique: true end + create_table "researchers", options: "ENGINE=InnoDB DEFAULT CHARSET=latin1", force: :cascade do |t| + t.string "name", limit: 191 + t.string "family_name", limit: 191 + t.string "given_names", limit: 191 + t.string "email", limit: 191 + t.string "provider", default: "orcid" + t.string "uid", limit: 191 + t.string "authentication_token", limit: 191 + t.string "role_id", default: "user" + t.boolean "auto_update", default: true + t.datetime "expires_at", default: "1970-01-01 00:00:00", null: false + t.datetime "created_at", precision: 3 + t.datetime "updated_at", precision: 3 + t.text "other_names" + t.string "confirmation_token", limit: 191 + t.datetime "confirmed_at" + t.datetime "confirmation_sent_at" + t.string "unconfirmed_email" + t.string "github", limit: 191 + t.string "github_uid", limit: 191 + t.string "github_token", limit: 191 + t.string "google_uid", limit: 191 + t.string "google_token", limit: 191 + t.integer "github_put_code" + t.boolean "is_public", default: true + t.boolean "beta_tester", default: false + t.index ["uid"], name: "index_researchers_on_uid", unique: true + end + add_foreign_key "allocator_prefixes", "allocator", column: "allocator", name: "FKE7FBD67446EBD781" add_foreign_key "allocator_prefixes", "prefix", column: "prefixes", name: "FKE7FBD674AF86A1C7" add_foreign_key "datacentre", "allocator", column: "allocator", name: "FK6695D60546EBD781" diff --git a/lib/tasks/event.rake b/lib/tasks/event.rake index b0ca30fd7..e9e206e05 100644 --- a/lib/tasks/event.rake +++ b/lib/tasks/event.rake @@ -40,3 +40,12 @@ namespace :datacite_crossref do Event.update_datacite_crossref(cursor: cursor) end end + +namespace :datacite_orcid_auto_update do + desc 'Import orcid ids for all events' + task :import_orcid => :environment do + cursor = (ENV['CURSOR'] || Event.minimum(:id)).to_i + + Event.update_datacite_orcid_auto_update(cursor: cursor) + end +end diff --git a/lib/tasks/provider.rake b/lib/tasks/provider.rake index 0d5649688..bc68acc2d 100644 --- a/lib/tasks/provider.rake +++ b/lib/tasks/provider.rake @@ -19,4 +19,4 @@ namespace :provider do task :refresh_index => :environment do Provider.__elasticsearch__.refresh_index! end -end \ No newline at end of file +end diff --git a/lib/tasks/researcher.rake b/lib/tasks/researcher.rake new file mode 100644 index 000000000..ce2ae17eb --- /dev/null +++ b/lib/tasks/researcher.rake @@ -0,0 +1,22 @@ +namespace :researcher do + desc 'Import all researchers' + task :import => :environment do + Researcher.__elasticsearch__.create_index! + Researcher.import + end + + desc "Create index for researchers" + task :create_index => :environment do + Researcher.__elasticsearch__.create_index! + end + + desc "Delete index for researchers" + task :delete_index => :environment do + Researcher.__elasticsearch__.delete_index! + end + + desc "Refresh index for researchers" + task :refresh_index => :environment do + Researcher.__elasticsearch__.refresh_index! + end +end \ No newline at end of file diff --git a/spec/factories/default.rb b/spec/factories/default.rb index 982528f38..f54fc6c86 100644 --- a/spec/factories/default.rb +++ b/spec/factories/default.rb @@ -262,6 +262,11 @@ association :provider, factory: :provider, strategy: :create end + factory :researcher do + sequence(:uid) { |n| "0000-0001-6528-202#{n}" } + name { Faker::Name.name } + end + factory :activity do association :doi, factory: :doi, strategy: :create end diff --git a/spec/jobs/orcid_auto_update_by_id_job_spec.rb b/spec/jobs/orcid_auto_update_by_id_job_spec.rb new file mode 100644 index 000000000..3924a40d8 --- /dev/null +++ b/spec/jobs/orcid_auto_update_by_id_job_spec.rb @@ -0,0 +1,16 @@ +require 'rails_helper' + +describe OrcidAutoUpdateByIdJob, type: :job do + let(:researcher) { create(:researcher) } + subject(:job) { OrcidAutoUpdateByIdJob.perform_later(researcher.uid) } + + it 'queues the job' do + expect { job }.to have_enqueued_job(OrcidAutoUpdateByIdJob) + .on_queue("test_lupo_background") + end + + after do + clear_enqueued_jobs + clear_performed_jobs + end +end diff --git a/spec/models/researcher_spec.rb b/spec/models/researcher_spec.rb index 0cff28dd2..f7e8a656e 100644 --- a/spec/models/researcher_spec.rb +++ b/spec/models/researcher_spec.rb @@ -1,21 +1,8 @@ # frozen_string_literal: true -require 'rails_helper' +require "rails_helper" describe Researcher, type: :model, vcr: true do - describe "find_by_id" do - it "found" do - id = "https://orcid.org/0000-0003-1419-2405" - researchers = Researcher.find_by_id(id) - expect(researchers[:data].size).to eq(1) - expect(researchers[:data].first).to eq(id: "https://orcid.org/0000-0003-1419-2405", name: "Martin Fenner", "givenName" => "Martin", "familyName" => "Fenner") - end - - it "not found" do - id = "https://orcid.org/xxx" - researchers = Researcher.find_by_id(id) - expect(researchers[:data]).to be_nil - expect(researchers[:errors]).to be_nil - end - end + it { is_expected.to validate_presence_of(:uid) } + it { is_expected.to validate_uniqueness_of(:uid) } end diff --git a/spec/requests/researchers_spec.rb b/spec/requests/researchers_spec.rb new file mode 100644 index 000000000..cc8b747b1 --- /dev/null +++ b/spec/requests/researchers_spec.rb @@ -0,0 +1,167 @@ +require 'rails_helper' + +describe "Researchers", type: :request, elasticsearch: true do + let!(:researcher) { create(:researcher) } + let(:token) { User.generate_token } + let(:params) do + { "data" => { "type" => "researchers", + "attributes" => { + "name" => "Martin Fenner" } } } + end + let(:headers) { {'HTTP_ACCEPT'=>'application/vnd.api+json', 'HTTP_AUTHORIZATION' => 'Bearer ' + token } } + + describe 'GET /researchers' do + let!(:researchers) { create_list(:researcher, 3) } + + before do + Researcher.import + sleep 1 + end + + it "returns researchers" do + get "/researchers", nil, headers + + expect(last_response.status).to eq(200) + expect(json['data'].size).to eq(4) + expect(json.dig('meta', 'total')).to eq(4) + end + end + + describe 'GET /researchers/:id' do + context 'when the record exists' do + it 'returns the researcher' do + get "/researchers/#{researcher.uid}", nil, headers + + expect(last_response.status).to eq(200) + expect(json['data']['id']).to eq(researcher.uid) + expect(json['data']['attributes']['name']).to eq(researcher.name) + end + end + end + + describe 'POST /researchers' do + context 'request is valid' do + let(:params) do + { "data" => { "type" => "researchers", + "attributes" => { + "uid" => "0000-0003-2584-9687", + "name" => "James Gill", + "givenNames" => "James", + "familyName" => "Gill" } } } + end + + it 'creates a researcher' do + post '/researchers', params, headers + + expect(json.dig('data', 'attributes', 'name')).to eq("James Gill") + end + + it 'returns status code 201' do + post '/researchers', params, headers + + expect(last_response.status).to eq(201) + end + end + + context 'request uses basic auth' do + let(:params) do + { "data" => { "type" => "researchers", + "attributes" => { + "uid" => "0000-0003-2584-9687", + "name" => "James Gill" } } } + end + let(:admin) { create(:provider, symbol: "ADMIN", role_name: "ROLE_ADMIN", password_input: "12345") } + let(:credentials) { admin.encode_auth_param(username: "ADMIN", password: "12345") } + let(:headers) { {'HTTP_ACCEPT'=>'application/vnd.api+json', 'HTTP_AUTHORIZATION' => 'Basic ' + credentials } } + + it 'creates a researcher' do + post '/researchers', params, headers + + expect(last_response.status).to eq(201) + expect(json.dig('data', 'attributes', 'name')).to eq("James Gill") + end + end + + context 'when the request is missing a required attribute' do + let(:params) do + { "data" => { "type" => "researchers", + "attributes" => { } } } + end + + it 'returns a validation failure message' do + post '/researchers', params, headers + + expect(last_response.status).to eq(422) + expect(json["errors"].first).to eq("source"=>"uid", "title"=>"Can't be blank") + end + end + + context 'when the request is missing a data object' do + let(:params) do + { "type" => "researchers", + "attributes" => { + "uid" => "0000-0003-2584-9687", + "name" => "James Gill" } } + end + + it 'returns status code 400' do + post '/researchers', params, headers + + expect(last_response.status).to eq(400) + end + + # it 'returns a validation failure message' do + # expect(response["exception"]).to eq("#") + # end + end + end + + describe 'PUT /researchers/:id' do + context 'when the record exists' do + let(:params) do + { "data" => { "type" => "researchers", + "attributes" => { + "name" => "James Watt" } } } + end + + it 'updates the record' do + put "/researchers/#{researcher.uid}", params, headers + + expect(last_response.status).to eq(200) + expect(json.dig('data', 'attributes', 'name')).to eq("James Watt") + end + end + end + + # # Test suite for DELETE /researchers/:id + # describe 'DELETE /researchers/:id' do + # before { delete "/researchers/#{researcher.symbol}", headers: headers } + + # it 'returns status code 204' do + # expect(response).to have_http_status(204) + # end + # context 'when the resources doesnt exist' do + # before { delete '/researchers/xxx', params: params.to_json, headers: headers } + + # it 'returns status code 404' do + # expect(response).to have_http_status(404) + # end + + # it 'returns a validation failure message' do + # expect(json["errors"].first).to eq("status"=>"404", "title"=>"The resource you are looking for doesn't exist.") + # end + # end + # end + + # describe 'POST /researchers/set-test-prefix' do + # before { post '/researchers/set-test-prefix', headers: headers } + + # it 'returns success' do + # expect(json['message']).to eq("Test prefix added.") + # end + + # it 'returns status code 200' do + # expect(response).to have_http_status(200) + # end + # end +end diff --git a/spec/support/elasticsearch_helper.rb b/spec/support/elasticsearch_helper.rb index a4d4ecdd3..fa78570da 100644 --- a/spec/support/elasticsearch_helper.rb +++ b/spec/support/elasticsearch_helper.rb @@ -1,5 +1,5 @@ ## https://github.com/elastic/elasticsearch-ruby/issues/462 -SEARCHABLE_MODELS = [Client, Provider, Doi, Event] +SEARCHABLE_MODELS = [Client, Provider, Doi, Event, Researcher] RSpec.configure do |config| config.around :all, elasticsearch: true do |example|