Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add org_id to api_clients table and refactor of API v1 services #2725

Merged
merged 13 commits into from
Nov 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/controllers/api/v1/base_api_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ def heartbeat

def render_error(errors:, status:)
@payload = { errors: [errors] }
render "/api/v1/error", status: status and return
render "/api/v1/error", status: status
end

private
Expand Down
167 changes: 82 additions & 85 deletions app/controllers/api/v1/plans_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,85 +9,74 @@ class PlansController < BaseApiController
respond_to :json

# GET /api/v1/plans/:id
# rubocop:disable Metrics/AbcSize
def show
plans = Plan.where(id: params[:id]).limit(1)

if plans.any?
if client.is_a?(User)
# If the specified plan does not belong to the org or the owner's org
if plans.first.org_id != client.org_id &&
plans.first.owner&.org_id != client.org_id

# Kaminari pagination requires an ActiveRecord resultset :/
plans = Plan.where(id: nil).limit(1)
end

elsif client.is_a?(ApiClient) && plans.first.api_client_id != client.id &&
!plans.first.publicly_visible?
# Kaminari pagination requires an ActiveRecord resultset :/
plans = Plan.where(id: nil).limit(1)
end

if plans.present? && plans.any?
@items = paginate_response(results: plans)
render "/api/v1/plans/index", status: :ok
else
render_error(errors: [_("Plan not found")], status: :not_found)
end
plans = Api::V1::PlansPolicy::Scope.new(client, Plan).resolve
.where(id: params[:id]).limit(1)

if plans.present? && plans.any?
@items = paginate_response(results: plans)
render "/api/v1/plans/index", status: :ok
else
render_error(errors: [_("Plan not found")], status: :not_found)
end
end
# rubocop:enable Metrics/AbcSize
# rubocop:enable

# POST /api/v1/plans
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockNesting
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
def create
dmp = @json.with_indifferent_access.fetch(:items, []).first.fetch(:dmp, {})

# If a dmp_id was passed in try to find it
if dmp[:dmp_id].present? && dmp[:dmp_id][:identifier].present?
scheme = IdentifierScheme.by_name(dmp[:dmp_id][:type]).first
dmp_id = Identifier.where(value: dmp[:dmp_id][:identifier],
identifier_scheme: scheme)
end

# Skip if this is an existing DMP
if dmp_id.present?
render_error(errors: _("Plan already exists. Send an update instead."),
status: :bad_request)
# Do a pass through the raw JSON and check to make sure all required fields
# were present. If not, return the specific errors
errs = Api::V1::JsonValidationService.validation_errors(json: dmp)
render_error(errors: errs, status: :bad_request) and return if errs.any?

# Convert the JSON into a Plan and it's associations
plan = Api::V1::Deserialization::Plan.deserialize(json: dmp)
if plan.present?
save_err = _("Unable to create your DMP")
exists_err = _("Plan already exists. Send an update instead.")
no_org_err = _("Could not determine ownership of the DMP. Please add an
:affiliation to the :contact")

# Try to determine the Plan's owner
owner = determine_owner(client: client, plan: plan)
plan.org = owner.org if owner.present? && plan.org.blank?
render_error(errors: no_org_err, status: :bad_request) and return unless plan.org.present?

# Validate the plan and it's associations and return errors with context
# e.g. 'Contact affiliation name can't be blank' instead of 'name can't be blank'
errs = Api::V1::ContextualErrorService.process_plan_errors(plan: plan)

# The resulting plan (our its associations were invalid)
render_error(errors: errs, status: :bad_request) and return if errs.any?
# Skip if this is an existing DMP
render_error(errors: exists_err, status: :bad_request) and return unless plan.new_record?

# If we cannot save for some reason then return an error
plan = Api::V1::PersistenceService.safe_save(plan: plan)
# rubocop:disable Layout/LineLength
render_error(errors: save_err, status: :internal_server_error) and return if plan.new_record?

# rubocop:enable Layout/LineLength

# If the plan was generated by an ApiClient then associate them
plan.update(api_client_id: client.id) if client.is_a?(ApiClient)

# Invite the Owner if they are a Contributor then attach the Owner to the Plan
owner = invite_contributor(contributor: owner) if owner.is_a?(Contributor)
plan.add_user!(owner.id, :creator)

# Kaminari Pagination requires an ActiveRecord result set :/
@items = paginate_response(results: Plan.where(id: plan.id))
render "/api/v1/plans/index", status: :created
else
# Time prior to JSON parser service call which will create the plan so
# we can stop the creation of duplicate plans below
now = (Time.now - 1.minute)
plan = Api::V1::Deserialization::Plan.deserialize!(json: dmp)

if plan.present?
if plan.created_at.utc < now.utc
render_error(errors: _("Plan already exists. Send an update instead."),
status: :bad_request)

else
# If the plan was generated by an ApiClient then associate them
plan.update(api_client_id: client.id) if client.is_a?(ApiClient)

assign_roles(plan: plan)

# Kaminari Pagination requires an ActiveRecord result set :/
@items = paginate_response(results: Plan.where(id: plan.id))
render "/api/v1/plans/index", status: :created
end
else
render_error(errors: [_("Invalid JSON")], status: :bad_request)
end
render_error(errors: [_("Invalid JSON!")], status: :bad_request)
end
rescue JSON::ParserError
render_error(errors: [_("Invalid JSON")], status: :bad_request)
end
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength, Metrics/BlockNesting
# rubocop:enable
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength

# GET /api/v1/plans
def index
Expand All @@ -111,33 +100,42 @@ def dmp_params
params.require(:dmp).permit(plan_permitted_params).to_h
end

def assign_roles(plan:)
# Attach all of the authors and then invite them if necessary
owner = nil
plan.contributors.data_curation.each do |contributor|
user = contributor_to_user(contributor: contributor)
next unless user.present?

# Attach the role
role = Role.new(user: user, plan: plan)
role.creator = true if contributor.data_curation?
# We only want one owner/creator so jusst use the 1st contributor
# which should be the contact in the JSON input
owner = contributor if contributor.data_curation?
role.administrator = true if contributor.data_curation? &&
!contributor.present?
role.save
end
def plan_exists?(json:)
return false unless json.present? &&
json[:dmp_id].present? &&
json[:dmp_id][:identifier].present?

scheme = IdentifierScheme.by_name(json[:dmp_id][:type]).first
Identifier.where(value: json[:dmp_id][:identifier], identifier_scheme: scheme).any?
end

# Get the Plan's owner
def determine_owner(client:, plan:)
contact = plan.contributors.select(&:data_curation?).first
# Use the contact if it was sent in and has an affiliation defined
return contact if contact.present? && contact.org.present?

# If the contact has no affiliation defined, see if they are already a User
user = lookup_user(contributor: contact)
return user if user.present?

# Otherwise just return the client
client
end

# rubocop:disable Metrics/AbcSize
def contributor_to_user(contributor:)
def lookup_user(contributor:)
return nil unless contributor.present?

identifiers = contributor.identifiers.map do |id|
{ name: id.identifier_scheme&.name, value: id.value }
end
user = User.from_identifiers(array: identifiers) if identifiers.any?
user = User.find_by(email: contributor.email) unless user.present?
return user if user.present?
user
end

def invite_contributor(contributor:)
return nil unless contributor.present?

# If the user was not found, invite them and attach any know identifiers
names = contributor.name&.split || [""]
Expand All @@ -155,7 +153,6 @@ def contributor_to_user(contributor:)
end
user
end
# rubocop:enable Metrics/AbcSize

end

Expand Down
21 changes: 18 additions & 3 deletions app/controllers/super_admin/api_clients_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ class ApiClientsController < ApplicationController

respond_to :html

include OrgSelectable

helper PaginableHelper

# GET /api_clients
Expand All @@ -29,7 +31,13 @@ def edit
# POST /api_clients
def create
authorize(ApiClient)
@api_client = ApiClient.new(api_client_params)

# Translate the Org selection
org = org_from_params(params_in: api_client_params, allow_create: false)
attrs = remove_org_selection_params(params_in: api_client_params)

@api_client = ApiClient.new(attrs)
@api_client.org = org if org.present?

if @api_client.save
UserMailer.api_credentials(@api_client).deliver_now
Expand All @@ -49,7 +57,13 @@ def create
def update
@api_client = ApiClient.find(params[:id])
authorize(@api_client)
if @api_client.update(api_client_params)

# Translate the Org selection
org = org_from_params(params_in: api_client_params, allow_create: false)
@api_client.org = org
attrs = remove_org_selection_params(params_in: api_client_params)

if @api_client.update(attrs)
flash.now[:notice] = success_message(@api_client, _("updated"))
else
flash.now[:alert] = failure_message(@api_client, _("update"))
Expand Down Expand Up @@ -93,7 +107,8 @@ def email_credentials
def api_client_params
params.require(:api_client).permit(:name, :description, :homepage,
:contact_name, :contact_email,
:client_id, :client_secret)
:client_id, :client_secret,
:org_id, :org_name, :org_sources, :org_crosswalk)
end

end
Expand Down
1 change: 1 addition & 0 deletions app/javascript/packs/application.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ import '../src/orgAdmin/templates/index';
import '../src/orgAdmin/templates/new';

// SuperAdmin view specific JS
import '../src/superAdmin/apiClients/form';
import '../src/superAdmin/notifications/edit';
import '../src/superAdmin/themes/newEdit';
import '../src/superAdmin/users/edit';
Expand Down
7 changes: 7 additions & 0 deletions app/javascript/src/superAdmin/apiClients/form.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { initAutocomplete } from '../../utils/autoComplete';

$(() => {
if ($('#api-client-org-controls').length > 0) {
initAutocomplete('#api-client-org-controls .autocomplete');
}
});
6 changes: 6 additions & 0 deletions app/models/api_client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,15 @@
# last_access :datetime
# created_at :datetime
# updated_at :datetime
# org_id :integer
#
# Indexes
#
# index_api_clients_on_name (name)
#
# Foreign Keys
#
# fk_rails_... (org_id => orgs.id)

class ApiClient < ApplicationRecord

Expand All @@ -30,6 +34,8 @@ class ApiClient < ApplicationRecord
# = Associations =
# ================

belongs_to :org, optional: true

has_many :plans

# If the Client_id or client_secret are nil generate them
Expand Down
29 changes: 28 additions & 1 deletion app/models/contributor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ class Contributor < ApplicationRecord

belongs_to :org, optional: true

belongs_to :plan
belongs_to :plan, optional: true

# =====================
# = Nested attributes =
Expand Down Expand Up @@ -96,6 +96,33 @@ def default_role

end

# Check for equality by matching on Plan, ORCID, email or name
def ==(other)
return false unless other.is_a?(Contributor) && plan == other.plan

current_orcid = identifier_for_scheme(scheme: "orcid")&.value
new_orcid = other.identifier_for_scheme(scheme: "orcid")&.value

email == other.email || name == other.name ||
(current_orcid.present? && current_orcid == new_orcid)
end

# Merges the contents of the other Contributor into this one while retaining
# any existing information
# rubocop:disable Metrics/AbcSize
def merge(other)
self.org = other.org unless org.present?
self.email = other.email unless email.present?
self.name = other.name unless name.present?
self.phone = other.phone unless phone.present?
self.investigation = true if other.investigation? && !investigation?
self.data_curation = true if other.data_curation? && !data_curation?
self.project_administration = true if other.project_administration? && !project_administration?
consolidate_identifiers!(array: other.identifiers.to_a)
self
end
# rubocop:enable Metrics/AbcSize

# ===================
# = Private Methods =
# ===================
Expand Down
2 changes: 2 additions & 0 deletions app/policies/api/v1/plans_policy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,15 @@ def initialize(client, scope)
## return the visible plans (via the API) to a given client
# ALL can view: public
# ApiClient can view: anything from the API client
# anything belonging to their Org (if applicable)
# User (non-admin) can view: any personal or organisationally_visible
# User (admin) can view: all from users of their organisation
# rubocop:disable Metrics/AbcSize
def resolve
ids = Plan.publicly_visible.pluck(:id)
if client.is_a?(ApiClient)
ids += client.plans.pluck(&:id)
ids += client.org.plans.pluck(&:id) if client.org.present?
elsif client.is_a?(User)
ids += client.org.plans.organisationally_visible.pluck(:id)
ids += client.plans.pluck(:id)
Expand Down
Loading