How to avoid being friends multiple times - ruby-on-rails

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

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

Move method from Controller to Model in Rails

I have been told to move a method "Top" from Controller to Model, but when I try to call it, it doesn't work anymore.
I am using Rails 6
This is my Controller:
class UsersController < ApplicationController
before_action :authenticate_user!
def index
#users = User.all
end
def show
#user = User.find(params[:id])
#posts = #user.posts.ordered_by_most_recent
end
def edit
#user = User.find(params[:id])
end
def following
#title = 'Following'
#user = User.find(params[:id])
#users = #user.following.paginate(page: params[:page])
render 'show_follow'
end
def followers
#title = 'Followers'
#user = User.find(params[:id])
#users = #user.followers.paginate(page: params[:page])
render 'show_follow'
end
def top
#userst = User.joins(:followers).order('COUNT(followings.follower_id) DESC').group('users.id').limit(10)
end
end
and this would be my Model:
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, authentication_keys: [:username]
validates :fullname, presence: true, length: { maximum: 20 }
validates :username, presence: true, length: { maximum: 20 }
validates_uniqueness_of :username
has_many :posts
has_many :active_followings, class_name: 'Following',
foreign_key: 'follower_id',
dependent: :destroy
has_many :passive_followings, class_name: 'Following',
foreign_key: 'followed_id',
dependent: :destroy
has_many :following, through: :active_followings, source: :followed
has_many :followers, through: :passive_followings, source: :follower
mount_uploader :photo, FileUploader
mount_uploader :coverimage, FileUploader
# Follows a user.
def follow(other_user)
following << other_user
end
# Unfollows a user.
def unfollow(other_user)
following.delete(other_user)
end
# Returns true if the current user is following the other user.
def following?(other_user)
following.include?(other_user)
end
end
All code here makes sense to me, so I only had to create a file called top.html.erb like this to render the Top:
<article class="timeline new-initial">
<h3>Top:</h3>
<ul class="posts">
<%= render #userst %>
</ul>
</article>
Now, to be honest, I am lost, I am not sure how to move this method to the User model in the right way to read it in the view section.
This seems like a job for a scope.
Model:
scope :top, -> { joins(:followers).order('COUNT(followings.follower_id) DESC').group('users.id').limit(10) }
Controller:
def top
#userst = User.top
end

How to nest devise_invitable such that a user can be invited to only 1 hotel

Goal
I would like to let a user.admin invite a user to only one of its hotels with the devise_invite gem.
=> User and Hotel are connected via a Join table UserHotel.
Issue
I started to doubt the way I set up devise(_invitable). With the way I currently set it up, I am not able to create the user_hotel Join_table and/or add the specific hotel params to the invited user, see output below for checks:
controller
>> #user.hotels => #<ActiveRecord::Associations::CollectionProxy []>
console:
pry(main)> User.invitation_not_accepted.last.hotels
User Load (0.6ms) SELECT "users".* FROM "users" WHERE "users"."invitation_token" IS NOT NULL AND "users"."invitation_accepted_at" IS NULL ORDER BY "users"."id" DESC LIMIT $1 [["LIMIT", 1]]
Hotel Load (0.4ms) SELECT "hotels".* FROM "hotels" INNER JOIN "user_hotels" ON "hotels"."id" = "user_hotels"."hotel_id" WHERE "user_hotels"."user_id" = $1 [["user_id", 49]]
=> []
UPDATE
The issue seems to be in the many-to-many relationship between user and hotel. When I break my controller 'new' action after hotel.user.new and test it I het the following:
>> #user.hotels => #<ActiveRecord::Associations::CollectionProxy []>
>> #hotel.users => #<ActiveRecord::Associations::CollectionProxy [#<User id: 2, email: "test#hotmail.com", created_at: "2019-11-05 14:17:46", updated_at: "2019-11-05 15:04:22", role: "admin">, #<User id: nil, email: "", created_at: nil, updated_at: nil, role: "admin">]>
Note
I set up users with devise, such that my users controller is build up as:
users/confirmations_controller.rb
users/invitations_controller.rb
users/omniauth_callbacks_controller.rb
users/password_controller.rb
users/registrations_controller.rb
users/sessions_controller.rb
users/unlocks_controller.rb
Code
routes
Rails.application.routes.draw do
devise_for :users
resources :hotels do
devise_for :users, :controllers => { :invitations => 'users/invitations' }
end
end
models
class User < ApplicationRecord
# Include default devise modules. Others available are:
# :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
has_many :user_hotels, dependent: :destroy
has_many :hotels, through: :user_hotels
accepts_nested_attributes_for :user_hotels
enum role: [:owner, :admin, :employee]
after_initialize :set_default_role, :if => :new_record?
def set_default_role
self.role ||= :admin
end
devise :invitable, :database_authenticatable, :registerable,
:recoverable, :rememberable, :validatable, :invitable
end
class UserHotel < ApplicationRecord
belongs_to :hotel
belongs_to :user
end
class Hotel < ApplicationRecord
has_many :user_hotels, dependent: :destroy
has_many :users, through: :user_hotels
accepts_nested_attributes_for :users, allow_destroy: true, reject_if: ->(attrs) { attrs['email'].blank? || attrs['role'].blank?}
end
views/hotels/show
<%= link_to "invite new user", new_user_hotel_invitation_path(#hotel)%>
controllers/users/invitations_controller.rb
class Users::InvitationsController < Devise::InvitationsController
def new
#hotel = Hotel.find(params[:hotel_id])
#user = #hotel.users.new
end
def create
#hotel = Hotel.find(params[:hotel_id])
#user = #hotel.users.new(hotel_user_params)
#user.invite!
end
private
def hotel_user_params
params.require(:user).permit(:email, :role,
hotel_users_attributes: [:hotel_id])
end
end
views/invitations/new.html.erb
<h2><%= t "devise.invitations.new.header" %></h2>
<%= simple_form_for(resource, as: resource_name, url: user_hotel_invitation_path(#hotel), html: { method: :post }) do |f| %> <%= f.error_notification %>
<% resource.class.invite_key_fields.each do |field| -%>
<div class="form-inputs">
<%= f.input field %>
</div>
<% end -%>
<%= f.input :role, collection: [:owner, :admin, :employee] %>
<div class="form-actions">
<%= f.button :submit, t("devise.invitations.new.submit_button") %>
</div>
<% end %>
Did you check this:
invitation_limit: The number of invitations users can send. The
default value of nil means users can send as many invites as they
want, there is no limit for any user, invitation_limit column is not
used. A setting of 0 means they can't send invitations. A setting n >
0 means they can send n invitations. You can change invitation_limit
column for some users so they can send more or less invitations, even
with global invitation_limit = 0.
Also, it would be really smart move to check more details about gem for more options: https://github.com/scambra/devise_invitable
I have something working, even tough I am not sure if this is the best way to do it (read: I highly doubt it).
class Users::InvitationsController < Devise::InvitationsController
def new
#hotel = Hotel.find(params[:hotel_id])
#user = #hotel.users.new
#user.hotels << #hotel
end
def create
#hotel = Hotel.find(params[:hotel_id])
#user = #hotel.users.new(hotel_user_params)
#user.hotels << #hotel
#user.invite!
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!

Rails Client side Collection Validation fails - Simple Form

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

Resources