Display soft-deleted records in Rails - ruby-on-rails

I am a rookie in Ruby on Rails, at the moment, I am developing a Rails application comes with soft delete feature. As you can see in the controller, in the index action, as an admin I will display records that are not included soft-deleted records as default.
But I want to make a checkbox or button on the page to include the soft-deleted records when I checked or click them. And I'm quite stuck right here, if anyone have a solution it would be great. Btw, don't hesitate to suggest a better approach for this situation. Any helps would be appreciated, I attach the controller and model below.
Controller
class BookingsController < ApplicationController
before_action :admin_user, only: %i(edit update)
before_action :load_tour_detail, only: %i(create reduce_quantity cal_revenue)
before_action :load_booking, except: %i(index new create index_with_deleted)
after_action :reduce_quantity, :cal_revenue, only: :create
after_action :increse_quantity, only: :destroy
respond_to :html, :json
def index
#bookings = if current_user.admin?
Booking.includes(:tour_detail).not_deleted
.paginate(page: params[:page])
else
Booking.includes(:tour_detail).where(user_id: current_user.id)
.paginate(page: params[:page])
end
end
Model
class Booking < ApplicationRecord
belongs_to :tour_detail
belongs_to :user
validates :people_number, presence: true
before_save :cal_price
enum status: {pending: 1, confirmed: 2, cancelled: 3}
scope :not_deleted, -> {where("deleted_at IS NULL")}
scope :deleted, -> {where("deleted_at IS NOT NULL")}
def soft_delete
update deleted_at: Time.now
end
def recover
update deleted_at: nil
end
end
UPDATE:
I found a solution for this, I made a button on index page which pass a param to controller. Then on the controller I check the praram is it valid? And redirect my app to the right action that includes soft_deleted records or not. I will post the solution below just in case my explanation in English is too bad.
If someone has any better solution for this, please just suggest, it would be useful for me. Thank you guys!
Controller
class BookingsController < ApplicationController
before_action :admin_user, only: %i(edit update)
before_action :load_tour_detail, only: %i(create reduce_quantity cal_revenue)
before_action :load_booking, except: %i(index new create index_with_deleted)
after_action :reduce_quantity, :cal_revenue, only: :create
after_action :increse_quantity, only: :destroy
respond_to :html, :json
def index
#bookings = if current_user.admin?
if params.has_key?(:soft_deleted)
Booking.includes(:tour_detail).all
.paginate(page: params[:page])
else
Booking.includes(:tour_detail).not_1deleted
.paginate(page: params[:page])
end
else
Booking.includes(:tour_detail).where(user_id: current_user.id)
.paginate(page: params[:page])
end
end
View
<% if params.has_key?(:soft_deleted) %>
<%= link_to bookings_path, class: "btn btn-sm btn-primary" do %>
Only existed bookings
<% end %>
<% else %>
<%= link_to bookings_path(soft_deleted: true), class: "btn btn-sm btn-warning" do %>
Included deleted bookings
<% end %>
<% end %>

You can use something like below:
def index
#bookings = if current_user.admin?
Booking.includes(:tour_detail)
.deleted(params[:checkbox_input_name] # ----------> This line here
.paginate(page: params[:page])
else
Booking.includes(:tour_detail)
.where(user_id: current_user.id)
.deleted(params[:checkbox_input_name] # ----------> This line here
.paginate(page: params[:page])
end
end
And then in your model, add a scope like below:
class Booking < ApplicationRecord
# -----------------
scope :deleted, -> lambda { |deleted_at| where("deleted_at IS NULL") if deleted_at.present? }
# The above scope will return all objects if deleted_at param is not present.
private
def soft_delete
update deleted_at: Time.now
end
def recover
update deleted_at: nil
end
end

Related

Rails ERB Validation

I'm trying to add a validation to my Rails app in order to display an error message if the user goes to the wrong id. The project has reviews, if I go to http://localhost:3000/reviews/:id that doesn't exist the app crashes, I'd like to prevent the runtime error by displaying a message.
In the model, I got this validation:
class Review < ApplicationRecord
validates :id, presence: true
end
Then, in the reviews/show.html.erb file, I'm trying this:
<% if #review.valid? %>
<div class='review-header'>
....
</div>
<% else %>
<% #review.errors.objects.first.full_message %>
<% end %>
This is also the Reviews Controller:
class ReviewsController < ApplicationController
before_action :set_review, only: [:show, :edit, :update, :destroy]
before_action :authorize!, only: [:edit, :destroy]
def index
if params[:search]
#reviews = Review.where("title like ?", "%#{params[:search]}%")
else
#reviews = Review.all
end
end
def new
#review = Review.new
#comment = Comment.new
#comment.review_id = #review.id
#We need to declare the comments in the new action.
end
def create
#review = current_user.reviews.new(review_params)
if #review.save
redirect_to review_path(#review)
else
render 'new'
end
end
def show
#comment = Comment.new
#We also need to declare the new comment in the show action.
end
def edit
end
def update
if #review.update(review_params)
redirect_to review_path(#review)
else
render 'edit'
end
end
def destroy
#review.destroy
redirect_to reviews_path
end
private
def set_review
#review = Review.find_by(id: params[:id])
end
def review_params
params.require(:review).permit(:title, :content, :category_id, :search)
end
def authorize!
authorize #review #authorize method using the Pundit gem
end
end
However, my project keep crashing rather than showing a message. If there's any way I can make this work? Thanks.
The whole setup of the question is actually broken.
You don't need to add a model validation for the id since ids are automatically generated by the database when you insert records. On most databases primary keys are also non-nullable. Adding the validation will actually break the model as will prevent you from saving records without manually assigning an id (bad idea).
Its also not the models job to verify that a record can be found in the controller. Instead your controller should use find so that it bails early if the record cannot be found:
class ReviewsController < ApplicationController
before_action :set_review, only: [:show, :edit, :update, :destroy]
before_action :authorize!, only: [:edit, :destroy]
private
def set_review
#review = Review.find(params[:id])
end
end
This halts execution of the method and other callbacks and prevents the NoMethodErrors that are bound to occur. There is no sense in continuing to process a request if the record that its supposed to CRUD doesn't exist.
By default Rails will handle an uncaught ActiveRecord::RecordNotFound exception by rendering a static HTML page located at public/404.html and returning a 404 status code. If you want to customize this on the controller level use rescue_from:
class ReviewsController < ApplicationController
before_action :set_review, only: [:show, :edit, :update, :destroy]
before_action :authorize!, only: [:edit, :destroy]
rescue_from ActiveRecord::RecordNotFound, with: :not_found
private
def set_review
#review = Review.find(params[:id])
end
def not_found
# renders app/reviews/not_found.html.erb
render :not_found,
status: :not_found
end
end
Note that this should be done in different view. If you add a <% if #review.present? %> to your reviews/show.html.erb view you should get your Rails licence revoked as the views one and only job is to display the review.
You can also configure the responses on the application level with config.exceptions_app.
The problem is that if the ID does not correspond to a review in the database, the #review object will be nil, and your line if #review.valid? will throw an error.
You need a different test, something like
<% if #review.present? %>
<div class='review-header'>
....
</div>
<% else %>
Review does not exist.
<% end %>

Displaying item with a specific parameter in the index

I am building a website for poetries.
There are two different type of poetries: famous or amateur.
I built the CRUD functions to display all the poetries (famous and amateur, without distinction) and this is working as intended (see the PoetrisController code below).
Now, I want to give the possibility to the user to choose if he wants to see only the amateur poetries or famous ones.
Basically the user clicks the link "Amateur" or "Famous" in the navbar and he is redirected to a new page listing only amateur or famous poetries.
My question is: should I create another Controller (for example PoetriesFamousController) and creating a index function inside it to display only the famous poetries or there is a way to use the already existing PoetriesController to show only the "famous poetries" if the user clicks the link in the navbar?
PoetriesController:
class PoetriesController < ApplicationController
skip_after_action :verify_authorized, only: [:home, :about, :newsletter, :disclaimer, :new, :create]
skip_before_action :authenticate_user!, only: [:home, :about, :newsletter, :disclaimer, :new, :create]
before_action :set_poetry, only: [:show, :edit, :update, :destroy,]
before_action :authenticate_user!, except: [:index, :amateur_poetries]
def index
if params[:search]
#poetries = policy_scope(Poetry).search(params[:search]).order("created_at DESC").limit(30)
else
#poetries = policy_scope(Poetry).order("RANDOM()").limit(30)
end
end
def show
authorize #poetry
end
def new
#poetry = Poetry.new
end
def create
Poetry.create(poetry_params)
redirect_to poetries_path
end
def edit
authorize #poetry
end
def update
#poetry.save
redirect_to poetry_path(#poetry)
end
def destroy
#poetry.destroy
redirect_to poetries_path
end
private
def poetry_params
params.require(:poetry).permit(:title, :author, :body, :poster, :country)
end
def set_poetry
#poetry = Poetry.find(params[:id])
end
end
Poetries.rb
class Poetry < ApplicationRecord
def self.search(search)
where("lower(title) LIKE ? OR lower(author) LIKE ? OR lower(country) LIKE ? OR lower(born) LIKE ?", "%#{search}%", "%#{search}%", "%#{search}%", "%#{search}%")
end
end
Routes.rb
get 'poetries', to: 'poetries#index', as: :poetries
get "poetries/new", to: "poetries#new"
post "poetries", to: "poetries#create"
get "poetries/:id/edit", to: "poetries#edit"
patch "poetries/:id", to: "poetries#update"
get 'poetries/:id', to: 'poetries#show', as: :poetry
delete "poetries/:id", to: "poetries#destroy"
Here is some idea for your problem
In your view (sample idea)
poetries type:
<%= select_tag :poetries_type, options_for_select(["Famous","Amateur"]), include_blank: true, :class => 'form-control' %>
in your controller:
def index
if params[:search]
if params[:poetries_type] == "Famous"
#poetries = Poetry.famous.search(params[:search]).order("created_at DESC").limit(30)
elsif params[:poetries_type] == "Amateur"
#poetries = Poetry.amateur.search(params[:search]).order("created_at DESC").limit(30)
else
#poetries = Poetry.search(params[:search]).order("created_at DESC").limit(30)
end
else
#poetries = policy_scope(Poetry).order("RANDOM()").limit(30)
end
end
Poetries.rb, add two scope for famous an amateur
def self.amateur
where("poster != ?","Admin")
end
def self.famous
where("poster = ?","Admin")
end
The simplest thing would be to add two more actions to your controller.
def famous
#poetries = #get the famous ones
render :index
end
def amateur
#poetries = #get the amateur ones
render :index
end
Then update your routes
get 'poetries', to: 'poetries#index', as: :poetries
get 'poetries/famous', to: 'poetries#famous'
get 'poetries/amateur', to: 'poetries#amateur
# rest of the routes

Rails 5 Not Raising Error with self.errors.add(:base)

I have a model called Thing and a controller called Things.
I followed this tutorial to try and set a maximum amount of Things a user can create.
Here's the warning: the terminal is giving a warning (not a huge issue) of DEPRECATION WARNING: Passing an argument to force an association to reload is now deprecated and will be removed in Rails 5.1. Please call "reload" on the result collection proxy instead. What should I do to make it go away?
Here's the problem: The line self.errors.add(:base, "Exceeded Things Limit") isn't displaying an alert or notice in the view. How would I achieve this? It's not creating a new Thing (because I met the maximum limit of 2) which is good, but it's just reloading a new form which would be horrible for user experience.
I'm working Rails 5 and Devise.
Here's my Thing model:
class Thing < ApplicationRecord
belongs_to :user
validate :thing_count_within_limit, :on => :create
attr_accessor :validation_code, :flash_notice
def self.search(search)
if search
where("zipcode LIKE ?", "%#{search}%")
else
all
end
end
def thing_count_within_limit
if self.user.things(:reload).count >= 2
self.errors.add(:base, "Exceeded Things Limit")
end
end
end
And here's my Things controller:
class thingsController < ApplicationController
before_action :find_thing, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user_first, only: [:edit, :update, :destroy]
before_action :authorized_pilot, only: [:edit, :update, :destroy, :profile]
def index
#things = Thing.all.order("created_at ASC")
#things = Thing.search(params[:search])
end
def new
#thing = current_user.things.build
end
def create
#thing = current_user.things.build(thing_params)
if #thing.save
redirect_to #thing
else
render "new"
end
end
def profile
#things = Thing.where(user_id: current_user)
end
def show
end
def edit
end
def update
if #thing.update(thing_params)
redirect_to #thing
else
render "edit"
end
end
def destroy
if #thing.destroy
redirect_to root_path
else
redirect_to #thing
end
end
private
def thing_params
params.require(:thing).permit(:title, :description, :image).merge(zipcode: current_user.zipcode)
end
def find_thing
#thing = thing.find(params[:id])
end
def authenticate_user_first
if current_user != thing.find(params[:id]).user
redirect_to #thing
else
end
end
end
Can anyone help? Help is greatly appreciated.
There are two things that aren't connected to each other.
First, there is the deprecation warning. Because it is just a warning, not an error, you can choose to ignore it at the moment. If you want to remove the warning, just follow its instruction and change this line
if self.user.things(:reload).count >= 2
to
self.user.things.reload.count >= 2
Seconds, your code works like expected. Rails validations do not raise any errors, but they add error messages to the object. Just make sure that you display the errors to the user. To display the error you added to :base, add something like the following to your new.html.erb view:
<% if #thing.errors[:base].any? %>
<div class="error_message">
<%= #thing.errors.full_messages_for(:base).to_sentence %>
</div>
<% end %>

Why am I getting a recordnotfound error when trying to access an instance in rails?

I have a user profile controller called "userinfo" and it's corresponding view. The userinfo index is the root path. In the homepage(which is the userinfo index), I have a link that takes you to the user profile page. It is giving me this error when I go to the home page:
My routes are:
My userinfos_controller:
class UserinfosController < ApplicationController
before_action :find_userinfo, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!
def index
#userinfors = Userinfo.find(params[:id])
end
def show
#myvideo = Video.last
end
def new
#userinformation = current_user.userinfos.build
end
def create
#userinformation = current_user.userinfos.build(userinfo_params)
if #userinformation.save
redirect_to root_path
else
render 'new'
end
end
def edit
end
def update
end
def destroy
#userinformation.destroy
redirect_to userinfo_path
end
private
def userinfo_params
params.require(:userinfo).permit(:name, :email, :college, :gpa, :major)
end
def find_userinfo
#userinformation = Userinfo.find(params[:id])
end
end
and my view is:
<%= link_to 'profile', userinfors_path(#userinfors) %>
My routes.rb file:
Rails.application.routes.draw do
devise_for :users
resources :userinfos do
resources :videos
end
resources :pages
get '/application/decide' => 'application#decide'
root 'userinfos#index'
get '/userinfos/:id', to: 'userinfos#show', as: 'userinfors'
end
Thanks for any help!
ok, there are multiple errors and you are not following conventions of rails, index is not for what you have used.
Index is used to list all the users and show for a particular one with id passed in params.
Your index path is, as you can see, /userinfos which is correct and it doesn't have any id with it but you are trying to find user with params[:id] which is nil and hence the error.
Lets try out this:
def index
#userinfors = Userinfo.all #pagination is recommended
end
In your index view,
<% #userinfors.each do |userinfor| %>
<%= link_to "#{userinfor.name}'s profile", userinfo_path(userinfor) %>
<% end %>
It should work now.
Please read routing and action controller to get the idea and understand the magic behind rails routing and mvc architecture..

Issue with setting up replies in Rails 4

So I am in the process of setting up a forum and everything is setup/working well except for my replies are not appearing on the thread "show" page. After checking the rails console, I see they are saving but the user_id and discussion_id are no. The user_id is always nil and the discussion_id is always 0. The discussion threads were easier to setup but with having these replies, I obviously seem to be having an issue. Here are my snippets of code:
class PostsController
# ...
before_filter :authenticate_user!
before_filter :set_discussion, only: [:new, :create, :destroy]
def create
#post = #discussion.post.new(create_params) do |post|
post.user = current_user
end
if #post.save
redirect_to #discussion, notice: "It has been posted!"
else
render :new
end
end
def destroy
#post = #discussion.posts.find(params[:id])
#post.destroy
flash.notice = "Deleted"
redirect_to discussion_path(#discussion)
end
private
def create_params
params.require(:post).permit(:reply)
end
def set_discussion
#discussion = Discussion.friendly.find(params[:id])
end
end
class DiscussionsController
def show
#discussion = Discussion.friendly.find(params[:id])
#post = Post.new
render :layout => 'discussion'
end
end
Partial rendered to reply:
<h2>Reply</h2>
<%= form_for [ #discussion, #post ] do |f| %>
<p>
<%= f.label :reply, "Reply" %><br/>
<%= f.text_field :reply %>
</p>
<p>
<%= f.submit 'Submit' %>
</p>
<% end %>
Partial rendered to show replies in on discussion page:
<h3><%= post.user.first_name %></h3>
<%= post.reply %>
Posted: <%= post.created_at.strftime("%b. %d %Y") %></p>
<p><%= link_to "Delete Comment", [post.discussion, post], data: {confirm: "Are you sure you wish to delete?"}, method: :delete, :class => "post_choices" %></p>
Just want to mention that I also have the correct associations between the three models (User, Discussion, Post). If there is more code needed, please let me know. I appreciate it very much for any information that may be helpful =)
Joe
EDIT
class User < ActiveRecord::Base
has_many :articles
has_many :discussions
has_many :posts
# ...
end
class Discussion
belongs_to :user
has_many :posts
extend FriendlyId
friendly_id :subject, use: :slugged
end
class Post
belongs_to :discussion
belongs_to :user
end
I could post the entire user model if needed but its all validations/devise aspects =P The other two I listed all of the contents in the models.
Edit 2
Thanks to Max, the user_id returns correctly in the console but still not the discussions. Going go dig around a bit more with the recent changes to see what else =)
There are a few issue you need to deal with.
First you should ensure that Devise is actually authorizing your controller action.
class PostsController < ApplicationController
before_filter :authenticate_user!
end
Otherwise current_user will return nil if there is no signed in user. And I'm
guessing that you do not want un-authenticated users to be able to create posts.
Also if you have a nested route you most likely want to check that the discussion actually
exists before trying to add posts.
class PostsController
before_filter :authenticate_user!
before_filter :set_discussion, only: [:new, :create, :destroy]
private
# Will raise an ActiveRecord::NotFoundError
# if the Discussion does not exist
def set_discussion
#discussion = Discussion.friendly.find(params[:id])
end
end
When you are creating resources be careful not to query the database needlessly.
This especially applies to CREATE and UPDATE queries which are expensive.
def create
#post = Post.create(post_params) # INSERT INTO 'users'
#post.discussion_id = params[:discussion_id]
#post.user = current_user
#post.save # UPDATE 'users'
flash.notice = "It has been posted!"
redirect_to discussions_path(#post.discussion)
end
Also you are not even checking if the record was created successfully.
So lets put it all together:
class PostsController
before_filter :authenticate_user!
before_filter :set_discussion, only: [:new, :create, :destroy]
def new
#post = #discussion.post.new
end
def create
# new does not insert the record into the database
#post = #discussion.post.new(create_params) do |post|
post.user = current_user
end
if #post.save
redirect_to #discussion, notice: "It has been posted!"
else
render :new # or redirect back
end
end
def destroy
#post = #discussion.posts.find(params[:id])
#post.destroy
flash.notice = "Deleted"
redirect_to discussion_path(#discussion)
end
private
def create_params
# Only permit the params which the user should actually send!
params.require(:post).permit(:reply)
end
# Will raise an ActiveRecord::NotFoundError
# if the Discussion does not exist
def set_discussion
#discussion = Discussion.friendly.find(params[:id])
end
end

Resources