Skip to content
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
33 changes: 33 additions & 0 deletions app/controllers/v2_errors_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class V2ErrorsController < ApplicationController
before_action :authenticate_user!

def summary
# Preguntas fallidas del usuario
failed_answers = Current.user.user_answers.incorrect.includes(question: [ :answers, { clinical_case: :category } ]).recent

# Agrupar por especialidad (categoría) para el resumen
specialties_count = failed_answers.each_with_object(Hash.new(0)) do |ua, hash|
category = ua.question.effective_category
hash[category] += 1 if category
end

most_failed_specialties = specialties_count.map do |category, count|
{ id: "esp#{category.id}", name: category.name, count: count }
end.sort_by { |s| -s[:count] }.take(5)

recent_failed_questions = failed_answers.take(10).map do |ua|
{
id: "q#{ua.question.id}",
question: ua.question.text,
correctAnswer: ua.question.answers.find(&:is_correct?)&.text,
userAnswer: ua.answer.text,
explanation: ua.answer.description # O la del correct answer si estuviera ahí
}
end

render json: {
mostFailedSpecialties: most_failed_specialties,
recentFailedQuestions: recent_failed_questions
}
end
end
33 changes: 33 additions & 0 deletions app/controllers/v2_flashcards_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
class V2FlashcardsController < ApplicationController
before_action :authenticate_user!

def review
# Obtener flashcards pendientes de repaso para el usuario actual
user_flashcards = Current.user.user_flashcards.due.limit(20)

# Si no hay pendientes, podríamos sugerir algunas nuevas o simplemente enviar vacío
# El requisito dice que el backend decide según SRS

render json: {
flashcards: user_flashcards.map { |uf|
{
id: uf.flashcard.id,
front: uf.flashcard.front,
back: uf.flashcard.back,
category: uf.flashcard.category&.name || "General"
}
}
}
end

def answer
user_flashcard = Current.user.user_flashcards.find_by(flashcard_id: params[:id])

if user_flashcard
user_flashcard.review(params[:quality].to_i)
render json: { message: "Respuesta registrada", next_review: user_flashcard.next_review }
else
render json: { error: "Flashcard no encontrada para este usuario" }, status: :not_found
end
end
end
35 changes: 35 additions & 0 deletions app/controllers/v2_images_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
class V2ImagesController < ApplicationController
before_action :authenticate_user!

def bank
# El banco de imágenes se basa en casos clínicos que tienen imágenes adjuntas
cases = ClinicalCase.with_attached_image.published

if params[:category].present?
cases = cases.joins(:category).where(categories: { name: params[:category] })
end

if params[:search].present?
search = "%#{params[:search]}%"
cases = cases.where("clinical_cases.name LIKE :search OR clinical_cases.tags LIKE :search", search: search)
end

cases = cases.paginate(page: params[:page] || 1, per_page: 20)

render json: {
images: cases.map { |c|
{
id: "img#{c.id}",
url: (rails_blob_url(c.image, only_path: false) rescue nil),
title: c.name,
category: c.category.name,
tags: c.tag_list
}
},
pagination: {
current: cases.current_page,
total: cases.total_pages
}
}
end
end
26 changes: 26 additions & 0 deletions app/controllers/v2_knowledge_base_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
class V2KnowledgeBaseController < ApplicationController
before_action :authenticate_user!

def index
topics = Topic.includes(:articles)

if params[:topic].present?
topics = topics.where("title LIKE ?", "%#{params[:topic]}%")
end

if params[:search].present?
search = "%#{params[:search]}%"
topics = topics.joins(:articles).where("articles.title LIKE :search OR articles.content LIKE :search", search: search).distinct
end

render json: {
topics: topics.map { |t|
{
id: "t#{t.id}",
title: t.title,
articles: t.articles.map { |a| { id: "a#{a.id}", title: a.title } }
}
}
}
end
end
31 changes: 31 additions & 0 deletions app/controllers/v2_leaderboard_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
class V2LeaderboardController < ApplicationController
before_action :authenticate_user!

def national
# En un entorno real, esto podría estar cacheado o usar una tabla de rankings
# Para esta implementación, calculamos on-the-fly ordenando por total_points

users_with_points = User.all.sort_by(&:total_points).reverse

top_players = users_with_points.take(100).each_with_index.map do |user, index|
{
rank: index + 1,
nickname: user.username || user.name || "DoctorX",
points: user.total_points,
avatar: "https://via.placeholder.com/150"
}
end

current_user_rank = users_with_points.index(Current.user) + 1 rescue nil

render json: {
currentUser: {
rank: current_user_rank,
points: Current.user.total_points,
avatar: "https://via.placeholder.com/150",
nickname: Current.user.username || Current.user.name || "DoctorX"
},
topPlayers: top_players
}
end
end
4 changes: 4 additions & 0 deletions app/models/article.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Article < ApplicationRecord
belongs_to :topic
validates :title, presence: true
end
4 changes: 4 additions & 0 deletions app/models/clinical_case.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class ClinicalCase < ApplicationRecord
validates :name, presence: true
validate :image_must_be_valid

def tag_list
tags&.split(",")&.map(&:strip) || []
end

private

def image_must_be_valid
Expand Down
4 changes: 4 additions & 0 deletions app/models/topic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
class Topic < ApplicationRecord
has_many :articles, dependent: :destroy
validates :title, presence: true
end
4 changes: 3 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ def calculate_accuracy
end

def total_points
achievements.sum(:points) || 0
achievement_points = achievements.sum(:points) || 0
correct_answer_points = user_answers.correct.count
achievement_points + correct_answer_points
end

def answered?(question)
Expand Down
8 changes: 8 additions & 0 deletions config/routes.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
Rails.application.routes.draw do
# V2 Endpoints
get "v2/leaderboard/national", to: "v2_leaderboard#national"
get "v2/images/bank", to: "v2_images#bank"
get "v2/flashcards/review", to: "v2_flashcards#review"
post "v2/flashcards/review/:id/answer", to: "v2_flashcards#answer"
get "v2/knowledge-base", to: "v2_knowledge_base#index"
get "v2/errors/summary", to: "v2_errors#summary"

# Nuevas rutas unificadas
resources :users do
collection do
Expand Down
5 changes: 5 additions & 0 deletions db/migrate/20260310120343_add_tags_to_clinical_cases.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
class AddTagsToClinicalCases < ActiveRecord::Migration[8.1]
def change
add_column :clinical_cases, :tags, :string
end
end
15 changes: 15 additions & 0 deletions db/migrate/20260310120345_create_topics_and_articles.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
class CreateTopicsAndArticles < ActiveRecord::Migration[8.1]
def change
create_table :topics do |t|
t.string :title
t.timestamps
end

create_table :articles do |t|
t.string :title
t.text :content
t.references :topic, null: false, foreign_key: true
t.timestamps
end
end
end
19 changes: 18 additions & 1 deletion db/schema.rb

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

48 changes: 48 additions & 0 deletions test/controllers/v2_endpoints_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
require "test_helper"

class V2EndpointsTest < ActionDispatch::IntegrationTest
setup do
@user = User.create!(email: "test@example.com", password: "password123", username: "testuser")
@category = Category.create!(name: "Test Category")
@token = JsonWebToken.encode(user_id: @user.id)
@headers = { "Authorization" => "Bearer #{@token}" }
end

test "should get national leaderboard" do
get "/v2/leaderboard/national", headers: @headers
assert_response :success
json = JSON.parse(response.body)
assert_includes json.keys, "currentUser"
assert_includes json.keys, "topPlayers"
end

test "should get image bank" do
get "/v2/images/bank", headers: @headers
assert_response :success
json = JSON.parse(response.body)
assert_includes json.keys, "images"
assert_includes json.keys, "pagination"
end

test "should get flashcards for review" do
get "/v2/flashcards/review", headers: @headers
assert_response :success
json = JSON.parse(response.body)
assert_includes json.keys, "flashcards"
end

test "should get knowledge base" do
get "/v2/knowledge-base", headers: @headers
assert_response :success
json = JSON.parse(response.body)
assert_includes json.keys, "topics"
end

test "should get errors summary" do
get "/v2/errors/summary", headers: @headers
assert_response :success
json = JSON.parse(response.body)
assert_includes json.keys, "mostFailedSpecialties"
assert_includes json.keys, "recentFailedQuestions"
end
end