Why do you have to explicitly specify scope with friendly_id? - ruby-on-rails

I'm using the friendly_id gem. I also have my routes nested:
# config/routes.rb
map.resources :users do |user|
user.resources :events
end
So I have URLs like /users/nfm/events/birthday-2009.
In my models, I want the event title to be scoped to the username, so that both nfm and mrmagoo can have events birthday-2009 without them being slugged.
# app/models/event.rb
def Event < ActiveRecord::Base
has_friendly_id :title, :use_slug => true, :scope => :user
belongs_to :user
...
end
I'm also using has_friendly_id :username in my User model.
However, in my controller, I'm only pulling out events pertinent to the user who is logged in (current_user):
def EventsController < ApplicationController
def show
#event = current_user.events.find(params[:id])
end
...
end
This doesn't work; I get the error ActiveRecord::RecordNotFound; expected scope but got none.
# This works
#event = current_user.events.find(params[:id], :scope => 'nfm')
# This doesn't work, even though User has_friendly_id, so current_user.to_param _should_ return "nfm"
#event = current_user.events.find(params[:id], :scope => current_user)
# But this does work!
#event = current_user.events.find(params[:id], :scope => current_user.to_param)
SO, why do I need to explicitly specify :scope if I'm restricting it to current_user.events anyway? And why does current_user.to_param need to be called explicitly? Can I override this?

I had the exact same problem with friendly_id 2.2.7 but when I updated to friendly_id 3.0.4 in my Rails 2.3.5 app, everything works. I have test all 4 find invocations you mentioned in my app and they work.
Something to take note of are a few API changes that may affect you. The ones I ran into were:
:strip_diacritics has been replaced with :strip_non_ascii.
I decided to switch to Stringex's String#to_url instead by overriding normalize_friendly_id
resource.has_better_id? is now !resource.friendly_id_status.best?
resource.found_using_numeric_id? is now resource.friendly_id_status.numeric?

Related

Why the :attribute_check in Declarative_Authorization don't found ID's

I have a problem since many days with Declarative_Authorization on Ruby on Rails 3.1.
I add this on the top of each controller :
class UsersController < ApplicationController
# Verifies that the user is connected and/or authorized to access the methods
filter_access_to :all, :attribute_check => true
(...)
end
It allows me to have this on my authorization_rules.rb:
has_permission_on [:albums], :to => [:adding] do
if_attribute :group => { :user => is { user } }
end
But when I add :attribute_check => true I get this error :
Couldn't find {Model} without an ID
I'm totally lost, I was starting with this gem.
Your user variable is null maybe, so find the error why its null
I found the trick, just add this on the top of the controller :
class Group
before_filter :set_group
(...)
protected
def set_group
#group = Group.find_by_slug(params[:slug])
end
end

Devise with Associated Record validation

My background
I am/was a PHP developper. Have been for 15 years. Ruby is new to me (My new challenge)!
Current Setup
I am using Devise with a User model.
Rails: 3.2.1
Devise: 2.1.2
Use Case
When the user registers (going thru Devise controller), I want to create the User record but also a Foo record automatically. I created an after_create which handles the creation of the Foo record.
Class User < ActiveRecord::Base
after_create :make_foo
def make_foo
Foo.create(
:name => name,
:user_id => id
)
end
end
Class Foo < ActiveRecord::Base
belongs_to :user
end
Symptoms
I had a problem where when the Foo record was not being created (validation for example), then the User record was still created (I did not want that). I added a Raise Exception in after_create which rolls back the User creation.
However, I would prefer some nice error handling rather than Exception being throwed. Right now I get a 500 Error page with that Exception.
I would prefer that the form can be shown again with the reason(s) of the failure.
Class User < ActiveRecord::Base
after_create :make_foo
def make_foo
foo = Foo.create(
:name => name,
:user_id => id
)
if !foo.valid?
raise Exception.new('Foo creation failed.')
end
end
end
Plea for help
Any suggestions?
Instead of raising an exception you can redirect back to same page with setting flash message in
if !foo.valid?
block like this
flash[:error] = 'error msg'
and redirect using
session[:return_to] = request.referer
redirect_to session[:return_to]
I ended up overriding the Devise Resitrations Controller and putting a begin...rescue...end inside the create method.
# routes.rb
devise_for :users, :controllers => { :registrations => "my_devise/registrations" }
# app/controllers/my_devise/registrations_controller.rb
class MyDevise::RegistrationsController < Devise::RegistrationsController
def create
begin
super
rescue Exception
self.resource.errors[:base] << "My error message here"
clean_up_passwords resource
respond_with resource
end
end
end
You might want to look at Rails3: Devise User has_one relationship to see if better modelling can make the problem easier.
The way you are modelling user.rb now, is indeed such that a User may exist without Foo (which must belong to a User however), so it just calls :make_foo as an after_create callback with no other guarantees whatsoever.

Devise remove a user but keep his data, and set a flag _is_deleted in rails 3.1

I would like to delete a user with devise but be able to save its data just setting a flag like is_deleted to true and prevent login for those users.
What would be the best way to do this in devise ? I have seen some write-ups on this but they were for rails 2.x projects, Im on rails 3.1
If you want to prevent sign_in users whose deleted_at fields are not null, override active_for_authentication? on your devise resource model:
def active_for_authentication?
super && !deleted_at
end
You can set that deleted flag normally then override the find_for_authentication class level method in the user model.
The following should work
def self.find_for_authentication(conditions)
super(conditions.merge(:is_deleted => false))
end
Another approach is to use a default scope on your model.
Define a state on your User model, and add a default scope (Rails 3), this will scope all the queries on the User model with the condition from the scope:
app/models/user.rb
class User < ActiveRecord::Base
default_scope where("state != 'disabled'")
def disable!
self.update_attribute(:state, 'disabled')
end
end
Then, over-write the destroy method in your session controller, make sure you to grab the destroy code from the version of devise you're using:
*app/controllers/registrations_controller.rb*
class Users::RegistrationsController < Devise::RegistrationsController
# paranoid DELETE /resource
def destroy
resource.disable! # we don't remove the record with resource.destroy
Devise.sign_out_all_scopes ? sign_out : sign_out(resource_name)
set_flash_message :notice, :destroyed if is_navigational_format?
respond_with_navigational(resource){ redirect_to after_sign_out_path_for(resource_name) }
end
end
You can take it a step further by defining a state machine on your User model (be careful of how this will not cascade down the dependency tree, like a :dependent => :destroy would):
app/models/user.rb
class User < ActiveRecord::Base
include ActiveRecord::Transitions
state_machine do
state :passive
state :active
state :disabled, :enter => :bye_bye_user
event :activate do
transitions :from => :passive, :to => :active
end
event :disable do
transitions :from => [:passive,:active], :to => :disabled
end
end
default_scope where("state != 'disabled'")
end
#dgmdan: In regards to using :deleted_at => nil instead of false:
Devise's find_for_authentication method runs the conditions through a filter which stringifies values. What's happening is that the nil value being passed in for deleted_at is being converted to an empty string. This makes the query match no one, and thus to the end user it looks like the username and password were incorrect.
find_for_authentication calls find_first_by_auth_conditions like this:
def find_for_authentication(tainted_conditions)
find_first_by_auth_conditions(tainted_conditions)
end
Per the author, find_first_by_auth_conditions takes an optional second parameter, another conditions hash, but this one does not go through the filter. So what you can do is change the method like this:
def self.find_for_authentication(conditions)
find_first_by_auth_conditions(conditions, {:deleted_at => nil})
end
The second conditions hash with the :deleted_at => nil should be passed straight through to the ORM layer.

Is there a plugin or gem that can help me do "invite a friend" capability in rails?

I want to add the ability for users to invite a friend.
The email should be generated so that, if someone clicks on the link and register, that person is automatically a friend.
Not sure what the options are, but wanted some ideas and strategies as an alternative to building it from scratch.
I'm not aware of any gems that handle the entire process (user >> email >> signup). If you're just looking to create the relationship when a user comes from a specific link, create a special invitation route (the separate controller isn't necessary but just to make it clear):
# routes.rb
match '/invite/:friend_id' => 'public#invite', :as => :invite
# PublicController
def invite
session[:referring_friend] = params[:friend_id]
redirect_to root_path
end
# UsersController
def create
#user = User.new(params[:user])
if #user.save
#user.create_friendship(session[:referring_friend]) if session[:referring_friend]
...
else
...
end
end
If you want to track conversion metrics, I'd recommend creating a link model and using that to track clicks and signups:
class Link < ActiveRecord::Base
belongs_to :user
attr_accessible :user, :user_id, :clicks, :conversions
def click!
self.class.increment_count(:clicks, self.id)
end
def convert!
self.class.increment_count(:conversions, self.id)
end
end
# routes.rb
match '/invite/:link_id' => 'links#hit', :as => :invite
# LinksController
def hit
link = Link.find(params[:link_id])
link.click!
session[:referring_link_id] = link.id
redirect_to root_path # or whatever path (maybe provided by link...)
end
# UsersController
def create
#user = User.new(params[:user])
if #user.save
if session[:referring_link_id]
link = Link.find(session[:referring_link_id])
link.convert!
#user.create_friendship(link.user_id)
end
...
else
...
end
end
Which method you choose depends on what you'll want to track down the road.
I don't know gem for rails. But there's an extension for Spree, rails based e-commerce project. Check it out & probably you can refer how it's implemented.
https://github.com/spree/spree_email_to_friend
I don't know about some gem to support this, but solution should be rather trivial. I guess you need Friendship model, you can place some status in it like 'waiting_for_approvment' and send in mail link with that Friendship model id. When user accepts either way you just change status to 'approved' or even 'rejected' if you want to track that too.
Start by defining the relationship:
class User < ActiveRecord::Base
has_and_belongs_to_many :friends, :class_name => "User", :join_table => "friends_users"
end
So really, User relates to itself with a different name. Then you can use something along the lines of:
#current_user.friends << #selected_user
in your controller.

How do I create an admin subdomain to manage subdomains in Rails

I am using AuthLogic and the subdomain method that dhh covered in this blog post, everything is working great, and as expected. What I'm trying to figure out is how to create a subdomain like 'admin' or 'host' that will have a user authenticated from AuthLogic (this may be trivial and unnecessary to mention) that will manage the subdomains. So basically, all subdomains will act normally, except admin.site.com which will go to its own controller and layout..
dhh suggested just throwing in an exception to redirect, but I'm not sure where that goes, it didnt seem that simple to me, any ideas?
EDIT
I think that the fact I am using AuthLogic is important here, because the subdomain logic isnt forwarding users anywhere, once authenticated AuthLogic sends the user to /account - so my question may be related to how do I tell AuthLogic to a different spot if the user is a root user, logging into the admin subdomain..
Here is the code we have implemented thus far
Company Model
class Company < ActiveRecord::Base
has_many :users
has_many :brands, :dependent => :destroy
validates_presence_of :name, :phone, :subdomain
validates_format_of :subdomain, :with => /^[A-Za-z0-9-]+$/, :message => 'The subdomain can only contain alphanumeric characters and dashes.', :allow_blank => true
validates_uniqueness_of :subdomain, :case_sensitive => false
validates_exclusion_of :format, :in => %w( support blog billing help api www host admin manage ryan jeff allie), :message => "Subdomain {{value}} is not allowed."
before_validation :downcase_subdomain
protected
def downcase_subdomain
self.subdomain.downcase! if attribute_present?("subdomain")
end
end
SubdomainCompanies Module
module SubdomainCompanies
def self.included( controller )
controller.helper_method(:company_domain, :company_subdomain, :company_url, :company_account, :default_company_subdomain, :default_company_url)
end
protected
# TODO: need to handle www as well
def default_company_subdomain
''
end
def company_url( company_subdomain = default_company_subdomain, use_ssl = request.ssl? )
http_protocol(use_ssl) + company_host(company_subdomain)
end
def company_host( subdomain )
company_host = ''
company_host << subdomain + '.'
company_host << company_domain
end
def company_domain
company_domain = ''
company_domain << request.domain + request.port_string
end
def company_subdomain
request.subdomains.first || ''
end
def default_company_url( use_ssl = request.ssl? )
http_protocol(use_ssl) + company_domain
end
def current_company
Company.find_by_subdomain(company_subdomain)
end
def http_protocol( use_ssl = request.ssl? )
(use_ssl ? "https://" : "http://")
end
end
Application Controller
class ApplicationController < ActionController::Base
include SubdomainCompanies
rescue_from 'Acl9::AccessDenied', :with => :access_denied
helper :all # include all helpers, all the time
protect_from_forgery # See ActionController::RequestForgeryProtection for details
helper_method :current_user_session, :current_user, :current_company_name
filter_parameter_logging :password, :password_confirmation
before_filter :check_company_status
protected
def public_site?
company_subdomain == default_company_subdomain
end
def current_layout_name
public_site? ? 'public' : 'login'
end
def check_company_status
unless company_subdomain == default_company_subdomain
# TODO: this is where we could check to see if the account is active as well (paid, etc...)
redirect_to default_company_url if current_company.nil?
end
end
end
Look into subdomain-fu which allows you to route to different controllers and actions based on the subdomain. I have done a Railscasts Episode on the subject.
It might looks something like this.
# in routes.rb
map.manage_companies '', :controller => 'companies', :action => 'index', :conditions => { :subdomain => "admin" }
This will need to be high enough up in the routes list so nothing else is matched before it.
FOR RAILS 2.3: You can download a full example app (with a step-by-step tutorial) showing how to implement an admin subdomain, a main domain, and multiple user subdomains using the Devise gem for authentication and the subdomain-routes gem for managing subdomains. Here's the link: subdomain authentication for Rails 2.3.
FOR RAILS 3: Here's a complete example implementation of Rails 3 subdomains with authentication (along with a detailed tutorial). It's much easier to do this in Rails 3 than in Rails 2 (no plugin required).

Resources