How can I save a field as other field in other table? - ruby-on-rails

I have an authentication feature that provides me the username of the current user. I also have a table of events (created by users). And when a user creates an event, how could I save a field called host with the current name of this user in the table events?

The concept is called "deserialization", which is why I made my own gem for this: quickery.
Using quickery
class Event < ApplicationRecord
belongs_to: user
quickery { user: { username: :host } }
end
Using Normal-Way
class Event < ApplicationRecord
belongs_to :user
before_save do
host = user.username
end
end
class User < ApplicationRecord
has_many :events
# you may want to comment out this `after_destroy`, if you don't want to cascade when deleted
after_destroy do
events.update_all(host: nil)
end
# you may want to comment out this `after_update`, if you don't want each `event.host` to be updated (automatically) whenever this `user.username` gets updated
after_update do
events.update_all(host: username)
end
end
Usage Example (for either above)
user = User.create!(username: 'foobar')
event = Event.create!(user: user)
puts event.host
# => 'foobar'

Or, if your Event doesn't belongs_to :user, then you'll need to update this manually in the controller as follows
class EventsController < ApplicationController
def create
#event = Event.new
#event.assign_attributes(event_params)
#event.host = current_user.username
if #event.save
# success, do something UPDATE THIS
else
# validation errors, do something UPDATE THIS
end
end
def update
#event = Event.find(params[:id])
#event.assign_attributes(event_params)
#event.host = current_user.username
if #event.save
# success, do something UPDATE THIS
else
# validation errors, do something UPDATE THIS
end
end
private
def event_params
params.require(:event).permit(:someattribute1, :someattribute2) # etc. UPDATE THIS
end
end

Related

Skip 'has_secure_password' validations based on a condition

I have the following model:
class User < ActiveRecord::Base
has_secure_password
# ...
end
I'm trying to skip validations from has_secure_password helper based on a condition.
So, after searching I found a way in this answer to ALWAYS skip validations, however when I tried to adopt this solution in my case (as I said, I want to skip it based on a condition), as follows:
class User < ActiveRecord::Base
has_secure_password validations: :super_admin?
private
def super_admin?
p "role #{role.inspect}"
role == 'super_admin'
end
end
... it doesn't skip the validation. It doesn't even call the super_admin? method.
Thanks in advance.
for your case, the one that you need to skip is user.authenticate when you creating session for the user
below is sample of creating sesion with logical or if user.super_admin? equal true then it will pass and user can login
class SessionsController < ApplicationController
def create
user = User.find_by_username(params[:session][:username])
if user && (user.super_admin? || user.authenticate(params[:session][:password]))
login user
flash[:success] = "Successfully login"
redirect_to users_path
else
flash.now[:error] = 'sorry cannot login'
render 'new'
end
end
end
in user.rb, remove private and check as follow
class User < ActiveRecord::Base
def super_admin?
p "role #{role.inspect}"
role == 'super_admin'
end
end

Rails accepts_nested_attributes_for always creates the nested models, but does not update them

Given the following:
class WebsitesController < ApplicationController
# POST /websites/save
# POST /websites/save.json
def save
Website.exists?(name: params[:website][:name]) ? update : create
end
# POST /websites
# POST /websites.json
def create
#server = Server.find_or_create_by_name(params[:server_id])
#website = #server.websites.new(params[:website])
#etc... #website.save
end
# PUT /websites/1
# PUT /websites/1.json
def update
#website = Website.find_by_name(params[:website][:name])
#etc... #website.update_attributes
end
end
The client does not have any IDs of these models
The request that gets sent only has the names, but not the ids.
And the following models
class Website < ActiveRecord::Base
serialize :website_errors
attr_accessible :plugins_attributes
has_many :plugins
accepts_nested_attributes_for :plugins
end
class Plugin < ActiveRecord::Base
belongs_to :website
end
When I make a POST request to /websites/save.json, the Website gets updated correctly if it exists, but the Plugins that belong to it always get recreated causing duplicate content in the Database. Why does this happen? I redirect to the update action which calls update_attributes so how can it be that it does not update it? I take it that it's because no ID is given with the request.
Can I make the Controller listen to plugin_name instead of plugin_id?
Modify your controller to have this:
def update
#website = Website.find_by_name(params[:website][:name])
if #website.update(params)
redirect_to website_path(#website)
else
render :edit
end
end
Also, if you're using strong_parameters, you'll need this at the bottom of your controller:
params.require(:website).
permit(
:name,
...,
plugins_attributes: [
:name,
...,
]
)
end

New object belonging_to won't associate with Devise User who created it

When my user creates an object that should have his id on it, the id comes out nil. Ideas why?
# controllers/units_controller.rb
class UnitsController < ApplicationController
before_filter :authenticate_user!
def new
#unit = Unit.new
#unit.user = current_user # also tried .user_id = current_user.id
[...]
# models/unit.rb
class Unit < ActiveRecord::Base
belongs_to :user
# :user has_many :units too
[...]
The object is saved successfully, but with an empty user_id field (showing nil).
No exception is thrown. There is no callback on save.
If you're doing this in the standard Rails two step (#new (POST)-> #create), then you need to associate Unit with current_user in the #create action.
def create
#unit = Unit.new
#unit.user = current_user
# ...
end

Using the save method together with update_attributes. Is this common?

A user can sign up as an artist. All the user needs to do now, is provide his email.
In Artist controller, def create. Is it normal to have something like:
def create
#artist = current_user
respond_to do |format|
if #artist.update_attributes(params[:user]) # params[:user] contains email
#artist.is_artist = true
#artist.save
....
In my User model, I have:
attr_accessible :email
Which means, I can't simply do #artist.update_attributes(:is_artist => true). I would have to use the save method instead. Is this type of approach common? Or is there a better way?
You can define before_create method in your model:
class User < ActiveRecord::Base
...
before_create :fill_fields
def fill_fields
is_artist = true
end
end
I would do the following:
1st: I wound not set up an ArtistController if you do not have an Artist Model. rather I would add a non-restful method in your UserController, and push the implemention logic into the model ...
# config/routes.rb
resources :users do
member {post 'signup_as_artist'}
end
# UserController
def signup_as_artist
#user = User.find(params[:id])
#user.signup_as_artist
end
# User
def signup_as_artist
self.update_attribute :is_artist, true
end
Good luck

Rails Authlogic authentication method

Within Authlogic, is there a way that I can add conditions to the authentication method? I know by using the find_by_login_method I can specify another method to use, but when I use this I need to pass another parameter since the find_by_login_method method only passes the parameter that is deemed the 'login_field'.
What I need to do is check something that is an association of the authentic model.. Here is the method I want to use
# make sure that the user has access to the subdomain that they are
# attempting to login to, subdomains are company names
def self.find_by_email_and_company(email, company)
user = User.find_by_email(email)
companies = []
user.brands.each do |b|
companies << b.company.id
end
user && companies.include?(company)
end
But this fails due to the fact that only one parameter is sent to the find_by_email_and_company method.
The company is actually the subdomain, so in order to get it here I am just placing it in a hidden field in the form (only way I could think to get it to the model)
Is there a method I can override somehow..?
Using the answer below I came up with the following that worked:
User Model (User.rb)
def self.find_by_email_within_company(email)
# find the user
user = self.find_by_email(email)
# no need to continue if the email address is invalid
return false if user.nil?
# collect the subdomains the provided user has access to
company_subdomains = user.brands.map(&:company).map(&:subdomain)
# verify that the user has access to the current subdomain
company_subdomains.include?(Thread.current[:current_subdomain]) && user
end
Application Controller
before_filter :set_subdomain
private
def set_subdomain
# helper that retreives the current subdomain
get_company
Thread.current[:current_subdomain] = #company.subdomain
end
User Session Model (UserSession.rb)
find_by_login_method :find_by_email_within_company
I have read a few things about using Thread.current, and conflicting namespaces.. This is a great solution that worked for me but would love to hear any other suggestions before the bounty expires, otherwise, +100 to Jens Fahnenbruck :)
Authlogic provides API for dealing with sub domain based authentication.
class User < ActiveRecord::Base
has_many :brands
has_many :companies, :through => :brands
acts_as_authentic
end
class Brand < ActiveRecord::Base
belongs_to :user
belongs_to :company
end
class Company < ActiveRecord::Base
has_many :brands
has_many :users, :through => :brands
authenticates_many :user_sessions, :scope_cookies => true
end
Session controller:
class UserSessionsController < ApplicationController
def create
#company = Company.find(params[:user_session][:company])
#user_session = #company.user_sessions.new(params[:user_session])
if #user_session.save
else
end
end
end
On the other hand
Here is a way to solve the problem using your current approach(I would use the first approach):
Set custom data - to the key email of the hash used to create the UserSession object.
AuthLogic will pass this value to find_by_login method. In the find_by_login method access the needed values.
Assumption:
The sub domain id is set in a field called company in the form.
class UserSessionsController < ApplicationController
def create
attrs = params[:user_session].dup #make a copy
attrs[:email] = params[:user_session] # set custom data to :email key
#user_session = UserSession.new(attrs)
if #user_session.save
else
end
end
end
Model code
Your code for finding the user with the given email and subdomain can be simplified and optimized as follows:
class User < ActiveRecord::Base
def find_by_email params={}
# If invoked in the normal fashion then ..
return User.first(:conditions => {:email => params}) unless params.is_a?(Hash)
User.first(:joins => [:brands => :company}],
:conditions => ["users.email = ? AND companies.id = ?",
params[:email], params[:company]])
end
end
Edit 1
Once the user is authenticated, system should provide access to authorized data.
If you maintain data for all the domains in the same table, then you have to scope the data by subdomain and authenticated user.
Lets say you have Post model with company_id and user_id columns. When a user logs in you want to show user's posts for the sub domain. This is one way to scope user's data for the subdomain:
Posts.find_by_company_id_and_user_id(current_company, current_user)
Posts.for_company_and_user(current_company, current_user) # named scope
If you do not scope the data, you will have potential security holes in your system.
In your lib folder add a file with the follwing content:
class Class
def thread_local_accessor name, options = {}
m = Module.new
m.module_eval do
class_variable_set :"###{name}", Hash.new {|h,k| h[k] = options[:default] }
end
m.module_eval %{
FINALIZER = lambda {|id| ###{name}.delete id }
def #{name}
###{name}[Thread.current.object_id]
end
def #{name}=(val)
ObjectSpace.define_finalizer Thread.current, FINALIZER unless ###{name}.has_key? Thread.current.object_id
###{name}[Thread.current.object_id] = val
end
}
class_eval do
include m
extend m
end
end
end
I found this here
Then add code in the controller like this:
class ApplicationController < ActionController
before_filter :set_subdomain
private
def set_subdomain
User.subdomain = request.subdomains[0]
end
end
And now you can do the following in your user model (assuming your company model has a method called subdomain:
class User < ActiveRecord::Base
thread_local_accessor :subdomain, :default => nil
def self.find_by_email_within_company(email)
self.find_by_email(email)
company_subdomains = user.brands.map(&:company).map(&:subdomain)
company_subdomains.include?(self.subdomain) && user
end
end
And FYI:
companies = user.brands.map(&:company).map(&:subdomain)
is the same as
companies = []
user.brands.each do |b|
companies << b.company.subdomain
end
With rails 3 you can use this workaround:
class UserSessionsController < ApplicationController
...
def create
#company = <# YourMethodToGetIt #>
session_hash = params[:user_session].dup
session_hash[:username] = { :login => params[:user_session][:username], :company => #company }
#user_session = UserSession.new(session_hash)
if #user_session.save
flash[:notice] = "Login successful!"
redirect_back_or_default dashboard_url
else
#user_session.username = params[:user_session][:username]
render :action => :new
end
...
end
Then
class UserSession < Authlogic::Session::Base
find_by_login_method :find_by_custom_login
end
and
class User < ActiveRecord::Base
...
def self.find_by_custom_login(hash)
if hash.is_a? Hash
return find_by_username_and_company_id(hash[:login], hash[:company].id) ||
find_by_email_and_company_id(hash[:login], hash[:company].id)
else
raise Exception.new "Error. find_by_custom_login MUST be called with {:login => 'username', :company => <Company.object>}"
end
end
...
end
Which is quite plain and "correct". I take me a lot of time to find out, but it works fine!

Resources