I'm writing a simple Rails app and I'm wondering what to name a controller that creates accounts.
Some background: Users create a schedule and it's publicly visible. There are users and events. An event can have multiple event_sessions. That's pretty much it.
During registration a user is created, an event is created, and sessions are created. So where do I put this, the UsersController? And, if account creation includes all this other stuff, do I put it in a new controller? If so, what do I call the controller — ExternalSiteController? AccountController?
I would start with something like the following, and tweak as necessary:
class UsersController < ActionController::Base
def create
# ...
User.create(user_params)
# ...
end
end
class User < ActiveRecord::Base
after_create :setup_initial_event
has_many :events
DEFAULT_EVENT_PARAMS = {
# ...
}
def setup_initial_event
events.create(DEFAULT_EVENT_PARAMS)
end
end
class Event < ActiveRecord::Base
after_create :setup_initial_sessions
belongs_to :user
has_many :sessions
def setup_initial_sessions
# You get the idea
end
end
If you don't have an account model (in which case AccountsController would be perfect), I'd put the code in the UsersController. User is probably the most complex and important model of the three (the registration of a user is what's kicking everything off, after all). Of course, you can create any object in any controller (i.e. you can call User.create() in the EventsController).
Related
Feel free to say if you think something is wrong.
I extended Devise Registration controller to create a Profile object to every new user:
class Users::RegistrationsController < Devise::RegistrationsController
def new
resource = build_resource({})
resource.profile = Profile.new
resource.profile.user_id = #user.id
respond_with resource
end
They both are has_one - has_one related and in database:
create_table :profiles do |t|
t.belongs_to :user, index: { unique: true }, foreign_key: true
end
So to get the right profile of current user, I must:
private
def set_profile
#profile = Profile.where(user_id: current_user.id).first
end
And this kinda solves the problem - seems other users cant go around this query and access other profiles (or CAN THEY?), but for other resources I use Pundit to control authorisation, so now it feels a bit messy.
So thats one concern. Other - I still don't know how to act when there is no user logged, because if visiting any restricted resource, this:
private
def set_some_resource
end
end
Throws - "undefined method `id' for nil:NilClass) - how is best to avoid this?
Thanks for any advices.
You may want to start by reading the Rails guides on assocations.
To create a one to one association you use belongs_to on the side with the foreign key column and has_one on the other.
class User
has_one :profile
end
class Profile
belongs_to :user
end
ActiveRecord then automatically links the records together. In general you should avoid setting ids (or getting associated records by ids) explicitly and instead use the assocations:
class Users::RegistrationsController < Devise::RegistrationsController
# ...
def new
# calls Devise::RegistrationsController#new
super do |user|
user.profile.new
end
end
end
Devise is pretty nifty and lets you pass a block to tap into the flow instead of copypasting the whole action.
Simularily you would fetch the current users profile with:
private
def set_profile
#profile = current_user.profile
end
You can set if the callback should be called by using the if: option.
before_action :set_profile, if: :user_signed_in?
But if the action requires authentication you should make sure that it is after :authenticate_user! anyways which will halt the filter chain.
And this kinda solves the problem - seems other users cant go around
this query and access other profiles (or CAN THEY?), but for other
resources I use Pundit to control authorisation, so now it feels a bit
messy.
You don't need to use Pundit to authorize creating a profile or fetching the current users profile. Since the profile is fetched via the user the is no way for another user to access it (well without hacking).
what you might want to authorize is the show, index, edit etc actions if you create a ProfilesController.
I have two models:
Student
Classroom
Both of them have an action that does the same exact thing: it shows a report of daily activity. That is:
/students/1
/classrooms/1
Grabs activity for the model in question and displays it on the page.
In an attempt to dry this up, I created a ReportsController which extracts all the common logic of building a report.
If I leave the routes like this:
/students/1/report
/classrooms/1/report
Then I can have the ReportsController#show action look for params for :student_id or :classroom_id to determine which model type it is dealing with (for purposes of querying the database and rendering the correct view).
But I would prefer the URLs to be cleaner, so I also changed my routes.rb file to pass the show action for these models to the reports#show controller action:
resources :students, :classrooms do
member do
get :show, to: 'reports#show'
end
end
This works, but I can no longer depend on params to identify which model to work with and which view to render.
Question: should I parse request.fullpath for the model? Or is there a better way to make a shared controller understand which model it is working with?
Routing both show methods to the same controller method for code reuse is somewhat like banging a nail in with a dumptruck.
Even if you can find the resource by looking at the request url you would start splitting the ResortsController into a bunch of ifs and switches even before you got off the ground.
One solution is to add the common action in a module:
module Reporting
extend ActiveSupport::Concern
def show
# the Student or Classroom should be available as #resource
render 'reports/show'
end
included do
before_action :find_resource, only: [:show]
end
private
def find_resource
model = self.try(:resource_class) || guess_resource_class
#resource = model.find(params[:id])
end
# This guesses the name of the resource based on the controller name.
def guess_resource_class
self.class.name[0..-11].singularize.constantize
end
end
class StudentController < ApplicationController
include Reporting
end
# Example where resource name cannot be deduced from controller
class PupilController < ApplicationController
include Reporting
private
def resource_class
Student
end
end
self.class.name[0..-11].singularize.constantize is basically how Rails uses convention over configuration to load a User automatically in your UsersController even without any code.
But the most important key to DRY controllers is to keep your controllers skinny. Most functionality can either be moved into the model layer or delegated out to service objects.
I would put the common logic in the Event Model:
#Event Model
class Event < ...
def self.your_event_method
#self here will be either student.events or classroom.events
#depending on which controller called it
end
end
class StudentsController < ...
...
def show
student = Student.find(params[:id])
student.events.your_event_method
end
end
class ClassroomsController < ...
...
def show
classroom = Classroom(params[:id])
classroom.events.your_event_method
end
end
I'm trying to make it so that when a new User is created (through Devise), a new Household(essentially a group) model will be created if no previous Household model with that name exists.
pseudocode:
if Household.find(params[:household_name))
# allow current_user to join household
else
# create new Household model with User's household_name parameter
end
I've overwritten the base user controller from Devise::RegistrationsController with controllers/registerhousehold_controller.rb:
class RegisterhouseholdController < Devise::RegistrationsController
But I'm not sure how to implement the actual creation here. Any suggestions?
No changes in controller required as far as I see.
User.rb
after_create :create_or_join_to_household
def create_or_join_to_household
household = Household.find(params[:household_name])
if household.present?
self.join_to_household
else
Household.create(name: params[:household_name])
#or self.households.create(name: params[:household_name])
#if you have a household - user relation somehow
end
p.s.
join_to_household would be another method in your user model that will create a household_users relation.
Simple - use the before_create callback in the user model to build the object, then you'll be able to use it when you save:
#app/models/user.rb
Class User < ActiveRecord::Base
before_create :set_household, if: Proc.new {|user| user.household_id.present? }
private
def set_household
if house = Household.find(self.household_id)
#if it is set
else
#create a new houshold
end
end
end
I had to call custom method after successful sign up, on my previous task.
U also need something similar.
I'm not sure about overriding.
Try this in App. controller
class ApplicationController < ActionController::Base
def after_sign_in_path_for(resource)
if Household.find(params[:household_name))
# allow current_user to join household
else
#create new Household model with User's household_name parameter
end
root_path
end
end
Check this
I wan't to notify the admin when a user deletes his account. My idea is to use the before_destroy validation to send an email from within the model.
This is probably better done in the controller but I don't want to subclass the devise controller because I think I could break something.
The problem is that the model can't access the current_user, which was my first thought about how to "get" the corresponding user account. But the model has to somehow know which user account is meant to be destroyed, right?, so there has to be some kind of variable being passed.
I've looked into the devise controller at github and it seems like everything is done using "resources".
https://github.com/plataformatec/devise/blob/master/app/controllers/devise/registrations_controller.rb
I can't figure out the last part, how could I access the id / or user object?
Simple add a hook in your model and send the mail..
class User < ActiveRecord::Base
before_destroy :notify_admin
private
def notify_admin
YourMailer.user_destroyed(self).deliver
end
end
The "self" will be your current_user object..
Something like that could work:
class Users::RegistrationsController < Devise::RegistrationsController
def destroy
# You have access to current_user here
super
end
end
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.