Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
ebced1c
Added ContentStatus ctrller
hepu Nov 5, 2025
51b6162
Using Language association with ResourceScore and ResourceDefaultOrder
hepu Nov 25, 2025
0add629
Merge branch 'feature/lang-model-association' into feature/content-st…
hepu Nov 25, 2025
ce1f9c3
Updated Content Status response
hepu Nov 25, 2025
b1acd1d
Touching resource on score or default order update/creation
hepu Nov 25, 2025
75af30d
Merge branch 'feature/mass-update-endpoint' into feature/content-status
hepu Nov 25, 2025
4f16f57
Added spec file
hepu Nov 25, 2025
74333d3
Updated spec for content_status
hepu Nov 25, 2025
3ee6646
Fixed unassigned scope on default_orders
hepu Nov 25, 2025
2ed61cc
Added mass_update endpoint for default orders and mass_update_ranked …
hepu Nov 26, 2025
b283daf
Updated spec
hepu Nov 26, 2025
6d7424c
Updated queries
hepu Dec 3, 2025
3ba7024
Fixed lints + Updated tests
hepu Dec 3, 2025
05f45b8
Fixed lint issues
hepu Dec 3, 2025
01f6df0
Added CRUD endpoints for ResourceScores
hepu Dec 4, 2025
d251a4d
Fixed route
hepu Dec 4, 2025
d333ba1
Updated brakeman.ignore
hepu Dec 4, 2025
c0ab607
Fixed lint issues
hepu Dec 4, 2025
bf90523
Removed duplicated endpoints
hepu Dec 4, 2025
e1365ed
Removed unused code
hepu Dec 4, 2025
593da93
Merge branch 'master' into feature/content-status
hepu Dec 9, 2025
c0bd2d6
Fixed lint issues
hepu Dec 9, 2025
0eca39d
Merge branch 'master' into feature/crud-endpoints
hepu Dec 9, 2025
8d603ac
Moved Default Orders to ResourceDefaultOrder endpoints
hepu Dec 10, 2025
c86b9d4
Adding validation for resource_type param
hepu Dec 10, 2025
55b764e
Added language validation on default_orders#index
hepu Dec 11, 2025
6945db8
Fixed lint issue
hepu Dec 11, 2025
481dba5
Added language validation on resource_default_orders#index
hepu Dec 11, 2025
b9fb4cc
Updated language query
hepu Dec 11, 2025
30640ed
Removed downcase from error msg
hepu Dec 11, 2025
55a0e91
Fixed last_update value
hepu Dec 12, 2025
69269b4
Merge branch 'feature/content-status' into feature/crud-endpoints
hepu Dec 15, 2025
ab8a3d3
Adding back default_order route + added language fallback
hepu Dec 15, 2025
2fd0b62
Fixed lang not found on default#mass_update
hepu Dec 16, 2025
fbde6e6
Ensuring mass_update endpoints remove all unselected resources
hepu Jan 14, 2026
34cda59
Added x86_64-darwin-25 to plaforms in Gemfile.lock. Added field param…
JasonBennett41 Jan 27, 2026
1c13db5
Refactored ResourceDefaultOrdersController. Updated model validations…
JasonBennett41 Jan 29, 2026
dcf8a57
Removed comments and logs. Ran linting commands. Made rspec happy ex…
JasonBennett41 Jan 30, 2026
a6631c5
Clean up spec resources
JasonBennett41 Jan 30, 2026
13db6d1
Resolve Gemfile conflicts
JasonBennett41 Jan 30, 2026
c5d8573
Removed rubocop ignore comment
JasonBennett41 Jan 31, 2026
c8ea1fd
Upgrade brakeman to latest (8.0.1)
JasonBennett41 Jan 31, 2026
a5289d9
Add missing specs to pass CodeCov check
JasonBennett41 Jan 31, 2026
e45f3d3
Switch to generic x86_64-darwin instead of versioned
JasonBennett41 Feb 2, 2026
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
15 changes: 13 additions & 2 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ GEM
bindex (0.8.1)
bootsnap (1.21.1)
msgpack (~> 1.2)
brakeman (7.1.2)
brakeman (8.0.1)
racc
builder (3.3.0)
bundler-audit (0.9.3)
Expand Down Expand Up @@ -214,6 +214,7 @@ GEM
faraday-net_http (3.4.2)
net-http (~> 0.5)
ffi (1.17.3-arm64-darwin)
ffi (1.17.3-x86_64-darwin)
ffi (1.17.3-x86_64-linux-gnu)
ffi (1.17.3-x86_64-linux-musl)
file_validators (3.0.0)
Expand Down Expand Up @@ -304,6 +305,8 @@ GEM
ffi (~> 1.0)
libddwaf (1.30.0.0.0-arm64-darwin)
ffi (~> 1.0)
libddwaf (1.30.0.0.0-x86_64-darwin)
ffi (~> 1.0)
libddwaf (1.30.0.0.0-x86_64-linux)
ffi (~> 1.0)
lint_roller (1.1.0)
Expand Down Expand Up @@ -359,6 +362,8 @@ GEM
nio4r (2.7.5)
nokogiri (1.19.0-arm64-darwin)
racc (~> 1.4)
nokogiri (1.19.0-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.19.0-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.19.0-x86_64-linux-musl)
Expand Down Expand Up @@ -391,6 +396,7 @@ GEM
ast (~> 2.4.1)
racc
pg (1.6.3-arm64-darwin)
pg (1.6.3-x86_64-darwin)
pg (1.6.3-x86_64-linux)
pg (1.6.3-x86_64-linux-musl)
pp (0.6.3)
Expand Down Expand Up @@ -644,6 +650,7 @@ GEM

PLATFORMS
arm64-darwin
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl

Expand Down Expand Up @@ -737,7 +744,7 @@ CHECKSUMS
bindata (2.5.0) sha256=29dccb8ba1cc9de148f24bb88930840c62db56715f0f80eccadd624d9f3d2623
bindex (0.8.1) sha256=7b1ecc9dc539ed8bccfc8cb4d2732046227b09d6f37582ff12e50a5047ceb17e
bootsnap (1.21.1) sha256=9373acfe732da35846623c337d3481af8ce77c7b3a927fb50e9aa92b46dbc4c4
brakeman (7.1.2) sha256=6b04927710a2e7d13a72248b5d404c633188e02417f28f3d853e4b6370d26dce
brakeman (8.0.1) sha256=c68ce0ac35a6295027c4eab8b4ac597d2a0bfc82f0d62dcd334bbf944d352f70
builder (3.3.0) sha256=497918d2f9dca528fdca4b88d84e4ef4387256d984b8154e9d5d3fe5a9c8835f
bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
byebug (13.0.0) sha256=d2263efe751941ca520fa29744b71972d39cbc41839496706f5d9b22e92ae05d
Expand Down Expand Up @@ -771,6 +778,7 @@ CHECKSUMS
faraday-follow_redirects (0.3.0) sha256=d92d975635e2c7fe525dd494fcd4b9bb7f0a4a0ec0d5f4c15c729530fdb807f9
faraday-net_http (3.4.2) sha256=f147758260d3526939bf57ecf911682f94926a3666502e24c69992765875906c
ffi (1.17.3-arm64-darwin) sha256=0c690555d4cee17a7f07c04d59df39b2fba74ec440b19da1f685c6579bb0717f
ffi (1.17.3-x86_64-darwin) sha256=1f211811eb5cfaa25998322cdd92ab104bfbd26d1c4c08471599c511f2c00bb5
ffi (1.17.3-x86_64-linux-gnu) sha256=3746b01f677aae7b16dc1acb7cb3cc17b3e35bdae7676a3f568153fb0e2c887f
ffi (1.17.3-x86_64-linux-musl) sha256=086b221c3a68320b7564066f46fed23449a44f7a1935f1fe5a245bd89d9aea56
file_validators (3.0.0) sha256=43700f0c1fe9b235bf7f4701375e1d2f9391352ab26723f123b934724db8067b
Expand Down Expand Up @@ -805,6 +813,7 @@ CHECKSUMS
libdatadog (25.0.0.1.0-x86_64-linux) sha256=260d78ab20bf5b5db310aade93900a02adf18434a5c0b28fec4d05db54f7c206
libddwaf (1.30.0.0.0) sha256=d5c350555ec5bdfb99534b37ad578163c83642ca03cecb3bae30fd29dc47d4fc
libddwaf (1.30.0.0.0-arm64-darwin) sha256=b4997ab7e8a4c41fa3993b00d89c209dfbed4d5ac5c03fc1dd70464185ffe193
libddwaf (1.30.0.0.0-x86_64-darwin) sha256=749989cf5b3ad3689969019706875260ffff5a91ebafcbf44766a3d635a0ac90
libddwaf (1.30.0.0.0-x86_64-linux) sha256=c8d7e1e097ed12bcdf5d4bcb716edcb98e72733c80dd884e0ee83a7cc43d6ae0
lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
listen (3.9.0) sha256=db9e4424e0e5834480385197c139cb6b0ae0ef28cc13310cfd1ca78377d59c67
Expand Down Expand Up @@ -834,6 +843,7 @@ CHECKSUMS
netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
nio4r (2.7.5) sha256=6c90168e48fb5f8e768419c93abb94ba2b892a1d0602cb06eef16d8b7df1dca1
nokogiri (1.19.0-arm64-darwin) sha256=0811dfd936d5f6dd3f6d32ef790568bf29b2b7bead9ba68866847b33c9cf5810
nokogiri (1.19.0-x86_64-darwin) sha256=1dad56220b603a8edb9750cd95798bffa2b8dd9dd9aa47f664009ee5b43e3067
nokogiri (1.19.0-x86_64-linux-gnu) sha256=f482b95c713d60031d48c44ce14562f8d2ce31e3a9e8dd0ccb131e9e5a68b58c
nokogiri (1.19.0-x86_64-linux-musl) sha256=1c4ca6b381622420073ce6043443af1d321e8ed93cc18b08e2666e5bd02ffae4
notiffany (0.1.3) sha256=d37669605b7f8dcb04e004e6373e2a780b98c776f8eb503ac9578557d7808738
Expand All @@ -846,6 +856,7 @@ CHECKSUMS
parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130
parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688
pg (1.6.3-arm64-darwin) sha256=7240330b572e6355d7c75a7de535edb5dfcbd6295d9c7777df4d9dddfb8c0e5f
pg (1.6.3-x86_64-darwin) sha256=ee2e04a17c0627225054ffeb43e31a95be9d7e93abda2737ea3ce4a62f2729d6
pg (1.6.3-x86_64-linux) sha256=5d9e188c8f7a0295d162b7b88a768d8452a899977d44f3274d1946d67920ae8d
pg (1.6.3-x86_64-linux-musl) sha256=9c9c90d98c72f78eb04c0f55e9618fe55d1512128e411035fe229ff427864009
pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
Expand Down
86 changes: 86 additions & 0 deletions app/controllers/content_status_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
class ContentStatusController < ApplicationController
def index
metrics = {
tools: {
default: Language.joins(resource_default_orders: {resource: :resource_type}).where(
resource_types: {name: "tract"}
).distinct("languages.id").count,
featured: Language.joins(resource_scores: {resource: :resource_type}).where(
resource_types: {name: "tract"},
resource_scores: {featured: true}
).distinct("languages.id").count,
ranked: Language.joins(resource_scores: {resource: :resource_type}).where(
resource_types: {name: "tract"}
).where.not(resource_scores: {score: nil}).distinct("languages.id").count,
total: Language.joins(resource_scores: {resource: :resource_type}).where(resource_types: {name: "tract"}).distinct("languages.id").count
},
lessons: {
default: Language.joins(resource_default_orders: {resource: :resource_type}).where(
resource_types: {name: "lesson"}
).distinct("languages.id").count,
featured: Language.joins(resource_scores: {resource: :resource_type}).where(
resource_types: {name: "lesson"},
resource_scores: {featured: true}
).distinct("languages.id").count,
ranked: Language.joins(resource_scores: {resource: :resource_type}).where(
resource_types: {name: "lesson"}
).where.not(resource_scores: {score: nil}).distinct("languages.id").count,
total: Language.joins(resource_scores: {resource: :resource_type}).where(resource_types: {name: "lesson"}).distinct("languages.id").count
},
countries: retrieve_countries_data
}

render json: metrics, status: :ok
rescue => e
render json: {errors: [{detail: "Error: #{e.message}"}]}, status: :unprocessable_content
end

private

def uniq_countries
ResourceScore.select(:country).distinct.pluck(:country)
end

def retrieve_lessons_data(country, language)
{
featured: Resource.joins(:resource_type, resource_scores: :language).where(
resource_types: {name: "lesson"}, resource_scores: {featured: true, country: country}
).where(resource_scores: {language: language}).count,
ranked: Resource.joins(:resource_type, resource_scores: :language).where(
resource_types: {name: "lesson"}, resource_scores: {country: country}
).where(resource_scores: {language: language}).where.not(resource_scores: {score: nil}).count
}
end

def retrieve_tools_data(country, language)
{
featured: Resource.joins(:resource_type, resource_scores: :language).where(
resource_types: {name: "tract"}, resource_scores: {featured: true, country: country}
).where(resource_scores: {language: language}).count,
ranked: Resource.joins(:resource_type, resource_scores: :language).where(
resource_types: {name: "tract"}, resource_scores: {country: country}
).where(resource_scores: {language: language}).where.not(resource_scores: {score: nil}).count
}
end

def retrieve_language_data(country, language)
{
language_code: language.code.downcase,
language_name: language.name,
lessons: retrieve_lessons_data(country, language),
tools: retrieve_tools_data(country, language),
last_updated: ResourceScore.where(country: country, language: language).maximum(:updated_at)&.strftime("%d-%m-%y") || "N/A"
}
end

def retrieve_countries_data
uniq_countries.map do |country|
{
country_code: country,
languages: Language.joins(:resource_scores).where(resource_scores: {country: country}).distinct.map do |language|
retrieve_language_data(country, language)
end
}
end
end
end
158 changes: 158 additions & 0 deletions app/controllers/resource_default_orders_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# frozen_string_literal: true

class ResourceDefaultOrdersController < ApplicationController
before_action :authorize!, only: %i[create destroy update mass_update]

def index
lang = params.dig(:filter, :lang) || params[:lang]
resource_type = params.dig(:filter, :resource_type) || params[:resource_type]

if lang.present?
language = Language.where("code = :lang OR LOWER(code) = LOWER(:lang)", lang: lang).first
raise "Language not found for code: #{lang}" unless language.present?
end

default_order_resources = all_default_order_resources(lang: lang, resource_type: resource_type)

render json: default_order_resources, include: params[:include], status: :ok
rescue => e
render json: {errors: [{detail: "Error: #{e.message}"}]}, status: :unprocessable_content
end

def create
sanitized_params = create_params
if create_params[:lang].present?
language = Language.where("code = :lang OR LOWER(code) = LOWER(:lang)",
lang: create_params[:lang]).first
end
sanitized_params.delete(:lang) if sanitized_params[:lang].present?
@resource_default_order = ResourceDefaultOrder.new(sanitized_params)
@resource_default_order.language = language if language.present?
@resource_default_order.save!
render json: @resource_default_order, status: :created
rescue => e
render json: {errors: formatted_errors("record_invalid", e)}, status: :unprocessable_content
end

def destroy
@resource_default_order = ResourceDefaultOrder.find(params[:id])
@resource_default_order.destroy!
render json: {}, status: :ok
rescue
render json: {errors: [{source: {pointer: "/data/attributes/id"}, detail: e.message}]},
status: :unprocessable_content
end

def update
@resource_default_order = ResourceDefaultOrder.find(params[:id])
sanitized_params = create_params
if create_params[:lang].present?
language = Language.where("code = :lang OR LOWER(code) = LOWER(:lang)",
lang: create_params[:lang]).first
end
sanitized_params.delete(:lang) if sanitized_params[:lang].present?
@resource_default_order.language = language if language.present?
@resource_default_order.update!(sanitized_params)
render json: @resource_default_order, status: :ok
rescue => e
render json: {errors: formatted_errors("record_invalid", e)}, status: :unprocessable_content
end

def mass_update
lang_code = params.dig(:data, :attributes, :lang)&.downcase
resource_type_name = params.dig(:data, :attributes, :resource_type)&.downcase
incoming_resource_ids = params.dig(:data, :attributes, :resource_ids) || []

raise "Language and Resource Type should be provided" unless lang_code.present? && resource_type_name.present?

language = Language.find_by("code = :lang OR LOWER(code) = LOWER(:lang)", lang: lang_code)
raise "Language not found for code: #{lang_code}" unless language.present?

resource_type = ResourceType.find_by(name: resource_type_name)
raise "ResourceType '#{resource_type_name}' not found" unless resource_type.present?

unless %w[lesson tract].include?(resource_type.name.downcase)
raise "ResourceType '#{resource_type_name}' is not supported"
end

unless incoming_resource_ids.is_a?(Array) && incoming_resource_ids.all?(Integer)
raise "resource_ids is expected to be an array of integers"
end

raise "resource_ids is expected to include a maximum of 9 ids" if incoming_resource_ids.length > 9

if incoming_resource_ids.uniq.length != incoming_resource_ids.length
raise "resource_ids cannot contain duplicate ids"
end

valid_resource_ids = Resource.where(id: incoming_resource_ids, resource_type_id: resource_type.id).pluck(:id)
invalid_resource_ids = incoming_resource_ids - valid_resource_ids
if invalid_resource_ids.any?
raise "Resources not found or do not match the provided resource type. Invalid IDs: #{invalid_resource_ids.join(", ")})"
end

current_default_orders = ResourceDefaultOrder
.joins(:resource)
.where(language_id: language.id)
.where(resources: {resource_type_id: resource_type.id})
.order(position: :asc)
.lock
.to_a

ResourceDefaultOrder.transaction do
current_default_orders.each do |ro|
ro.update_column(:position, nil)
end

orders_by_resource_id = current_default_orders.index_by(&:resource_id)
incoming_resource_ids.each_with_index do |resource_id, index|
relevant_order = orders_by_resource_id[resource_id]

if relevant_order
relevant_order.update!(position: index + 1)
else
ResourceDefaultOrder.create!(
resource_id: resource_id,
language_id: language.id,
position: index + 1
)
end
end
current_default_orders.each do |ro|
ro.destroy! unless incoming_resource_ids.include?(ro.resource_id)
end
end
resulting_default_orders = ResourceDefaultOrder
.joins(:resource)
.where(language_id: language.id)
.where(resources: {resource_type_id: resource_type.id})
.order(position: :asc)

render json: resulting_default_orders, status: :ok
rescue => e
render json: {errors: [{detail: "Error: #{e.message}"}]}, status: :unprocessable_content
end

private

def all_default_order_resources(lang:, resource_type: nil)
scope = Resource.joins(:resource_default_orders)

if lang.present?
language = Language.where("code = :lang OR LOWER(code) = LOWER(:lang)", lang: lang).first
scope = scope.joins(resource_default_orders: :language).where(languages: {id: language.id})
end

if resource_type.present?
scope = scope.joins(:resource_type).where(resource_types: {name: resource_type.downcase})
end

scope.order("resource_default_orders.position ASC NULLS LAST, resources.created_at DESC")
end

def create_params
params.require(:data).require(:attributes).permit(
:resource_id, :lang, :position
)
end
end
Loading