I have two tables User (Devise) and Phones
I would like for it when the user signs up threw registrations done with devise, the user_id and the type of phone is loaded into the Phones table
But I am not sure how to do this. I have linked the User with Phones with
User.rb
has_many :phones
Phones.rb
belongs_to :user
db (schema)
t.index ["user_id"], name: "index_phones_on_user_id"
Any help would be appreciated
you can run an after_create callback on User model
after_create :assign_phone
private
def assign_phone
phone = phones.new
# assign required values
phone.save
end
As mentioned in the comments, phone.save will silently return false if there's an error, using !(bang) will raise and error another solution is checking if it is saved or not and then perform task that you want..
def assign_phone
phone = phones.new
# assign required values
if phone.save
#on successfull save
else
#on failure
end
end
in user.rb
after_create :assign_phone
def assign_phone
phone = self.phones.build
phone.save
end
This way you will not be able to add type of phone. However, if you want to save type as well then you have to override "create" method of devise/registrations_controller.rb
Related
I am using devise gem for user management. So, I have a User model.
What I want to be able to do is have a username column in the User model. By default, I want to be able to set the default username for users as 'user'+id where id is unique for every user and a column in User model.
I want to be able to do something like:
class AddColsToUser < ActiveRecord::Migration[6.1]
def change
add_column :users, :username, :string, default: 'user'+id
end
end
Is that possible? Thanks for reading :))
I would recommend using a before_validation callback to create a new username. You can expand this a bit to keep retrying in case of failure, but this should hopefully help you get started.
Make sure to add a unique constraint or validation as well!
before_validation :set_username
private
def set_username
return if user.blank?
username = "{user}_#{rand(100)}"
end
Another option if you just want a prettier parameter in your url's is to override the to_param method in your user model. This will give you a friendly url like "1-my-name", but when parsed to an integer will just be "1" so rails will still find it when doing lookups.
def to_param
[id, name.parameterize].join("-")
end
I need to check if a similar record exist in database before save, if true then update the existing record without saving, else create a new one
In rails 5:
returning false in a hook method doesn't halt callbacks and "throw :abort" is used instead.
the problem is using "throw :abort" rolls back any changes made in the before_save callback.
what I am trying to do is to check for a similar recored in "before_save" and if a similar record exist I need to update the current record and stop saving the new one.
I used
before_save :check
def check
if (similar record exist..)
update current...
return false <==========
end
true
end
but this is not working any more in Rails 5 so returning false doesn't stop it from saving the new record too.
and I tried
before_save :check
def check
if (exist..)
update current...
throw :abort <========
end
true
end
this stops saving current record to db but it perform "rollback" so the updated recored is missed !!
how can I do that ?
I think this is one possible way. This example if with a Product model looking for same name.
before_create :rollback_if_similar_exists
after_rollback :update_existing_record
def rollback_if_similar_exists
throw :abort if Product.exists? name: self.name
end
def update_existing_record
# do here what you need
puts name
puts "find the existing record"
puts "update data"
end
Here is a slightly different approach you could take:
Instead of using before_save, create your own validation and use assign_attributes instead of update or create since assign_attributes won't actually write to the database. You could then call the valid? function on your record to execute your validations. If you get a duplicate record error from the validation you defined, then have your code handle updating the existing record in the logic of your error handling.
Something like this in your controller:
#record.assign_attributes(my_parameter: params[:my_parameter])
if #record.valid?
#record.save
else
puts #record.errors.messages.inspect
#update your existing record instead.
end
Then in your model:
validate :my_validation
def my_validation
if record_already_exists
return errors.add :base, "Your custom error message here."
end
end
I'd recommend using #find_or_initialize_by or #find_or_create_by to instantiate your instances. Instead of placing record swapping code inside a callback. This means you'll do something like this (example controller create):
class Post < ApplicationController
def create
#post = Post.find_or_initialize_by(title: param[:title])
if #post.update(post_params)
redirect_to #post
else
render :new
end
end
end
Pair this with a validation that doesn't allow you to create double records with similar attributes and you're set.
create_table :posts do |t|
t.string :title, null: false
t.text :body
end
add_index :posts, :title, unique: true
class Post < ApplicationRecord
validates :title, presence: true, uniqueness: true
end
I don't recommend the following code, but you could set the id of your instance to match the record with similar data. However you'll have to bypass persistence (keeps track of new and persistent records) and dirty (keeps track of attribute changes). Otherwise you'll create a new record or update the current id instead of the similar record id:
class Post < ApplicationRecord
before_save :set_similar_id
private
def set_similar_id
similar_record = Post.find_by(title: title)
return unless similar_record
#attributes['id'].instance_variable_set :#value, similar_record.id
#new_record = false
end
end
Keep in mind that only changes are submitted to the database when creating a new record. For new records these are only the attributes of which the attributes are set, attributes with value nil are not submitted and will keep their old value.
For existing records theses are the attributes that are not the same as there older variant and the rule old_value != new_value (not actual variable names) applies.
How would I validate a model in the following scenario. We have three models, being account, time log and project. An account has_many projects, and a project belongs_to account.
When a user creates a time log, they are able to select from a list of projects associated with that account, put in some more details, and save the log.
One of our developers has pointed out that it's possible to manipulate the code going back to the controller when a time log is being saved and if you pass the id of a project belonging to another account back to the controller, that project name then becomes visible in a view. In this way you could build a list of other account's projects, which is not cool.
So what I want to achieve is to validate the record being saved to ensure that the project id is actually a project associated with the current_account.
How would I achieve this?
At the moment, this is how I am building the time log
def create
#log = #employee.time_logs.build(params[:employee_time_log])
#log.account_id = current_account.id
if #log.save
flash[:notice] = "Time log sucessfully saved."
redirect_to employee_time_logs_path(#employee)
else
render :form
end
end
and the time log model looks like this
class EmployeeTimeLog < ActiveRecord::Base
#validations
validates :date, presence: true
validates :description, presence: true
#associations
belongs_to :employee
belongs_to :account
belongs_to :company_project
end
You're talking about a case of privilege escalation here.
The Rails Security Guide has some tips about this:
This is alright for some web applications, but certainly not if the user is not authorized to view all projects. If the user changes the id to 42, and they are not allowed to see that information, they will have access to it anyway. Instead, query the user's access rights, too:
#project = #current_user.projects.find(params[:id])
In your case, you want to allow this:
#log = #employee.time_logs.build(project_id: 'good', …)
and disallow this:
#log = #employee.time_logs.build(project_id: 'bad', …)
All projects belonging to an account are queried like so:
current_account.projects
which can be used for further queries:
current_account.projects.find('good')
#=> returns a record because ID belongs to account
current_account.projects.find('bad')
#=> raises ActiveRecord::RecordNotFound
And so that's your way of ensuring you have the right project_id passed to your controller!
user_supplied_project_id = params[:project_id]
timelog_params = params.merge(project_id: current_account.projects.find(user_supplied_project_id))
#log = #employee.time_logs.build(timelog_params)
Thanks for all the help. This was the solution in the end
user_supplied_project_id = params[:employee_time_log][:company_project_id]
timelog_params = user_supplied_project_id == '' ? params[:employee_time_log].merge(company_project_id: '') : params[:employee_time_log].merge(company_project_id: current_account.company_projects.find(user_supplied_project_id).id)
I have Transaction and Account models in my app. Transaction belongs_to :account and Account has_many :transactions.
When user create transaction, app updates balance of transaction's account with
after_create :add_to_account
In Account model I have after_update method, but I need to run it only if user updates account by himself, without transaction creating.
So I need to run this method, only if it was called from accounts_path or unless it was called from transactions_path or Transaction model. How can I implement it with if statement?
Thanks for any help!
UPDATE (if someone has the same problem)
bbozo's method works, but maybe my description of problem wasn't clear: there is no problem with after_create method in Transaction, the problem was with after_update method in Account model. So, the solution is:
class Account
attr_accessor :initiated_by_user
after_update :run_it_only_when_it_was_initiated_by_user
private
def run_it_only_when_it_was_initiated_by_user
if initiated_by_user
..
end
end
end
and in accounts_controller
def update
#account = current_user.accounts.find(params[:id])
#account.initiated_by_user = true
...
end
class Transaction
attr_accessor :initiated_by_user
after_create :add_to_account
private
def add_to_account
if initiated_by_user
..
end
end
end
and then in controller
t = Transaction.new(params[:transaction].permit(foo etc etc))
t.initiated_by_user = true
#...
I was previously adding my current user id to my tracks via the track controller's create method using:
#track.user_id = current_user.id
This worked fine, however, i've now nested tracks within my releases model and am trying to do the same via callbacks in my releases model using:
before_save :add_user_to_tracks
before_update :add_user_to_tracks
def add_user_to_tracks
tracks.each { |t| t.user_id = self.current_user.id}
end
I get an undefined method `current_user' error, however, I know this is very close to working as if I test it using "99" instead of self.current_user.id it adds 99 to the user_id for each track in the db.
Any ideas why I can't access current_user.id
I think you should send user_id from your form using hidden field.
If you don't have user_id in Release model. You can create it as virtual attribute.
class Release < ActiveRecord::Base
attr_accessor :user_id
before_save :add_user_to_tracks
before_update :add_user_to_tracks
def add_user_to_tracks
tracks.each { |t| t.user_id = user_id}
end
end