Skip to content

Commit

Permalink
Merge pull request #1373 from CruGlobal/GT-1852-API-Support-tracking-…
Browse files Browse the repository at this point in the history
…training-tips-a-user-has-completed

[GT-1852] API support (the) tracking (of) training tips (that) a user has completed
  • Loading branch information
andrewroth authored May 21, 2024
2 parents 237f863 + 72d1ffa commit 1b49797
Show file tree
Hide file tree
Showing 12 changed files with 294 additions and 6 deletions.
8 changes: 4 additions & 4 deletions app/controllers/favorite_tools_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@ def index

def create
tool_ids.each do |tool_id|
current_user.tools << Resource.find(tool_id) unless current_user.tool_ids.include?(tool_id.to_i)
@user.tools << Resource.find(tool_id) unless @user.tool_ids.include?(tool_id.to_i)
end
render_current_favorites
end

def destroy
current_user.favorite_tools.where(tool_id: tool_ids).delete_all
current_user.tools.reload
@user.favorite_tools.where(tool_id: tool_ids).delete_all
@user.tools.reload
render_current_favorites
end

Expand All @@ -36,6 +36,6 @@ def validate_ids
end

def render_current_favorites
render json: current_user.tools, include: params[:include], fields: field_params({resource: []})
render json: @user.tools, include: params[:include], fields: field_params({resource: []})
end
end
32 changes: 32 additions & 0 deletions app/controllers/training_tips_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
class TrainingTipsController < WithUserController
before_action :convert_hyphen_to_dash, only: [:create, :update]

def create
user_training_tip = @user.user_training_tips.create!(permitted_params)
response.headers["Location"] = "users/#{@user.id}/training-tips/#{user_training_tip.id}"
render json: user_training_tip, status: :created
rescue ActiveRecord::RecordInvalid => e
render json: {errors: formatted_errors("record_invalid", e)}, status: :unprocessable_entity
end

def update
user_training_tip = @user.user_training_tips.find(params[:id])
user_training_tip.update!(permitted_params)
response.headers["Location"] = "users/me/training-tips/#{user_training_tip.id}"
render json: user_training_tip
end

def destroy
user_training_tip = @user.user_training_tips.find(params[:id])
user_training_tip.destroy!
head :no_content
end

protected

def permitted_params
tool_id = params.dig(:data, :relationships, :tool, :data, :id).to_i
language_id = params.dig(:data, :relationships, :language, :data, :id).to_i
permit_params(:tip_id, :is_completed).merge(tool_id: tool_id, language_id: language_id)
end
end
1 change: 1 addition & 0 deletions app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class User < ApplicationRecord
has_many :user_counters, dependent: :destroy
has_many :favorite_tools, dependent: :destroy
has_many :tools, through: :favorite_tools
has_many :user_training_tips, dependent: :destroy

has_many :user_attributes, dependent: :destroy

Expand Down
10 changes: 10 additions & 0 deletions app/models/user_training_tip.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class UserTrainingTip < ActiveRecord::Base
validates :tool_id, uniqueness: {scope: [:user_id, :tool_id, :language_id, :tip_id], message: "combination already exists"}
validates :tip_id, presence: true

belongs_to :user
belongs_to :language
belongs_to :tool, class_name: "Resource"
end
1 change: 1 addition & 0 deletions app/serializers/user_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class UserSerializer < ActiveModel::Serializer
attribute :last_name, key: "family-name"

has_many :tools, key: "favorite-tools"
has_many :user_training_tips, key: "training-tips"

def created_at
object.created_at.iso8601 # without this, the default serializer datetime will add 3 ms digits which we prefer not to have
Expand Down
11 changes: 11 additions & 0 deletions app/serializers/user_training_tip_serializer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class UserTrainingTipSerializer < ActiveModel::Serializer
attribute :tip_id, key: "tip-id"
attribute :is_completed, key: "is-completed"

type "training-tip"

belongs_to :language
belongs_to :tool
end
8 changes: 6 additions & 2 deletions config/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,14 @@
delete "users/:id", to: "users#destroy"
patch "users/:id", to: "users#update"

scope "users/me/relationships" do
scope "users/:user_id/relationships" do
resources :favorite_tools, path: "favorite-tools", only: [:index, :create]
end
delete "users/me/relationships/favorite-tools", to: "favorite_tools#destroy"
delete "users/:user_id/relationships/favorite-tools", to: "favorite_tools#destroy"

scope "users/:user_id" do
resources :training_tips, path: "training-tips", only: [:create, :update, :destroy]
end

get "monitors/lb"
get "monitors/commit"
Expand Down
15 changes: 15 additions & 0 deletions db/migrate/20230825233539_create_user_training_tip.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class CreateUserTrainingTip < ActiveRecord::Migration[6.1]
def change
create_table :user_training_tips do |t|
t.references :user, null: false, foreign_key: true
t.references :tool, null: false, foreign_key: {to_table: :resources}
t.references :language, null: false, foreign_key: true
t.string :tip_id
t.boolean :is_completed

t.timestamps
end

add_index :user_training_tips, [:user_id, :tool_id, :language_id, :tip_id], unique: true, name: "training-tips-unique-index"
end
end
17 changes: 17 additions & 0 deletions db/schema.rb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

175 changes: 175 additions & 0 deletions spec/acceptance/training_tips_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
# frozen_string_literal: true

require "acceptance_helper"

resource "UserTrainingTips" do
header "Accept", "application/vnd.api+json"
header "Content-Type", "application/vnd.api+json"

let!(:user) { FactoryBot.create(:user) }
let(:raw_post) { params.to_json }
let(:authorization) { AuthToken.generic_token }

post "users/me/training-tips" do
let(:attributes) do
{
"tip-id": "tip id here",
"is-completed": true
}
end

let(:attributes_invalid) do
{
"tip-id": ""
}
end

let(:relationships) {
{
language: {
data: {
type: "language",
id: Language.first.id
}
},

tool: {
data: {
type: "resource",
id: Resource.first.id
}
}
}
}

requires_okta_login

it "create user training tip" do
do_request data: {type: "training-tip", attributes: attributes, relationships: relationships}

expect(status).to eq(201)
data = JSON.parse(response_body)["data"]
expect(data).not_to be_nil
expect(data["attributes"]).to eq(serializer_output_style(attributes))
expect(data["relationships"]).to eq(serializer_output_style(relationships))
end

it "returns error message when user training tip is not created" do
do_request data: {type: "training-tips", attributes: attributes_invalid, relationships: relationships}

expect(status).to eq(400)
expect(JSON.parse(response_body)["errors"]).not_to be_empty
expect(JSON.parse(response_body)["errors"][0]["detail"]).to eql "Validation failed: Tip can't be blank"
end
end

put "users/me/training-tips/:id" do
requires_okta_login

let(:tool) { Resource.first }
let(:language) { Language.first }
let(:training_tip) { FactoryBot.create(:user_training_tip, user_id: user.id, tool_id: tool.id, language_id: language.id, tip_id: "tip", is_completed: false) }
let(:id) { training_tip.id }

let(:attributes) do
{
"tip-id": "new tip id",
"is-completed": true
}
end

let(:relationships) do
{
language: {
data: {
type: "language",
id: Language.second.id
}
},

tool: {
data: {
type: "resource",
id: Resource.second.id
}
}
}
end

it "updates a user training tip" do
do_request id: training_tip.id, data: {
type: "training-tip",
attributes: attributes,
relationships: relationships
}

expect(status).to eq(200)
data = JSON.parse(response_body)["data"]
expect(data).not_to be_nil
expect(data["attributes"]).to eq(serializer_output_style(attributes))
expect(data["relationships"]).to eq(serializer_output_style(relationships))
end
end

delete "users/me/training-tips/:id" do
requires_okta_login

let(:resource) { Resource.first }
let(:tool) { Resource.first }
let(:language) { Language.first }
let!(:training_tip) { FactoryBot.create(:user_training_tip, user_id: user.id, tool_id: tool.id, language_id: language.id, tip_id: "tip", is_completed: false) }
let(:id) { training_tip.id }
let(:invalid_id) { -1 }
requires_authorization

it "delete user training tip succeed and returns ':not_content'" do
expect do
do_request id: id
end.to change(UserTrainingTip, :count).by(-1)

expect(status).to be(204)
expect(UserTrainingTip.find_by(id: id)).to be_nil
end

it "delete user training tip fails and returns ':not_found'" do
do_request id: invalid_id

expect(status).to be(404)
end
end

get "users/me?include=training-tips" do
requires_authorization

let(:tool) { Resource.first }
let(:language) { Language.first }
let!(:training_tip_1) { FactoryBot.create(:user_training_tip, user_id: user.id, tool_id: tool.id, language_id: language.id, tip_id: "tip", is_completed: false) }

let(:tool_2) { Resource.second }
let(:language_2) { Language.second }
let!(:training_tip_2) { FactoryBot.create(:user_training_tip, user_id: user.id, tool_id: tool_2.id, language_id: language_2.id, tip_id: "tip", is_completed: false) }

# this should not be included
let(:other_user) { FactoryBot.create(:user) }
let(:tool_3) { Resource.first }
let(:language_3) { Language.first }
let!(:training_tip_3) { FactoryBot.create(:user_training_tip, user_id: other_user.id, tool_id: tool_3.id, language_id: language_3.id, tip_id: "tip", is_completed: false) }

it "gets all training tips for a user" do
do_request
expect(status).to eq(200)

data = JSON.parse(response_body)["data"]
expect(data["relationships"]["training-tips"]["data"]).to eq([{"id" => training_tip_1.id.to_s, "type" => "training-tip"}, {"id" => training_tip_2.id.to_s, "type" => "training-tip"}])

included = JSON.parse(response_body)["included"]
expect(included.first["id"]).to eq(training_tip_1.id.to_s)
expect(included.second["id"]).to eq(training_tip_2.id.to_s)
end
end

# change _ to - in keys, and make any ids (number values) strings
def serializer_output_style(hash)
hash.deep_transform_keys { |key| key.to_s.tr("_", "-") }.deep_transform_values { |v| /^\d+$/.match?(v.to_s) ? v.to_s : v }
end
end
4 changes: 4 additions & 0 deletions spec/factories/user_training_tips.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
FactoryBot.define do
factory :user_training_tip do
end
end
18 changes: 18 additions & 0 deletions spec/models/training_tip_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# frozen_string_literal: true

require "rails_helper"

RSpec.describe UserTrainingTip, type: :model do
context "create new training tip" do
let(:tip_id) { "tip" }
let(:user) { FactoryBot.create(:user) }
let(:tool) { Resource.first }
let(:language) { Language.first }

subject { UserTrainingTip.new(tool_id: tool.id, language_id: language.id, tip_id: tip_id, is_completed: true, user: user) }

it "is valid" do
expect(subject).to be_valid
end
end
end

0 comments on commit 1b49797

Please sign in to comment.