Rails 4: custom validations with external records - validation

Hi I have a model called PurchasingGroup, a purchasing group has many Goals.
Goal model has 2 attributes: no_of_users and discount
I need to validate that the goals are consecutive, for example if I create a Goal with no_of_users = 10 and discount = 15 then the next goal I create must have greater values, otherwise I have to show the error to the user, right now Im making the validation in the create action of the controller, I know it is a bad practice so I want to know how to create this validation, I could not achieved it using custom validations in the model level.
I need to access the purchasing group and then check if the last group goal values are greater than or equal to the values of the new goal:
Below is the validation I have in the controller, it works but I want to do it right:
def create
respond_to do |format|
#purchasing_group = PurchasingGroup.find params[:purchasing_group_id]
#goal = Goal.new goal_params
#error_messages = ""
if not #purchasing_group.goals.empty?
if #purchasing_group.goals.last.no_of_users >= #goal.no_of_users
#error_messages = "The goals are consecutive! No. Users: must be greater than the previous goal value"
end
if #purchasing_group.goals.last.discount >= #goal.discount
#error_messages = "#{#error_messages}\nThe goals are consecutive! discount: must be greater than the previous goal value"
end
end
#if there are no errors then we save the object
if #error_messages.empty?
if #goal.save
#goal.update_attributes purchasing_group_id: params[:purchasing_group_id]
end
end
#In a js template I handle the errors, that is not relevant for this question.
format.js
end
end

if I understood you right, then:
validate :count_no_of_users
private
def count_no_of_users
last_goal = PurchasingGroup.find(self.purchasing_group_id).goals.last
error(:count_no_of_user, "Should be more than #{last_goal.no_of_user}") if self.no_of_user < last_goal.no_of_user
end
and same for discount
you can validate it in single or separate validations.

Related

Odoo tree view only show one record with compute

I'm trying to display a user signed documents (from the "Sign app") on his page, so I added this to the inherited model:
x_signatures_relation = fields.One2many("signature.request.item", "partner_id")
x_signatures = fields.One2many("signature.request", compute="_get_signed_documents")
#api.one
def _get_signed_documents(self):
ids = []
for signature in self.x_signatures_relation:
ids.append(signature.signature_request_id)
self.x_signatures = ids
"signature.request.item" is the table relating the partner (user) with "signature.request" the actual signature.
However this return an empty view even though the current user has two signatures, but if I replace :
self.x_signatures = ids
with :
self.x_signatures = ids[0]
or :
self.x_signatures = ids[1]
It displays the record, so what's going on ?
Odoo has a very specific set of rules about how you are "allowed" to manipulate One2many and Many2Many fields.
See my recent answer, which gives a detailed explanation of all options and when/how to use them. The Odoo documentation also explains it as well.
In your case, you are setting the value in a compute method, so you want to completely replace any existing values.
# Instead of
# self.x_signatures = ids
# Try this, which uses the special number 6 to mean
# "replace any existing ids with these ids"
self.x_signatures = [(6, 0, ids)]
Furthermore, you could simplify your compute method:
#api.one
def _get_signed_documents(self):
self.x_signatures = [(6, 0, self.x_signatures_relation.ids)]

How to update attributes while doing a duplicate Rails

We are creating a scheduling app and attempting to create a copy/paste function for schedules from week to week. I am trying to figure out how to duplicate a schedule for certain period of time, while updating the attributes upon paste. Right now I can copy the schedule, but when running it on postman, the dates and times stay the exact same (as we would expect with a .dup.) I believe it would be best to set the start/end times to nil and then upon paste maybe the attributes get updated at that time?
Here is the function I have so far:
def copy
set_calendar
if params["start_date"] && params["end_date"]
start_date = params["start_date"].to_date
end_date = params["end_date"].to_date
if #calendar.users.owners.include?(current_user) || #calendar.users.managers.include?(current_user)
#past_shifts = Shift.where(calendar_id: #calendar.id, start_time: start_date.beginning_of_day .. end_date.end_of_day).to_a
if
#past_shifts.each do |past_shift|
shift = past_shift.dup
shift.users = past_shift.users
shift.update(shift_params)
shift.save
end
render json: "copied", status: :ok
else
render json: #usershift.errors, status: :uprocessable_entity
end
else
render json: ("You do not have access to copy shifts"), status: :unauthorized
end
end
end
The shift.update(shift_params) is the part that needs to update the start and end times. Here are the shift params:
def shift_params
params.permit(:start_time, :end_time, :calendar_id, :capacity, :published)
end
As far as relationship set ups, this current method is being created in the shifts controller. Shift has many users through usershifts, user has many shifts through usershifts, and usershift model belongs to both.
Just curious - are you sure params for your copy method contains values for start_time and end_time? If so, why not to use them directly:
shift.start_time = params['start_time']
shift.end_time = params['end_time']
With use shift_params you will also update other 3 attributes: :calendar_id, :capacity, :published. Not sure if this is necessary in this case.
Using shift.update method, in this case, is not reasonable. It works with existing record and saves updated attributes to the database. In your case the record is new, and you save all the changes with calling shift.save later.

Code architecture - Flask - Where to put form validation from database?

I'm wondering where should I put a validation form which accessing database.
Basically I will need user to enter item_type and I want to check first i the item_type has exist in database.
There are 3 options:
In the database model, I have ItemType class and I put function add() which will check if existing item has exist or not
In view, so in the route of the page, from wtforms form.validate_on_submit(), I do a check to get data from database, and if exist I will put error in here
In wtforms validate(), adding extra validation after default validation of the Form class
I've seen people using number 2 and 3, but not sure which one is the best. The error message that I want will also need to display it on the specific field of the form (this is achieveable by method 2 and 3 since they have reference to the form field) but then again since it is related to accessing database, maybe it is better to put everything regarding database access to model functions?
In my opinion, if it comes from a form then it should be validated on that form and then raise an error for that specific field when invalid. See the example bellow:
class SigninForm(Form):
"""Form for signin"""
email = StringField('Email',
validators=[
DataRequired("Email shouldn't be empty."),
Email('Email format is not correct.')
])
password = PasswordField('Password',
validators=[DataRequired("Password shouldn't be empty.")])
def validate_email(self, field):
"""
verify if account exists and if not raise an error in
that field.
"""
user = User.query.filter(User.email == self.email.data).first()
if not user:
raise ValueError("Account doesn't exist.")
def validate_password(self, field):
"""
Verify if password is valid and if not raise an error in
that field.
"""
if self.email.data:
user = User.query.filter(User.email == self.email.data).first()
if not user or not user.check_password(self.password.data):
raise ValueError('Password is not correct.')
else:
self.user = user
The view function for this example:
#app.route('/signin', methods=['GET', 'POST'])
def signin():
"""Signin"""
form = SigninForm()
if form.validate_on_submit():
# sign in function to register user into a session
signin_user(form.user)
return redirect(url_for('site.index'))
return render_template('account/signin/signin.html', form=form)

How can I validate a DateTime field

I had two fields available_to and available_from in a model called Hotel. When creating a new hotel, we got to specify the date and time! I want to validate these two fields, in such a way that, if one selects available_to as 23/2/2015 , the available_from should not be 23/2/2014.
The fastest way is creating a custom validation method within model itself.
Try updating Hotel as follows:
class Hotel < ActiveRecord::Base
validate :validate_available_to if Proc.new { |h| h.available_from && h.available_to }
def validate_available_to
return if available_to > available_from
errors.add(:available_to, "Date should be at least #{available_from.next.strftime("%d/%m/%Y")}")
end
end
How to use
h = Hotel.new
h.available_from = Date.parse("22/2/2014")
h.available_to = Date.parse("22/2/2014")
h.valid?
# => false
h.errors[:available_to]
# => ["Date should be at least 23/02/2014"]
However, when you're extending you validation rules, and code gets complicated, you should consider moving away validation from Model to separate Validation classes.
Check validates_with, with ActiveModel::Validator for more.
Hope that helps!

Rails 4 named scope with record always at end

Is there a way to order records alphabetically, excluding one record, which I want at the end?
class Category < ActiveRecord::Base
default_scope -> { order(:name) }
end
I always want the category named 'Other' to be at the end.
You can try the below ActiveRecord query
Category.order("CASE WHEN name = 'Other' then 0 else 1 END DESC, name ASC")
This is a little bit tricky. In SQL you can add CASE statements to your ORDER BY. In your case the SQL would be something similar to.
SELECT *
FROM categories
ORDER BY
(
CASE
WHEN name = 'Other' THEN 1
ELSE 0
END
)
Here's a live example.
As far as I know, the ActiveRecord order method accepts arbitrary string, so you could (not tested) be able to pass the case to the method
Category.order("CASE WHEN name = 'Other' ... ")
This approach seems complicated, but if you can get it to work is by far the most efficient.
The second alternative is to play a little bit with ActiveRecord.
class Category < ActiveRecord::Base
def self.ordered(condition = nil)
# Get the ID of the record to exclude
excluded = self.where(name: 'Other').pluck(:id).first
# order the records and collect the ids
ids = where('id <> ?', excluded).order(condition).pluck(:id)
# append the excluded at the end
ids << excluded
# recreate the scope and return it.
where(id: ids)
end
end
Category.where(...).ordered
Generally speaking, I encourage you to avoid default_scopes in ActiveRecord. It's so easy to add them, but very hard to remove them when you need.
I would recommend just to add an additional field "position" and sort everything by it. Or you can order by 2 fields (position, name). All Categories will have position equal = 0, and "Other" equal to 999 for example

Resources