Counter Cache customizing for zeros and large numbers - ruby-on-rails

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

Related

Multiple Checkbox in Ruby

I'm trying to create an "ingredient" checkbox list derived from my "recipes", I'd like for the values to be saved in the database so that when it's checked and I refresh the page, it still shows as checked.
The error says "uninitialized constant #Class:0x00007f8f2d360830::Parties"
Here's an example of what i am trying to do
Controller:
# parties_controller.rb
def ingredients
#party = Party.find(params[:party_id])
#party_recipe = #party.recipes
#party_recipe.each do |recipe|
#ingredients = recipe.ingredients
end
The models:
Party model
#party.rb
class Party < ApplicationRecord
has_many :party_recipes
has_many :recipes, through: :party_recipes
end
Recipe model
#recipe_ingredient.rb
class RecipeIngredient < ApplicationRecord
belongs_to :recipe
belongs_to :ingredient
end
Ingredient model
#ingredient.rb
class Ingredient < ApplicationRecord
has_many :recipe_ingredients
has_many :recipes, through: :recipe_ingredients
end
Form:
#ingredients.html.erb
<% form_for "/parties/#{#party.id}/ingredients" do |f| %>
<% Parties::Recipes::Ingredients.each do |ingredient| %>
<%= check_box_tag(ingredient) %>
<%= ingredient %>
<% end %>
<% end %>
Schema:
create_table "ingredients", force: :cascade do |t|
t.string "name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "parties", force: :cascade do |t|
t.string "title"
t.string "address"
t.bigint "user_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "theme"
t.date "date"
t.integer "attendancy"
t.integer "appetizers"
t.integer "mains"
t.integer "desserts"
t.string "status", default: "pending"
t.index ["user_id"], name: "index_parties_on_user_id"
end
create_table "party_recipes", force: :cascade do |t|
t.bigint "recipe_id", null: false
t.bigint "party_id", null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["party_id"], name: "index_party_recipes_on_party_id"
t.index ["recipe_id"], name: "index_party_recipes_on_recipe_id"
end
create_table "recipe_ingredients", force: :cascade do |t|
t.bigint "recipe_id", null: false
t.bigint "ingredient_id", null: false
t.string "amount"
t.boolean "included", default: false
t.index ["ingredient_id"], name: "index_recipe_ingredients_on_ingredient_id"
t.index ["recipe_id"], name: "index_recipe_ingredients_on_recipe_id"
end
create_table "recipes", force: :cascade do |t|
t.string "title"
t.text "description"
end
add_foreign_key "party_recipes", "parties"
add_foreign_key "party_recipes", "recipes"
add_foreign_key "recipe_ingredients", "ingredients"
add_foreign_key "recipe_ingredients", "recipes"
I'm not entirely sure where exactly needs to be corrected, any help appreciated, thank you so much!
Well the error message is correct, you don't have any model called Parties, in fact in Rails, models are always singular, camel-case. So that explains the error message.
However that won't fix your problem! The iterator in the view should be
<% #ingredients.each do |ingredient| %>
<%= check_box_tag(ingredient) %>
<%= ingredient %>
<% end %>
Because I think you are trying to populate an #ingredients variable in your controller. However it still won't work, b/c the value of the #ingredients variable is not being correctly assigned...
Personally I much prefer the "fat model skinny controller" design style for Rails. So I would have a PartiesController#ingredients method that looks like this:
# parties_controller.rb
def ingredients
#party = Party.find(params[:party_id])
#ingredients = #party.ingredients
end
then in your Party model:
# app/models/party.rb
def ingredients
recipes.map(&:ingredients).flatten
end
Why do it this way? Well you're just getting started with Rails, but eventually (soon hopefully) you'll be writing tests, and it's much much easier to write tests on models than controllers.
Now, there could well be some other issues in your code, but try my suggestions and see where that gets you.
#Les Nightingill's answer should work well for organizing your controller and model! Regarding when you click refresh and the value of the boxes are saved either;
Set up some listeners in javascript and send a request to your update controller method every time there is a value change for one of your check boxes.
Or add a save button at the bottom of your form that points to your update controller method to save the values of the checkboxes. Something like:
<%= submit_tag "Save", data: { disable_with: "Saving..." } %>
https://api.rubyonrails.org/v5.2.3/classes/ActionView/Helpers/FormTagHelper.html#method-i-submit_tag

Rails: Routing "Courses" -> "Chapters" -> "Lessons"

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.

How to implement counter with buttons to a post in a polymorphic associations?

I have an app where I can make post and create comments to them. And I need to implement 2 buttons (like and unlike) in my view to every post(tweet) and comment to this tweet. Also I have 2 routes to my likes: post and destroy: (all routes are here)
resource :user_profiles, only: %i[edit update]
resources :tweets
resources :comments, only: %i[create destroy]
resources :likes, only: %i[create destroy]
root to: 'home#index'
But also I have polymorphic associations:
class Like < ApplicationRecord
belongs_to :likable, polymorphic: true
belongs_to :user
end
class Tweet < ApplicationRecord
belongs_to :user
has_many :likes, as: :likable
has_many :comments, dependent: :destroy
end
class Comment < ApplicationRecord
has_many :likes, as: :likable
belongs_to :tweet
belongs_to :user
end
I wrote some code in my tweet.preview
<% if tweet.likes.find_by(user_id: current_user.id)%>
<%= button_to 'Unlike', like_path(likable_id: tweet.id, likable_type: tweet.class.name, user_id: current_user.id), method: :delete %>
<% else %>
<%= button_to 'Like', likes_path(likable_id: tweet.id, likable_type: tweet.class.name, user_id: current_user.id), method: :post%>
<% end %>
But I don't know how to implement like controller to count likes and display 2 buttons with counter to view.
My db.schema is here:
create_table "comments", force: :cascade do |t|
t.string "content"
t.integer "user_id"
t.integer "tweet_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "likes", force: :cascade do |t|
t.integer "user_id"
t.string "likable_type"
t.integer "likable_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "tweets", force: :cascade do |t|
t.string "content"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_tweets_on_user_id"
end
create_table "user_profiles", force: :cascade do |t|
t.string "username"
t.string "first_name"
t.string "last_name"
t.date "birthday"
t.string "bio"
t.integer "user_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["user_id"], name: "index_user_profiles_on_user_id"
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", null: false
t.datetime "updated_at", 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
Thanks in advance!
Well you just need to pass the parent of the future like record (whereas it is a post or a comment) to the create action of the like controller.
You need to add a link to your buttons such as : like_path(parent_type: "comment", parent_id: #comment_id), method: "post"
(here #comment_id may not be an instance variable, especially if you loop through all comment of a specific tweet, it may instead look like comment.id if in your loop you did something like #tweet.comments.each do |comment|)
Then in your like controller, you can just assign them to the record.
I assume here that the user id "is already known" and you create the like from the user record.
#like = current_user.likes.new(likable_id: params[:parent_id], likable_type: params[:parent_type])
#like.save
So most of the job is made in your view by creating the correct link for your different buttons...
So i'm not sure about your setup. Are you using a gem?
So the secret to do this is to create two seperated paths:
resources :likes do
member do
get :like
get :unlike
end
end
inside your controller, you can then execute the logic inside each method:
def like
//find the user
//follow the user
//redirect him
end
def unlike
//find the user
//unfollow the user
//redirect him
end
Also, you can then simply use your buttons for each like/unlike path:
<% if tweet.likes.find_by(user_id: current_user.id)%>
<%= button_to 'Unlike', likes_unlike_path(likable_id: tweet.id, likable_type: tweet.class.name, user_id: current_user.id), method: :delete %>
<% else %>
<%= button_to 'Like', likes_like_path(likable_id: tweet.id, likable_type: tweet.class.name, user_id: current_user.id), method: :post%>
<% end %>
Now, to display the sizes of a like, you could retrieve the amount of users that have liked the tweet:
<h3>People who liked this tweet: <%= likes.count %> </h3>
obviously, only those people who have liked something will be displayed. If you also need a negative number, you could do two things: Either create a join table for your likes or add a new integer and call it "number_likes"
Then, inside your like method, simply increase the number_likes integer by 1. Whenvever someone clicks on the like link, the number will increase.
Inside the unlike method, simply substract by 1 whenever unlikes something. This should give you more or less the same result, but it would also allow you to add a negativ like number.
def like
number_likes += 1
//your previous logic here
end
def unlike
number_likes -=1
end
in your view, you could then simply get the tweet.likes.number_likes to display the amount of likes.
If something is still unclear, let me know!
Happy coding!

How to select all objects that have instances of another object (has_many and has_one association)?

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 %>

Rails - One-direction table association - accessing data issue

I have two models:
class Order < ActiveRecord::Base
belongs_to :user
has_one :order_type
end
class OrderType < ActiveRecord::Base
belongs_to :order
end
my schema.rb:
create_table "order_types", force: :cascade do |t|
t.string "ort_name"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
create_table "orders", force: :cascade do |t|
t.string "ord_name"
t.date "ord_due_date"
t.integer "user_id"
t.integer "ordertype_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
add_index "orders", ["ordertype_id"], name: "index_orders_on_ordertype_id"
add_index "orders", ["user_id"], name: "index_orders_on_user_id"
There is only one-direction association between them. The Order model has a column "ordertype_id" that links to the appropriate order_type.
My question is, what is the best practice to access the ort_name value for each #order in a view.
Currently, I am using:
<p>
<strong>Ord type:</strong>
<% OrderType.where(id: #order.ordertype_id).each do |t| %>
<%= t.ort_name %>
<% end %>
</p>
This solution results in many code repetitions. How I should change that? Can somebody advise, as I am not so experienced yet?
I tried this code, but it did not work:
#orders.order_type
There are many problems which you should address. It's ok to be a beginner, just take yourself time to learn and improve.
Schema
First off, your schema is set up badly. If you want to limit the order type to certain values, you should do this with a validation.
class Order
TYPES = %w[foo bar three four five]
validates :order_type, inclusion: { in: TYPES }
end
This way, you can easily add values in the future, and remove the complexity of adding a new model and its relations.
Column Names
Secondly, you should revise your column names. ord_name and ord_due_date is bad, it leads to ugly calls like order.ord_name. You should drop the prefix ord, it's superfluous.
Both steps would lead to this schema.rb
create_table "orders", force: :cascade do |t|
t.string "name"
t.date "due_date"
t.integer "user_id"
t.string "order_type"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
Logic placement
My final advice is to never call queries from your view. Logic should always be in the controller / model & passed to the view via instance variables.
This is a big no no in rails:
<% OrderType.where(id: #order.ordertype_id).each do |t| %>
...
<% end %>
In the end, accessing the type is simply accomplished with:
#order.order_type
Update your Order model to this:
class Order < ActiveRecord::Base
belongs_to :user
has_one :order_type, foreign_key: 'ordertype_id`
end
then order_type should be easily accessible:
#order.order_type.ort_name

Resources