im new to Ruby on Rails. I try to connect 3 tables.
My Models:
Course.rb:
class Course < ApplicationRecord
has_many :chapters
has_many :lessons, through: :chapters
end
Chapter.rb
class Chapter < ApplicationRecord
belongs_to :course
belongs_to :lesson
end
Lesson.rb
class Lesson < ApplicationRecord
has_many :chapters
has_many :courses, through: :chapters
end
My Course Controller looks like this:
class CourseController < ApplicationController
def index
end
def show
#course = Course.find(params[:id])
#chapters = #course.chapters
#lessons = #course.lessons
end
end
and show.html.erb
<% #chapters.each do |c| %>
<%= c.chapter %>
<%= link_to "lektionen", c %>
<% end %>
I can see a list of my courses. Thats working. Also i can see the chapters. But for any reason it is not forwarding the id for making a relationship between these models.
Chapter Controller:
class ChapterController < ApplicationController
def index
end
def show
#chapters = Chapter.find(params[:id])
#lessons = #chapters.lessons
end
end
i want to forward the id of the course so that chapters << lessons are the children of courses.
-courses
-- chapters
--- lessons
My schema:
ActiveRecord::Schema.define(version: 2021_04_11_090326) do
create_table "chapters", force: :cascade do |t|
t.string "chapter"
t.integer "course_id", null: false
t.integer "lesson_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["course_id"], name: "index_chapters_on_course_id"
t.index ["lesson_id"], name: "index_chapters_on_lesson_id"
end
create_table "courses", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "lessons", force: :cascade do |t|
t.string "title"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
add_foreign_key "chapters", "courses"
add_foreign_key "chapters", "lessons"
end
My routes.rb
Rails.application.routes.draw do
get 'chapter/index'
get 'chapter/show'
get 'course/index'
get 'course/show'
# For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html
resources :course
resources :chapter
resources :lessons
end
if i click "courses#show" action i could see the chapters. If i click on one chapter i got this error:
undefined method `lessons' for #Chapter:0x000055f7aeefbe50
I want to show only lessons which are belongs to a specific chapter and the chapter is related to the course.
But im not in able to get this working.
I hope yo can help me.
I think you should rethink you data model in this case:
Make it Course -> Chapter -> Lesson
class Course < ApplicationRecord
has_many :chapters # a course has many chapters. alright
end
class Chapter < ApplicationRecord
belongs_to :course # a chapter always belongs to a course
has_many :lessons # and has many lessons ! check!
end
class Lesson < ApplicationRecord
belongs_to :chapter # a lesson belongs to a chapter
# helper method if you want to access course directly from lesson
def course
#course ||= chapter&.course
end
end
Database
Your database should look something like this:
suggestion -> do not name it chapter in chapters table. call it title or name
ActiveRecord::Schema.define(version: 2021_04_11_090326) do
create_table "courses", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "chapters", force: :cascade do |t|
t.string "title"
t.integer "course_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["course_id"], name: "index_chapters_on_course_id"
end
create_table "lessons", force: :cascade do |t|
t.string "title"
t.integer "chapter_id", null: false
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["chapter_id"], name: "index_lessons_on_chapter_id"
end
end
With this setup you are ready to go with a good hierarchy.
Draw the routes:
Rails.application.routes.draw do
resources :courses
resources :chapters
resources :lessons
end
if you want to make the hierarchy in the url you could do something more advanced
Rails.application.routes.draw do
# use this if you know what you are doing :D
resources :courses do
resources :chapters do
resources :lessons
end
end
end
Your controllers:
class CourseController < ApplicationController
def index
courses
end
def show
course
end
protected
# get all courses
def courses
#courses ||= Course.order(:name)
end
# find course by id
def course
#course ||= courses.find(params[:id])
end
end
In your view views/courses/show.html.erb
<h1><%= #course.name %></h1>
<% #course.chapters.each do |chapter| %>
<h2><%= chapter.title %></h2>
<% chapter.lessons.each do |lesson| %>
<%= link_to "Lesson: " + lesson.title, lesson_url(lesson) %>
<% end %>
<% end %>
N+1 problem:
To avoid N+1 problem when rendering this hierarchy change your controller and add includes to the query:
def course
#course ||= courses.includes(chapters: :lessons).find(params[:id])
end
I hope this gives you a good start with a clean data model.
Related
I'm a beginner in rails and am making a card app. I have a user, card, and user_card models with a many to many, through relationship set up between the cards and users. My problem is that when I return the card table and try to include: the users I get an empty array. I've tried resetting the database but still nothing.
ActiveRecord::Schema.define(version: 2022_06_15_200100) do
create_table "cards", force: :cascade do |t|
t.string "name"
t.string "image"
t.string "text"
t.integer "level"
t.string "types"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.integer "quantity", default: 0
end
create_table "user_cards", force: :cascade do |t|
t.integer "user_id"
t.integer "card_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["card_id"], name: "index_user_cards_on_card_id"
t.index ["user_id"], name: "index_user_cards_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "username"
t.string "password_digest"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
class User < ApplicationRecord
has_secure_password
has_many :user_cards
has_many :cards, through: :user_cards
end
class Card < ApplicationRecord
has_many :user_cards
has_many :users, through: :user_cards
end
class UserCard < ApplicationRecord
belongs_to :user
belongs_to :card
end
controller
class CardsController < ApplicationController
wrap_parameters false
def create
card = Card.create(card_params)
if card.valid?
render json: card, status: :created, include: :users
else
render json:{errors: card.errors}
end
def index
card = Card.all
render json: card, include: :users
end
In order for the User.first.cards to work, you need to ensure the application is inserting data in the user_cards table.
You may check if there are any records in there by doing UserCard.all in your rails console.
Coming to the controller, after creating a card record, you have to assign it to a user record in order for the relationship to be established.
def create
card = Card.create(card_params)
if card.valid?
card.users << current_user # Or card.users << (any user object like) User.first
render json: card, status: :created, include: :users
else
render json:{errors: card.errors}
end
end
card.users << user object will create the necessary record in the user_cards table and you'll be able to access them using includes: :user
You may refer the examples given - here in the Rails API guide
My goal is for users to add individual games pulled from an API gem (https://github.com/games-directory/api-giantbomb) to their personal library. I want users to be able to browse other people's libraries. I have the games showing up via search along with a show page for each game.
I am running into two problems: can't add games to a user's library and can't view other people's library.
Here is my games controller:
class GamesController < ApplicationController
#search for games
def index
#games = GiantBomb::Search.new().query(params[:query]).resources('game').limit(100).fetch
end
#Shows data for individual games
def show
#game = GiantBomb::Game.detail(params[:id])
end
#Adding and removing games to a user's library
def library
type = params[:type]
#game = GiantBomb::Game
if type == "add"
current_user.library_additions << #game
redirect_to user_library_path, notice: "Game was added to your library"
elsif type == "remove"
current_user.library_additions.delete(#game)
redirect_to root_path, notice: "Game was removed from your library"
else
# Type missing, nothing happens
redirect_to game_path(#game), notice: "Looks like nothing happened. Try once more!"
end
end
private
def game_params
params.require(:game).permit(:name, :search, :query)
end
end
When I try to add a game to my library, I get "Game(#70231217467720) expected, got GiantBomb::Game which is an instance of Class(#70231150447440)". So my #game is incorrect but I am not sure what should be there instead.
Even if I could add the game to my library, I can't view other user's libraries. Here is my current controller.
class LibraryController < ApplicationController
#before_action :authenticate_user!
def index
#library_games = User.library_additions
end
end
I get 'undefined method library_additions' even though it is in the model. If I change User to current_user I can see the page, but that means users can only see their page and not others.
Here are my game, user, and library model:
class Game < ApplicationRecord
has_many :libraries
has_many :added_games, through: :libraries, source: :user
end
class User < ApplicationRecord
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
has_many :games
has_many :libraries
has_many :library_additions, through: :libraries, source: :game
end
class Library < ApplicationRecord
belongs_to :game
belongs_to :user
end
I made my library a join table for users and games but I am thinking I didn't do it correctly. Here is my schema:
ActiveRecord::Schema.define(version: 2020_11_19_143536) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "games", force: :cascade do |t|
t.string "name"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.string "search"
end
create_table "libraries", force: :cascade do |t|
t.integer "user_id"
t.integer "game_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "users", force: :cascade do |t|
t.string "email", default: "", null: false
t.string "encrypted_password", default: "", null: false
t.string "reset_password_token"
t.datetime "reset_password_sent_at"
t.datetime "remember_created_at"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["email"], name: "index_users_on_email", unique: true
t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true
end
end
Am I missing a migration or do I need the rework the models and controllers?
[edit] Here are my routes, I am getting a pathing error when I try to add a game.
Rails.application.routes.draw do
devise_for :users
resources :games do
member do
put "add", to: "games#library"
put "remove", to: "games#library"
end
end
resources :library, only:[:index]
root to: 'pages#home'
get '/search', to: 'games#search', as: :search
get '/games', to: 'games#index', as: :index
get '/user/:id', to: 'user#show'
get '/user/:id/library', to: 'library#index', as: :user_library
end
Here, the error clearly states it is expecting an instance of Game not GiantBomb::Game, so you have to create one.
#game = Game.new(name: 'some name', other fields ....)
if type == "add"
current_user.library_additions << #game
About the other error you can only call association methods on an instance not on the class itself
def index
# You could get the user'id through params for example
#library_games = User.find(params[:user_id]).library_additions
end
I am just working on a simple rails project in which the models have these many relationships between them :
A author can have many posts
A post can have many comments
likes and dislikes belongs to each post
Now, I have rendered the authors data ( in json ) and the output which I am getting is this :
As we can say that it is rendering only author and post data ( neither comments nor likes/dislikes ).
I am very new to RubyOnRails. So, Whatever I have tried so far is this below :
Controller :
class AuthorsController < ApplicationController
def show
#auth = Author.find_by(id: params[:id])
render json: #auth
end
end
Models :
class Author < ApplicationRecord
has_many :posts
end
class Comment < ApplicationRecord
belongs_to: post
end
class Dislike < ApplicationRecord
belongs_to: post
end
class Like < ApplicationRecord
belongs_to: post
end
class Post < ApplicationRecord
has_many :comments
end
Serializers :
class AuthorSerializer < ActiveModel::Serializer
attributes :id, :name, :age
has_many :posts
end
class CommentSerializer < ActiveModel::Serializer
attributes :id, :content, :username
end
class DislikeSerializer < ActiveModel::Serializer
attributes :id, :dislikecount
end
class LikeSerializer < ActiveModel::Serializer
attributes :id, :likecount
end
class PostSerializer < ActiveModel::Serializer
attributes :name, :content
has_many :comments, serializer: CommentSerializer
end
schema.rb :
ActiveRecord::Schema.define(version: 2020_03_25_091544) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
create_table "authors", force: :cascade do |t|
t.string "name"
t.integer "age"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
create_table "comments", force: :cascade do |t|
t.string "content"
t.string "username"
t.bigint "post_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["post_id"], name: "index_comments_on_post_id"
end
create_table "dislikes", force: :cascade do |t|
t.integer "dislikecount"
t.bigint "post_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["post_id"], name: "index_dislikes_on_post_id"
end
create_table "likes", force: :cascade do |t|
t.integer "likecount"
t.bigint "post_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["post_id"], name: "index_likes_on_post_id"
end
create_table "posts", force: :cascade do |t|
t.string "name"
t.string "content"
t.bigint "author_id"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
t.index ["author_id"], name: "index_posts_on_author_id"
end
end
Now, I just want to render complete data of a author ( Means, The expected output must include author detail + post details + comments + likes + dislikes ) in json form.
I have searched a lot to overcome this issue, but could not resolve this issue.
You need to make 2 changes in your code -
class Post < ApplicationRecord
has_many :comments
has_many :likes
has_many :dislikes
belongs_to :author
end
class PostSerializer < ActiveModel::Serializer
attributes :name, :content
has_many :comments, serializer: CommentSerializer
has_many :likes
has_many :dislikes
end
I have two models, User and Course.
User has_many Courses.
Course has_one User.
Right now I'm grabbing all users and displaying them on courses index page, however, I've realised that I should only be displaying users that have courses. I'm unsure how to do this?
Here is my index method from courses controller:
#courses_controller.rb
def index
#courses = Course.all
#users = User.all
end
Here are my models:
# user.rb
class User < ApplicationRecord
# User has many courses
has_many :courses, dependent: :destroy
end
# course.rb
class Course < ApplicationRecord
has_one :user
validates :user_id, presence: true
end
And my schema:
ActiveRecord::Schema.define(version: 20170505114247) do
create_table "courses", force: :cascade do |t|
t.string "name"
t.string "prerequisite"
t.text "description"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "picture"
t.index ["user_id", "created_at"], name: "index_courses_on_user_id_and_created_at"
t.index ["user_id"], name: "index_courses_on_user_id"
end
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "password_digest"
t.index ["email"], name: "index_users_on_email", unique: true
end
end
Since there is a one-to-many association between users and courses then on the courses index page when you iterate over each course you can include the user information. You don't need to include #users = User.all in the controller. So your iterator might look something like this
In controller:
#courses = Course.includes(:user).all #to avoid n+1 query
Your view:
# app/views/courses/index.html.erb
<% #courses.each do |course| %>
<%= course.name %>
<%= course.description %>
<%= course.user.name %>
<% end %>
I'm still learning rails so any help you can provide would be super helpful. I've set a count for my likes on my book app. Thus, every time a user likes a book - the number increases by one or decreases if the unlike it. However, if no one has liked a book yet - a 0 appears. I'd like that to be blank so that only when a user has liked it will the number appear. I've listed all my relevant code below. Thank you so much.
Schema.rb
create_table "books", force: :cascade do |t|
t.string "title"
t.integer "user_id"
t.integer "book_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "avatar_file_name"
t.string "avatar_content_type"
t.integer "avatar_file_size"
t.datetime "avatar_updated_at"
t.integer "likes_count", default: 0, null: false
end
create_table "likes", force: :cascade do |t|
t.integer "user_id"
t.integer "book_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Book.rb
class Book < ApplicationRecord
has_many :likes, :counter_cache => true
has_many :users, through: :likes
belongs_to :user
end
Likes.rb
class Like < ApplicationRecord
belongs_to :book, :counter_cache => true
belongs_to :user
end
Likes Count Migration
class AddLikecountsToBook < ActiveRecord::Migration[5.0]
def change
add_column :books, :likes_count, :integer, :null => false, :default => 0
end
end
With associations in rails you get several interogation methods such as .any? and .none? which can be used to create conditional expressions.
<% if book.likes.any? %>
<%= number_to_human(book.likes.size) %>
<% end %>
# or
<%= number_to_human(book.likes.size) unless book.likes.none? %>
This uses the counter cache as well to avoid n+1 queries.
If you do not want your view to display 0 you could add a if statement in your view.
<% if #votes == 0 %>
be the first to rate this book
<% else %>
<%= #votes %>
<% end %>
Or when returning the variable to the view from the controller
def
if #votes == 0
#votes = ''
end
end