Rails Client side Collection Validation fails - Simple Form - ruby-on-rails

I have to build a simple app that allows users to loan and borrow books. Simply put a User can create books, and they can pick another user to loan the book to.
I have three models User, Book and Loan:
class User < ActiveRecord::Base
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
has_many :books
has_many :loans, through: :books
has_many :borrowings, class_name: "Loan"
validates :username, uniqueness: true
validates :username, presence: true
end
class Book < ActiveRecord::Base
belongs_to :user
has_many :loans
validates :title, :author, presence: true
end
class Loan < ActiveRecord::Base
belongs_to :user
belongs_to :book
validates :user, :book, :status, presence: true
end
The LoansController looks like this:
class LoansController < ApplicationController
before_action :find_book, only: [:new, :create]
def new
#users = User.all
#loan = Loan.new
authorize #loan
end
def create
#loan = Loan.new
#loan.book = #book
#loan.user = User.find(loan_params[:user_id])
#loan.status = "loaned"
authorize #loan
if #loan.save
redirect_to :root
else
render :new
end
end
private
def loan_params
params.require(:loan).permit(:user_id)
end
def find_book
#book = Book.find(params[:book_id])
end
end
My form looks like:
<%= simple_form_for([#book, #loan]) do |f| %>
<%= f.input :user_id, collection: #users.map { |user| [user.username, user.id] }, prompt: "Select a User" %>
<%= f.submit %>
<% end %>
If I submit the form without selecting a user, and keep the "Select a User" prompt option, the form is submitted and the app crash because it can't find a user with id=
I don't know why the user presence validation in the form does not work...

you will change your Create method
def create
#loan = Loan.new
#loan.book = #book
#loan.user = User.find_by_id(loan_params[:user_id])
#loan.status = "loaned"
authorize #loan
if #loan.save
redirect_to :root
else
render :new
end
end

Related

Ruby- How to add comments list to user's dashboard?

I have three tables: books, comments and users. Tables are related. Users can login and then comment on books. Users have a dashboard. How can I make users see the comments that have been made to their books on their dashboard?
Here are the models,
book.rb:
class Book < ApplicationRecord
validates :title, presence: true
validates :author, presence: true
belongs_to :user
has_many :comments
end
comment.rb:
class Comment < ApplicationRecord
belongs_to :book
belongs_to :user
scope :approved, -> {where(status: true)}
end
user.rb:
class User < ApplicationRecord
before_create :set_username
has_many :books
has_many :comments
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable
attr_writer :login
def login
#login || self.username || self.email
end
# validates_length_of :username,
# :within => 5..50,
# :too_short => " is too short, must be at least 5 characters.",
# :presence => true
private
def set_username
self.username = self.email.split("#").first
end
def self.find_first_by_auth_conditions(warden_conditions)
conditions = warden_conditions.dup
if login = conditions.delete(:login)
where(conditions).where(["lower(username) = :value OR lower(email) = :value", { :value => login.downcase }]).first
else
if conditions[:username].nil?
where(conditions).first
else
where(username: conditions[:username]).first
end
end
end
end
dashboard controller:
class DashboardController < ApplicationController
def index
#books = current_user.books
end
end
Since you already have the book, you can also include the comments to reduce n+1. So in your DashboardController you can modify the statement you have with #books = current_user.books.includes(:comments). This will collect all of the comments for that user's books. Then in the view you can iterate through them like this. Where you can display them however you want, ie, ol/ul with children li for teh comments
<% #books.each do |book| %>
<%= book.title %>
<%= book.author %>
<p>Comments:</p>
<% book.comments.each do |comment| %>
<%= comment.text %>
<% end %>
<% end %>
I have some suggestion for you.
You can write like this
class DashboardController < ApplicationController
def index
#books = current_user.books.includes(:comments)
end
end
It will get all of the comments you need, especially you also avoid n+1 query.
Or you can also write
class DashboardController < ApplicationController
def index
#books = current_user.books
#comments = current_user.comments.where('id = ?', #books.pluck(:id))
end
end

Complete course and modules using Rails 5 assign to user

Edit #2
Here is the courses controller
class CoursesController < ApplicationController
layout proc { user_signed_in? ? "dashboard" : "application" }
before_action :set_course, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!, except: [:index, :show]
before_action :authorize_admin, except: [:index, :show, :complete]
def index
#courses = Course.all.order(created_at: :asc)
end
def show
course = Course.friendly.find(params[:id])
#course_modules = course.course_modules.order(created_at: :asc)
end
def new
#course = Course.new
end
def edit
end
def create
#course = Course.new(course_params)
respond_to do |format|
if #course.save
format.html { redirect_to courses_path, notice: 'Course was successfully created.' }
format.json { render :show, status: :created, location: courses_path }
else
format.html { render :new }
format.json { render json: #course.errors, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if #course.update(course_params)
format.html { redirect_to #course, notice: 'Course was successfully updated.' }
format.json { render :show, status: :ok, location: #course }
else
format.html { render :edit }
format.json { render json: #course.errors, status: :unprocessable_entity }
end
end
end
def destroy
#course.destroy
respond_to do |format|
format.html { redirect_to courses_url, notice: 'Course was successfully destroyed.' }
format.json { head :no_content }
end
end
private
def set_course
#course = Course.friendly.find(params[:id])
end
def course_params
params.require(:course).permit(:title, :summary, :description, :trailer, :price)
end
end
Edit #1
So going off Jagdeep's answer below I have now done the following:
course.rb
class Course < ApplicationRecord
extend FriendlyId
friendly_id :title, use: :slugged
has_many :course_modules
validates :title, :summary, :description, :trailer, :price, presence: true
def complete?
self.update_attribute(:complete, true)
end
end
course_modules_user.rb
class CourseModulesUser < ApplicationRecord
belongs_to :course_module
belongs_to :user
def complete!
self.update_attribute(:complete, true)
end
end
courses_user.rb
class CoursesUser < ApplicationRecord
belongs_to :course
belongs_to :user
end
user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable
has_one_attached :avatar
has_many :courses_users
has_many :courses, through: :courses_users
has_many :course_modules_users
has_many :course_modules, through: :course_modules_users
def mark_course_module_complete!(course_module)
self.course_modules_users
.where(course_module_id: course_module.id)
.first
.complete!
end
def after_confirmation
welcome_email
super
end
protected
def welcome_email
UserMailer.welcome_email(self).deliver
end
end
Migrations
class CreateCoursesUsers < ActiveRecord::Migration[5.2]
def change
create_table :courses_users do |t|
t.integer :course_id
t.integer :user_id
t.boolean :complete
t.timestamps
end
end
end
class CreateCourseModulesUsers < ActiveRecord::Migration[5.2]
def change
create_table :course_modules_users do |t|
t.integer :course_module_id
t.integer :user_id
t.boolean :complete
t.timestamps
end
end
end
However, I'm getting errors like this
Original Question
So this is a continuation of a previous question, however, this will stray off from the topic of that so here is a new one.
After this, I got roughly what I wanted to get working which is allowing people to mark off modules and make the course complete if all modules are complete. However, upon testing a new user the modules and courses are being marked as complete (obviously a new user isn't going to complete the course on sign-in, nor are any modules going to be complete) so I need for all users to be separate in terms of what is marked as complete and what isn't.
Previously a user by the name of #engineersmnky mentioned HABTM relationship, however, I've not dealt with this previously.
Here is how I have things setup thus far:
user.rb
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable, :confirmable,
:recoverable, :rememberable, :trackable, :validatable
has_one_attached :avatar
has_many :courses
def after_confirmation
welcome_email
super
end
protected
def welcome_email
UserMailer.welcome_email(self).deliver
end
end
course.rb
class Course < ApplicationRecord
extend FriendlyId
friendly_id :title, use: :slugged
has_many :users
has_many :course_modules
validates :title, :summary, :description, :trailer, :price, presence: true
def complete!
update_attribute(:complete, true)
end
end
course_module.rb
class CourseModule < ApplicationRecord
extend FriendlyId
friendly_id :title, use: :slugged
belongs_to :course
has_many :course_exercises
validates :title, :course_id, presence: true
scope :completed, -> { where(complete: true) }
after_save :update_course, if: :complete?
private
def update_course
course.complete! if course.course_modules.all?(&:complete?)
end
end
if the course is complete conditional courses/index.html.erb
<% if course.complete? %>
<%= link_to "Completed", course, class: "block text-lg w-full text-center text-white px-4 py-2 bg-green hover:bg-green-dark border-2 border-green-dark leading-none no-underline" %>
<% else %>
<%= link_to "View Modules", course, class: "block text-lg w-full text-center text-grey-dark hover:text-darker px-4 py-2 border-2 border-grey leading-none no-underline hover:border-2 hover:border-grey-dark" %>
<% end %>
if course module is complete conditional courses/show.html.erb
<% if course_module.complete? %>
<i class="fas fa-check text-green float-left mr-1"></i>
<span class="text-xs mr-2">Completed</span>
<% else %>
<%= link_to complete_course_module_path(course_module), method: :put do %>
<i class="fas fa-check text-grey-darkest float-left mr-2"></i>
<% end %>
Databases
Course Modules
Courses
You will need to create new tables courses_users and course_modules_users to distinguish between courses/course_modules of different users.
Remove field complete from tables courses and course_modules. We don't want to mark a course/course_module as completed globally. See this for how to use migrations to do that.
Further, define has_many :through associations between users and course/course_modules as below:
class User < ApplicationRecord
has_many :courses_users
has_many :courses, through: :courses_users
has_many :course_modules_users
has_many :course_modules, through: :course_modules_users
end
class Course < ApplicationRecord
has_many :course_modules
end
class CoursesUser < ApplicationRecord
# Fields:
# :course_id
# :user_id
# :complete
belongs_to :course
belongs_to :user
end
class CourseModule < ApplicationRecord
belongs_to :course
end
class CourseModulesUser < ApplicationRecord
# Fields:
# :course_module_id
# :user_id
# :complete
belongs_to :course_module
belongs_to :user
end
Now, the queries can be made this way:
Course.all
=> All courses
Course.find(1).course_modules
=> All course modules of a course
user = User.find(1)
course = Course.find(1)
# Assign `course` to `user`
user.courses_users.create(course_id: course.id)
user.courses
=> [course]
course_module = CourseModule.find(1)
# Assign `course_module` to `user`
user.course_modules_users.create(course_module_id: course_module.id)
user.course_modules
=> [course_module]
Now, to mark a course module complete for a user, do this:
class User < ApplicationRecord
def mark_course_module_complete!(course_module)
self.course_modules_users
.where(course_module_id: course_module.id)
.first
.complete!
end
end
class CourseModulesUser < ApplicationRecord
def complete!
self.update_attribute(:complete, true)
end
end
course_module = CourseModule.find(1)
user.mark_course_module_complete!(course_module)
Similarly for courses:
class User < ApplicationRecord
def mark_course_complete!(course)
self.courses_users
.where(course_id: course.id)
.first
.complete!
end
end
class CoursesUser < ApplicationRecord
def complete!
self.update_attribute(:complete, true)
end
end
This should solve your issue of marking courses and course modules as completed on user basis.
There are other things to consider to make it completely functional which i will leave for you to implement e.g. marking a user's course complete automatically when all course modules of a user are complete (Yes, you need to fix that again), marking a user's course incomplete if at least one of its course modules get incompleted, etc.
SO is always open, if you get stuck again.
Undefined method complete? for CourseModule:0x000..
I will focus on the error and explain the reason for it. You are calling complete? on a course_module here <% if course_module.complete? %>. But you don't have a method called complete? in the CourseModule model. That explains why the error has triggered.
You should define it in the CourseModule to avoid the error
class CourseModule < ApplicationRecord
def complete?
#your logic here
end
end
Note:
If you are willing to try a different approach, I will recommend you to have a go with enums. enums are very powerful and serves with in-built methods which comes very handy.
For example, you can change the CourseModel to below with enums
class CourseModule < ApplicationRecord
extend FriendlyId
friendly_id :title, use: :slugged
enum status: [ :completed, :not_completed ]
belongs_to :course
has_many :course_exercises
.......
end
By this you can simply call course_module.completed? which returns true or false based on the status of the course_module. And to update a course_module status as completed, just call course_module.completed!

has_one, has_many & polymorphic relations

I want to add an adress to gym and that gym only have one adress, so also i have users that can have many adresses, cause that i want to use polymorphic relations for adresses but the fields_for it's not shown
Models
# Adress model
class Adress < ApplicationRecord
include AdressTypeAsker
belongs_to :adressable, polymorphic: true
end
# Gym model
class Gym < ApplicationRecord
belongs_to :user
has_one :adress, as: :adressable
accepts_nested_attributes_for :adress
end
# User model
class User < ApplicationRecord
include UserTypeAsker
include UserAdressType
devise :database_authenticatable, :registerable, :recoverable, :rememberable, :trackable, :validatable
has_many :adresses, as: :adressable
accepts_nested_attributes_for :adresses
has_many :gyms
end
Gym Controller
class GymsController < ApplicationController
def index
if current_user.is_manager?
#gym = current_user.gyms.new
#gym.adress
end
end
def create
if current_user.is_manager?
#gym = current_user.gyms.new(gym_params)
#gym.save!
redirect_to gyms_path
else
redirect_to gym_path
end
end
private
def gym_params
params.require(:gym).permit(:name, :open, :close, adress_attributes: [:name, :longitude, :latitude])
end
end
Nested form
= form_for #gym do |f|
.col.s6
= f.label :open
= f.time_field :open, class: "timepicker"
.col.s6
= f.label :close
= f.time_field :close, class: "timepicker"
.col.s12
= f.label :name
= f.text_field :name
.col.s12
= f.fields_for :adress do |adress_form|
= adress_form.label :name, "adress name"
= adress_form.text_field :name
= adress_form.label :longitude
= adress_form.text_field :longitude
= adress_form.label :latitude
= adress_form.text_field :latitude
= f.submit "Add home adress", class: "waves-effect waves-teal btn-large gymtoy-primary z-depth-0"
Migration
class AddAdressToGyms < ActiveRecord::Migration[5.1]
def change
add_column :adresses, :adressable_id, :integer, index: true
add_column :adresses, :adressable_type, :string
end
end
you new to build address for gym, so modify index action as
def index
if current_user.is_manager?
#gym = current_user.gyms.new
#gym.build_adress
end
end
OR
modify fields_for as
= f.fields_for :adress, #gym.adress || #gym.build_adress do |adress_form|

Nested objects in form not validating

I have a nested form where users can book appointments. However, I've noticed an issue with the form where a user can fill out the required Client model fields and not the required Appointment model fields and the form still submits since for some reason the validation on the Appointment model isn't being triggered. The only time the Appointment validation is triggered is when the associated form fields are populated. How do I get the nested form to verify that the Appointment fields are being filled out? Since clients can have multi
Customer model:
class Customer < ActiveRecord::Base
has_many :appointments
accepts_nested_attributes_for :appointments
attr_accessible :name, :email, :appointments_attributes
validates_presence_of :name, :email
validates :email, :format => {:with => /^[^#][\w.-]+#[\w.-]+[.][a-z]{2,4}$/i}
validates :email, :uniqueness => true
end
Appointment model:
class Appointment < ActiveRecord::Base
belongs_to :customer
attr_accessible :date
validates_presence_of :date
end
Customers controller:
class CustomersController < ApplicationController
def new
#customer = Customer.new
#appointment = #customer.appointments.build
end
def create
#customer = Customer.find_or_initialize_by_email(params[:customer])
if #customer.save
redirect_to success_customers_path
else
# Throw error
#appointment = #customer.appointments.select{ |appointment| appointment.new_record? }.first
render :new
end
end
def success
end
end
Customers form view:
= simple_form_for #customer, :url => customers_path, :method => :post, :html => { :class => "form-horizontal" } do |customer_form|
= customer_form.input :name
= customer_form.input :email
= customer_form.simple_fields_for :appointments, #appointment do |appointment_form|
= appointment_form.input :date
UPDATE: Providing routes
resources :customers, :only => [:new, :create] do
get :success, :on => :collection
end
If a customer has to have an appointment:
class Customer < ActiveRecord::Base
has_many :appointments
accepts_nested_attributes_for :appointments
attr_accessible :name, :email, :appointments_attributes
validates_presence_of :name, :email, :appointment # <- add your appointment
....
end
This will require each customer has at least one appointment.
EDIT based on comment
Instead of using build in your controller, I think you can use create instead which will then associate that appointment with the customer and force validation.
Customers controller:
def edit
#customer = Customer.find_or_initialize_by_email(params[:customer])
#appointment = #customer.appointments.create
end
And you'd do the same in your new method

How to avoid being friends multiple times

I am stuck with the friendships model. I can be friends with an user multiple times. So I need a condition to avoid add to friend link .
My users_controller :
class UsersController < ApplicationController
before_filter :authenticate_user!
def index
#users = User.all
end
def show
#user = User.find(params[:id])
#topics = #user.topics.paginate(page: params[:page])
#friendship = #user.friendships.build(:friend_id => params[:friend_id])
#friendships = #user.friendships.all
end
my show.html.erb:
<section>
<h1><%= #user.username %></h1>
<%= link_to "Arkadaşlarıma Ekle", friendships_path(:friend_id => #user), :method => :post,class: "btn btn-large btn-primary" %>
</section>
my friendships_controller :
class FriendshipsController < ApplicationController
before_filter :authenticate_user!
def create
#friendship = current_user.friendships.build(:friend_id => params[:friend_id])
if #friendship.save
flash[:notice] = "Arkadaşlara eklendi."
redirect_to root_url
else
flash[:error] = "Arkadaşlara Eklenemiyor."
redirect_to root_url
end
end
def destroy
#friendship = current_user.friendships.find(params[:id])
#friendship.destroy
flash[:notice] = "Arkadaşlarımdan kaldırıldı."
redirect_to current_user
end
end
so I guess , I tried add this method in users_controller but still no solution for it. Can you help me to fix it?
def friend?(other_user)
friendships.find_by_friend_id(other_user.id)
end
and In before link
<% unless friend?(#user) %>
%= link_to "Arkadaşlarıma Ekle", friendships_path(:friend_id => #user), :method => :post,class: "btn btn-large btn-primary" %>
<%end %>
My user model :
class User < ActiveRecord::Base
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
# :lockable, :timeoutable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
# Setup accessible (or protected) attributes for your model
attr_accessible :email, :password, :password_confirmation, :remember_me, :username
# attr_accessible :title, :body
has_many :topics
has_many :posts
has_many :friendships
has_many :friends, :through => :friendships
has_many :inverse_friendships, :class_name => "Friendship", :foreign_key => "friend_id"
has_many :inverse_friends, :through => :inverse_friendships, :source => :user
end
my Friendship model :
class Friendship < ActiveRecord::Base
attr_accessible :friend_id, :user_id
belongs_to :user
belongs_to :friend, :class_name => "User"
validates :friend, :presence => true, :unless => :friend_is_self
def friend_is_self
user_id == friend_id ? false : true
end
end
I think it is best to do such validations on the model level. use validate_uniqueness_of in your model and test for validity in your controller.
I think the friend? method should be something similar as:
def friend?(other_user)
current_user.friendships.find_by_friend_id(other_user.id)
end
since I guess friendship should be a table with user_id and friend_id, what we want to check is if this user has othter_user as a friend or not.
Your friend? method is returning the friendship or nil, not true or false as you would expect with a ? method.
Try
def friend?(other_user)
# this assumes Friendship is a user, which probably isn't true
# but I don't have your models but this should give you the right idea
self.friendships.include other_user
end

Resources