How to validate column can be updated once? - ruby-on-rails

I have an agreement model and it has a released_at column. I want to validate that agreement is can be released once. How can I do that?
Controller:
# GET /aggreements/1/release
def release
#agreement.update(released_at: Time.now.utc)
if #agreement.save
render json: {success: ["Agreement released."]}
else
render json: #agreement.errors, status: :unprocessable_entity
end
end
Model:
class Agreement < ApplicationRecord
validate :released_agreement_cannot_be_released
def released_agreement_cannot_be_released
if released_at.present?
errors.add(:released_at, "already released")
end
end
end
Thank you.

I've figured it out, here is the solution.
Controller:
# GET /agreements/1/release
def release
#agreement.released_at = Time.now.utc
if #agreement.save
render json: {success: ["Agreement released."]}
else
render json: #agreement.errors, status: :unprocessable_entity
end
end
Model:
class Agreement < ApplicationRecord
validate :released_agreement_cannot_be_released
def released_agreement_cannot_be_released
if released_at_was.present?
errors.add(:released_at, "already released")
end
end
end

Related

Rails update action fails to check params

I am building a Rails API and found out that put request passes without required parameters. That is weird for me as app won't allow post request without parameters. Moreover, when I’m trying to update the spending without attributes via Rails console, it fails. But via Postman/CURL request passes successfully
The controller looks like this:
class SpendingsController < ApplicationController
before_action :find_spending, only: %i[show update destroy]
def create
spending = Spending.new(spending_params)
spending.user = current_user
spending.category = Category.find_by(id: spending_params[:category_id])
if spending.valid?
spending.save
render json: SpendingSerializer.new(spending), status: :ok
else
render json: ActiveRecordErrorsSerializer.new(spending), status: :bad_request
end
end
def index
spendings = Spending.where(user_id: current_user.id).order("#{sort_spendings}")
total_value = Spending.where(user_id: current_user.id).pluck(:amount).sum
render json: {spendings: SpendingSerializer.new(spendings), total_amount: total_value}, status: :ok
end
def show
if #spending.valid?
render json: SpendingSerializer.new(#spending), status: :ok
else
render json: ActiveRecordErrorsSerializer.new(#spending), status: :not_found
end
end
def update
if #spending.valid?
#spending.update(spending_params)
render json: SpendingSerializer.new(#spending), status: :ok
else
render json: ActiveRecordErrorsSerializer.new(#spending), status: :bad_request
end
end
def destroy
if #spending.destroy
head :no_content
else
render json: ActiveRecordErrorsSerializer.new(#spending), status: :not_found
end
end
private
def spending_params
params.require(:spending).permit(:description, :amount, :category_id)
end
def find_spending
begin
#spending = Spending.find(params[:id])
rescue ActiveRecord::RecordNotFound
render json: {errors: "Spending with id #{params[:id]} not found"}, status: :not_found
end
end
def sort_spendings
sort = { sort_by: "created_at", sort_dir: "desc"}
sort[:sort_by] = params[:sort_by].split(" ").first if params[:sort_by].present?
sort[:sort_dir] = params[:sort_by].split(" ").last if params[:sort_by].present?
sort.values.join(" ")
end
end
And my model:
class Spending < ApplicationRecord
belongs_to :user
belongs_to :category
validates :description,
presence: true
end
I’m really out of ideas, why is that happening. Any guesses what can that be related to?
First thing that I noticed is your update method. You check validation before updating the model. #spending.valid? always returns true in this case. My suggestion to modify it. #spending.update(spending_params) returns true if it update is successful and false if it fails.
def update
if #spending.update(spending_params)
render json: SpendingSerializer.new(#spending), status: :ok
else
render json: ActiveRecordErrorsSerializer.new(#spending), status: :bad_request
end
end
created method an be also optimised. You don't need find and assign category separately. It will be assigned as all spending_params.
def create
spending = Spending.new(spending_params)
spending.user = current_user
spending.save
render json: SpendingSerializer.new(spending), status: :ok
else
render json: ActiveRecordErrorsSerializer.new(spending), status: :bad_request
end
end

Can I validates_uniqueness_of email in two models at once in rails?

I have a model for a client, when I create a new client it creates a User with the client email. The same thing happens when I create an Affiliate. Can I use validates_uniqueness_of the email in both Client and user at the same time?
Or should I do something like, before save check if there is a User with the same email, and print an error?
I tried this, but it doesn't work
validate :uniqueness_of_user
private
def uniqueness_of_user
#user = User.find_by_email(:email)
if #user.present?
errors.add(:email, "Bang!")
end
end
Edit:
This is the controller:
def create
#affiliate = Affiliate.new(affiliate_params)
respond_to do |format|
if verify_recaptcha(model: #affiliate) && #affiliate.save
#user = User.create!(user_parameter)
pplicationMailer.confirmation(#affiliate).deliver_now
format.html {redirect_to :back, notice: 'Thanks for your submission, we will be in touch shortly. Check your email for your affiliate number and password.' }
format.json { render :show, status: :created, location: #affiliate }
else
format.html { render :signup, layout: "sign-ups" }
format.json { render json: #affiliate.errors, status: :unprocessable_entity }
end
end
end
I would have preferred to save the email_address in a different table. for example: address
class Address < ApplicationRecord
belongs_to :affilat
belongs_to :client
validates :email,
presence: true
uniqueness: true
end
and use nested form to save date here.
Check below code for a good article about this case, I copied the code but for full explanation check the article link :
# app/models/concerns/validate_identifier_uniqueness_across_models.rb
module ValidateIdentifierUniquenessAcrossModels
extend ActiveSupport::Concern
##included_classes = []
included do
##included_classes << self
validate :identifier_unique_across_all_models
end
private
def identifier_unique_across_all_models
return if self.identifier.blank?
##included_classes.each do |klass|
scope = klass.where(identifier: self.identifier)
if self.persisted? && klass == self.class
scope = scope.where('id != ?', self.id)
end
if scope.any?
self.errors.add :identifier, 'is already taken'
break
end
end
end
end
# app/models/product.rb
class Product < ActiveRecord::Base
include ValidateIdentifierUniquenessAcrossModels
end
# app/models/category.rb
class Category < ActiveRecord::Base
include ValidateIdentifierUniquenessAcrossModels
end
Reference:
https://www.krautcomputing.com/blog/2015/03/29/rails-validate-uniqueness-across-models/
UPDATE:
Solution 1 If data are saved at the same time, then the best thing is to include the data saved together in a Transaction as below:
def create
email_validity
#affiliate = Affiliate.new(affiliate_params)
respond_to do |format|
ActiveRecord::Base.transaction do
if verify_recaptcha(model: #affiliate) && #affiliate.save
#user = User.create!(user_parameter)
if #user.valid?
ApplicationMailer.confirmation(#affiliate).deliver_now
format.html {redirect_to :back, notice: 'Thanks for your submission, we will be in touch shortly. Check your email for your affiliate number and password.'}
format.json {render :show, status: :created, location: #affiliate}
else
email_validity = false
raise ActiveRecord::Rollback
end
else
format.html {render :signup, layout: "sign-ups"}
format.json {render json: #affiliate.errors, status: :unprocessable_entity}
end
end
if email_validity == false
respond_to do |format|
format.html {redirect_to :back, notice: 'Sorry, email is in use!'}
# you can add your json return as you like
end
end
end
end
Reference:
http://api.rubyonrails.org/classes/ActiveRecord/Rollback.html
Solution 2 before saving to affiliate or creating a user, check in both tables for email existence in your controller.

How can a user log in a user right after signing up?

I know I should put the code in the create action of the users controller, but I'm not sure what code.
this is my controller code:
# frozen_string_literal: true
class UsersController < ProtectedController
skip_before_action :authenticate, only: [:signup, :signin]
# POST '/sign-up'
def signup
user = User.create(user_creds)
if user.valid?
render json: user, status: :created
else
render json: user.errors, status: :bad_request
end
end
# POST '/sign-in'
def signin
creds = user_creds
if (user = User.authenticate creds[:email],
creds[:password])
render json: user, serializer: UserLoginSerializer, root: 'user'
else
head :unauthorized
end
end
# DELETE '/sign-out/1'
def signout
if current_user == User.find(params[:id])
current_user.logout
head :no_content
else
head :unauthorized
end
end
# PATCH '/change-password/:id'
def changepw
if !current_user.authenticate(pw_creds[:old]) ||
(current_user.password = pw_creds[:new]).blank? ||
!current_user.save
head :bad_request
else
head :no_content
end
end
def index
render json: User.all
end
def show
user = User.find(params[:id])
render json: user
end
def update
head :bad_request
end
private
def user_creds
params.require(:credentials)
.permit(:email, :password, :password_confirmation)
end
def pw_creds
params.require(:passwords)
.permit(:old, :new)
end
private :user_creds, :pw_creds
end
i know i should change something
in my create user but not sure where
i tried to use #current_user = user under the sign up part but it didnt work.
Do you want user to be signed in when he/she sign up?
# POST '/sign-up'
def signup
user = User.create(user_creds)
if user.valid?
User.authenticate(user_creds[:email], user_creds[:password])
render json: user, status: :created
else
render json: user.errors, status: :bad_request
end
end

In Rails, why am I getting a "204 - No Content" response for my update/PATCH/PUT, using Active Model Serializers?

This code is for a UserList (a user can create a User To-Do List). This particular resource does not hold the list items, but just the title of the list, and the type of list.
class Api::V1::UserListsController < ApplicationController
respond_to :json
skip_before_filter :verify_authenticity_token
def index
if authenticate_user
user_lists = #current_user.user_lists
if user_lists
respond_with user_lists, each_serializer: Api::V1::UserListSerializer
else
render json: { error: "Could not find user's lists."}, status: :not_found
end
else
render json: { error: "User is not signed in." }, status: :unauthorized
end
end
def show
if authenticate_user
user_lists = #current_user.user_lists
user_list = user_lists.find_by_id(params[:id])
if user_list
respond_with user_list, serializer: Api::V1::UserListSerializer
else
render json: { error: "Could not find user's list."}, status: :not_found
end
else
render json: { error: "User is not signed in." }, status: :unauthorized
end
end
def create
if authenticate_user
user_list = #current_user.user_lists.new(user_list_params)
if (user_list.save!)
respond_with :api, :v1, #current_user, user_list, serializer: Api::V1::UserListSerializer
else
render json: { error: "Could not create new User List."}, status: :unprocessable_entity
end
else
render json: { error: "User is not signed in." }, status: :unauthorized
end
end
def update
if authenticate_user
user_list = #current_user.user_lists.find_by_id(params[:id])
if (user_list.update_attributes(user_list_update_params))
respond_with :api, :v1, #current_user, user_list, serializer: Api::V1::UserListSerializer
#respond_with user_list, serializer: Api::V1::UserListSerializer
else
render json: { error: "Could not update User List." }, status: :unprocessable_entity
end
end
end
private
def user_list_params
params.require(:user_list).permit(:user_id, :type_id, :title)
end
def user_list_update_params
params.require(:user_list).permit(:type_id, :title)
end
end
Now the update works when I PUT/PATCH... but I get a
Completed 204 No Content in 24ms (ActiveRecord: 4.3ms)
It's been about 4+ months since I've done any rails, and back then I was only just beginning to learn it.
1) Does anyone know why I'm not getting anything back? I know it's something to do with my respond_with line of code in update, but I'm not sure exactly what.
2) Can someone clarify to me the difference between the SHOW respond_with and the CREATE respond_with. I recall having an issue grasping this back then, and obviously now.
SHOW
respond_with user_list, serializer: Api::V1::UserListSerializer
CREATE
respond_with :api, :v1, #current_user, user_list, serializer: Api::V1::UserListSerializer
a) Why does create require :api and :v1 first, but show does not?
b) Why does create require the #current_user, but show does not?
Appendix: Here is my Serializer for reference
class Api::V1::UserListSerializer < ActiveModel::Serializer
attributes :id, :user_id, :type_id, :title
has_many :items, embed: :ids
end
I know this is 2 years too late, but after some digging, I found the empty response with the 204 is intentional (as mentioned above). If you use respond_with this will always be the case. A workaround would be to use render instead (example below):
class Api::V1::ItemsController < ApplicationController
respond_to :json
...
def update
#item = Item.find(params[:id]
if #item
#item.update_attribute(item_params)
render json: #item
end
end
...
end
You're not supposed to get anything back other than the 204. Any intelligent client does not need to receive back the data it just sent you -- it needs only confirmation that the data was persisted.
Do not mistakenly pass your class Api::V1::UserListSerializer as a key/value pair (Hash form). You will get an error including the text class or module needed. It should look like this:
serialize :some_array, Api::V1::UserListSerializer
Or, perhaps clearer would be:
serialize(:some_array, Api::V1::UserListSerializer)
You miss one param and you are rendering an object class with no content : 204 - No Content
That may seem obvious, but it is common to be in the habit of passing things as a key/value pair.
One improve:
before_action :authenticate_user, only: [:create, :show, :update, ...]
https://apidock.com/rails/ActiveRecord/Base/serialize/class
def update
#item = Item.find(params[:id])
respond_with(:api, :v1, #item) do |format|
if #item.update(item_params)
format.json { render json: #item}
else
format.json { render json: {error: #item.errors.full_messages}}
end
end
end

Rails custom validate - uniqueness of a property across models

I'm stuck at defining a custom validation method that's purpose is to verify uniqueness of a property across two models
I realize this is bad code, but i wanted to get the test passing before refactor
here is a model with the custom validation to check another model property, error undefined local variable or method `params' (be gentle I'm still trying to figure out RoR)
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models
def uniqueness_of_a_slug_across_models
#sprocket = Sprocket.where(slug: params[:widget_slug]).first
if #sprocket.present?
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
end
end
You don't have access to params in a model. It belongs to controller and view. What you could do is to call custom method in widgets controller (instead of regular save) in order to pass params to a model:
class WidgetsController < ActionController::Base
def create
#widget = Widget.new(widget_params)
if #widget.save_with_slug_validation(params)
redirect_to widgets_path
else
render :new
end
end
end
and define it:
class Widget < ActiveRecord::Base
# ...
def save_with_slug_validation(params)
sprocket = Sprocket.find_by(slug: params[:widget_slug])
if sprocket
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
save
end
end
I didn't test it but it should work.
P.S. Rails 4 style is used.
UPD
I should have tested it, sorry. Please use another approach.
Widgets controller:
# POST /widgets
# POST /widgets.json
def create
#widget = widget.new(widget_params)
#widget.has_sprocket! if Sprocket.find_by(slug: params[:widget_slug])
respond_to do |format|
if #widget.save
format.html { redirect_to [:admin, #widget], notice: 'widget was successfully created.' }
format.json { render action: 'show', status: :created, location: #widget }
else
format.html { render action: 'new' }
format.json { render json: #widget.errors, status: :unprocessable_entity }
end
end
end
Widget model:
class Widget < ActiveRecord::Base
include Slugable
validates :name, presence: true
validate :uniqueness_of_a_slug_across_models, if: 'has_sprocket?'
def uniqueness_of_a_slug_across_models
errors.add(:uniqueness_of_a_slug_across_models, "can't be shared slug")
end
def has_sprocket!
#has_sprocket = true
end
def has_sprocket?
!!#has_sprocket
end
end
It would be better to move has_sprocket! and has_sprocket? methods and maybe validation itself to Slugable concern.

Resources