Skip to content

Commit 1b49797

Browse files
authored
Merge pull request #1373 from CruGlobal/GT-1852-API-Support-tracking-training-tips-a-user-has-completed
[GT-1852] API support (the) tracking (of) training tips (that) a user has completed
2 parents 237f863 + 72d1ffa commit 1b49797

12 files changed

+294
-6
lines changed

app/controllers/favorite_tools_controller.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ def index
77

88
def create
99
tool_ids.each do |tool_id|
10-
current_user.tools << Resource.find(tool_id) unless current_user.tool_ids.include?(tool_id.to_i)
10+
@user.tools << Resource.find(tool_id) unless @user.tool_ids.include?(tool_id.to_i)
1111
end
1212
render_current_favorites
1313
end
1414

1515
def destroy
16-
current_user.favorite_tools.where(tool_id: tool_ids).delete_all
17-
current_user.tools.reload
16+
@user.favorite_tools.where(tool_id: tool_ids).delete_all
17+
@user.tools.reload
1818
render_current_favorites
1919
end
2020

@@ -36,6 +36,6 @@ def validate_ids
3636
end
3737

3838
def render_current_favorites
39-
render json: current_user.tools, include: params[:include], fields: field_params({resource: []})
39+
render json: @user.tools, include: params[:include], fields: field_params({resource: []})
4040
end
4141
end
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
class TrainingTipsController < WithUserController
2+
before_action :convert_hyphen_to_dash, only: [:create, :update]
3+
4+
def create
5+
user_training_tip = @user.user_training_tips.create!(permitted_params)
6+
response.headers["Location"] = "users/#{@user.id}/training-tips/#{user_training_tip.id}"
7+
render json: user_training_tip, status: :created
8+
rescue ActiveRecord::RecordInvalid => e
9+
render json: {errors: formatted_errors("record_invalid", e)}, status: :unprocessable_entity
10+
end
11+
12+
def update
13+
user_training_tip = @user.user_training_tips.find(params[:id])
14+
user_training_tip.update!(permitted_params)
15+
response.headers["Location"] = "users/me/training-tips/#{user_training_tip.id}"
16+
render json: user_training_tip
17+
end
18+
19+
def destroy
20+
user_training_tip = @user.user_training_tips.find(params[:id])
21+
user_training_tip.destroy!
22+
head :no_content
23+
end
24+
25+
protected
26+
27+
def permitted_params
28+
tool_id = params.dig(:data, :relationships, :tool, :data, :id).to_i
29+
language_id = params.dig(:data, :relationships, :language, :data, :id).to_i
30+
permit_params(:tip_id, :is_completed).merge(tool_id: tool_id, language_id: language_id)
31+
end
32+
end

app/models/user.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ class User < ApplicationRecord
44
has_many :user_counters, dependent: :destroy
55
has_many :favorite_tools, dependent: :destroy
66
has_many :tools, through: :favorite_tools
7+
has_many :user_training_tips, dependent: :destroy
78

89
has_many :user_attributes, dependent: :destroy
910

app/models/user_training_tip.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
class UserTrainingTip < ActiveRecord::Base
4+
validates :tool_id, uniqueness: {scope: [:user_id, :tool_id, :language_id, :tip_id], message: "combination already exists"}
5+
validates :tip_id, presence: true
6+
7+
belongs_to :user
8+
belongs_to :language
9+
belongs_to :tool, class_name: "Resource"
10+
end

app/serializers/user_serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ class UserSerializer < ActiveModel::Serializer
77
attribute :last_name, key: "family-name"
88

99
has_many :tools, key: "favorite-tools"
10+
has_many :user_training_tips, key: "training-tips"
1011

1112
def created_at
1213
object.created_at.iso8601 # without this, the default serializer datetime will add 3 ms digits which we prefer not to have
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
class UserTrainingTipSerializer < ActiveModel::Serializer
4+
attribute :tip_id, key: "tip-id"
5+
attribute :is_completed, key: "is-completed"
6+
7+
type "training-tip"
8+
9+
belongs_to :language
10+
belongs_to :tool
11+
end

config/routes.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,10 +66,14 @@
6666
delete "users/:id", to: "users#destroy"
6767
patch "users/:id", to: "users#update"
6868

69-
scope "users/me/relationships" do
69+
scope "users/:user_id/relationships" do
7070
resources :favorite_tools, path: "favorite-tools", only: [:index, :create]
7171
end
72-
delete "users/me/relationships/favorite-tools", to: "favorite_tools#destroy"
72+
delete "users/:user_id/relationships/favorite-tools", to: "favorite_tools#destroy"
73+
74+
scope "users/:user_id" do
75+
resources :training_tips, path: "training-tips", only: [:create, :update, :destroy]
76+
end
7377

7478
get "monitors/lb"
7579
get "monitors/commit"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
class CreateUserTrainingTip < ActiveRecord::Migration[6.1]
2+
def change
3+
create_table :user_training_tips do |t|
4+
t.references :user, null: false, foreign_key: true
5+
t.references :tool, null: false, foreign_key: {to_table: :resources}
6+
t.references :language, null: false, foreign_key: true
7+
t.string :tip_id
8+
t.boolean :is_completed
9+
10+
t.timestamps
11+
end
12+
13+
add_index :user_training_tips, [:user_id, :tool_id, :language_id, :tip_id], unique: true, name: "training-tips-unique-index"
14+
end
15+
end

db/schema.rb

Lines changed: 17 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
# frozen_string_literal: true
2+
3+
require "acceptance_helper"
4+
5+
resource "UserTrainingTips" do
6+
header "Accept", "application/vnd.api+json"
7+
header "Content-Type", "application/vnd.api+json"
8+
9+
let!(:user) { FactoryBot.create(:user) }
10+
let(:raw_post) { params.to_json }
11+
let(:authorization) { AuthToken.generic_token }
12+
13+
post "users/me/training-tips" do
14+
let(:attributes) do
15+
{
16+
"tip-id": "tip id here",
17+
"is-completed": true
18+
}
19+
end
20+
21+
let(:attributes_invalid) do
22+
{
23+
"tip-id": ""
24+
}
25+
end
26+
27+
let(:relationships) {
28+
{
29+
language: {
30+
data: {
31+
type: "language",
32+
id: Language.first.id
33+
}
34+
},
35+
36+
tool: {
37+
data: {
38+
type: "resource",
39+
id: Resource.first.id
40+
}
41+
}
42+
}
43+
}
44+
45+
requires_okta_login
46+
47+
it "create user training tip" do
48+
do_request data: {type: "training-tip", attributes: attributes, relationships: relationships}
49+
50+
expect(status).to eq(201)
51+
data = JSON.parse(response_body)["data"]
52+
expect(data).not_to be_nil
53+
expect(data["attributes"]).to eq(serializer_output_style(attributes))
54+
expect(data["relationships"]).to eq(serializer_output_style(relationships))
55+
end
56+
57+
it "returns error message when user training tip is not created" do
58+
do_request data: {type: "training-tips", attributes: attributes_invalid, relationships: relationships}
59+
60+
expect(status).to eq(400)
61+
expect(JSON.parse(response_body)["errors"]).not_to be_empty
62+
expect(JSON.parse(response_body)["errors"][0]["detail"]).to eql "Validation failed: Tip can't be blank"
63+
end
64+
end
65+
66+
put "users/me/training-tips/:id" do
67+
requires_okta_login
68+
69+
let(:tool) { Resource.first }
70+
let(:language) { Language.first }
71+
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) }
72+
let(:id) { training_tip.id }
73+
74+
let(:attributes) do
75+
{
76+
"tip-id": "new tip id",
77+
"is-completed": true
78+
}
79+
end
80+
81+
let(:relationships) do
82+
{
83+
language: {
84+
data: {
85+
type: "language",
86+
id: Language.second.id
87+
}
88+
},
89+
90+
tool: {
91+
data: {
92+
type: "resource",
93+
id: Resource.second.id
94+
}
95+
}
96+
}
97+
end
98+
99+
it "updates a user training tip" do
100+
do_request id: training_tip.id, data: {
101+
type: "training-tip",
102+
attributes: attributes,
103+
relationships: relationships
104+
}
105+
106+
expect(status).to eq(200)
107+
data = JSON.parse(response_body)["data"]
108+
expect(data).not_to be_nil
109+
expect(data["attributes"]).to eq(serializer_output_style(attributes))
110+
expect(data["relationships"]).to eq(serializer_output_style(relationships))
111+
end
112+
end
113+
114+
delete "users/me/training-tips/:id" do
115+
requires_okta_login
116+
117+
let(:resource) { Resource.first }
118+
let(:tool) { Resource.first }
119+
let(:language) { Language.first }
120+
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) }
121+
let(:id) { training_tip.id }
122+
let(:invalid_id) { -1 }
123+
requires_authorization
124+
125+
it "delete user training tip succeed and returns ':not_content'" do
126+
expect do
127+
do_request id: id
128+
end.to change(UserTrainingTip, :count).by(-1)
129+
130+
expect(status).to be(204)
131+
expect(UserTrainingTip.find_by(id: id)).to be_nil
132+
end
133+
134+
it "delete user training tip fails and returns ':not_found'" do
135+
do_request id: invalid_id
136+
137+
expect(status).to be(404)
138+
end
139+
end
140+
141+
get "users/me?include=training-tips" do
142+
requires_authorization
143+
144+
let(:tool) { Resource.first }
145+
let(:language) { Language.first }
146+
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) }
147+
148+
let(:tool_2) { Resource.second }
149+
let(:language_2) { Language.second }
150+
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) }
151+
152+
# this should not be included
153+
let(:other_user) { FactoryBot.create(:user) }
154+
let(:tool_3) { Resource.first }
155+
let(:language_3) { Language.first }
156+
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) }
157+
158+
it "gets all training tips for a user" do
159+
do_request
160+
expect(status).to eq(200)
161+
162+
data = JSON.parse(response_body)["data"]
163+
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"}])
164+
165+
included = JSON.parse(response_body)["included"]
166+
expect(included.first["id"]).to eq(training_tip_1.id.to_s)
167+
expect(included.second["id"]).to eq(training_tip_2.id.to_s)
168+
end
169+
end
170+
171+
# change _ to - in keys, and make any ids (number values) strings
172+
def serializer_output_style(hash)
173+
hash.deep_transform_keys { |key| key.to_s.tr("_", "-") }.deep_transform_values { |v| /^\d+$/.match?(v.to_s) ? v.to_s : v }
174+
end
175+
end

spec/factories/user_training_tips.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
FactoryBot.define do
2+
factory :user_training_tip do
3+
end
4+
end

spec/models/training_tip_spec.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
# frozen_string_literal: true
2+
3+
require "rails_helper"
4+
5+
RSpec.describe UserTrainingTip, type: :model do
6+
context "create new training tip" do
7+
let(:tip_id) { "tip" }
8+
let(:user) { FactoryBot.create(:user) }
9+
let(:tool) { Resource.first }
10+
let(:language) { Language.first }
11+
12+
subject { UserTrainingTip.new(tool_id: tool.id, language_id: language.id, tip_id: tip_id, is_completed: true, user: user) }
13+
14+
it "is valid" do
15+
expect(subject).to be_valid
16+
end
17+
end
18+
end

0 commit comments

Comments
 (0)