Create Rails model with argument of associated model? - ruby-on-rails

I have two models, User and PushupReminder, and a method create_a_reminder in my PushupReminder controller (is that the best place to put it?) that I want to have create a new instance of a PushupReminder for a given user when I pass it a user ID. I have the association via the user_id column working correctly in my PushupReminder table and I've tested that I can both create reminders & send the reminder email correctly via the Rails console.
Here is a snippet of the model code:
class User < ActiveRecord::Base
has_many :pushup_reminders
end
class PushupReminder < ActiveRecord::Base
belongs_to :user
end
And the create_a_reminder method:
def create_a_reminder(user)
#user = User.find(user)
#reminder = PushupReminder.create(:user_id => #user.id, :completed => false, :num_pushups => #user.pushups_per_reminder, :when_sent => Time.now)
PushupReminderMailer.reminder_email(#user).deliver
end
I'm at a loss for how to run that create_a_reminder method in my code for a given user (eventually will be in a cron job for all my users). If someone could help me get my thinking on the right track, I'd really appreciate it.
Thanks!

Edit: I've posted a sample Rails app here demonstrating the stuff I'm talking about in my answer. I've also posted a new commit, complete with comments that demonstrates how to handle pushup reminders when they're also available in a non-nested fashion.
Paul's on the right track, for sure. You'll want this create functionality in two places, the second being important if you want to run this as a cron job.
In your PushupRemindersController, as a nested resource for a User; for the sake of creating pushup reminders via the web.
In a rake task, which will be run as a cron job.
Most of the code you need is already provided for you by Rails, and most of it you've already got set in your ActiveRecord associations. For #1, in routes.rb, setup nested routes...
# Creates routes like...
# /users/<user_id>/pushup_reminders
# /users/<user_id>/pushup_reminders/new
# /users/<user_id>/pushup_reminders/<id>
resources :users do
resources :pushup_reminders
end
And your PushupRemindersController should look something like...
class PushupRemindersController < ApplicationController
before_filter :get_user
# Most of this you'll already have.
def index
#pushup_reminders = #user.pushup_reminders
respond_with #pushup_reminders
end
# This is the important one.
def create
attrs = {
:completed => false,
:num_pushups => #user.pushups_per_reminder,
:when_sent => Time.now
}
#pushup_reminder = #user.pushup_reminders.create(attrs)
respond_with #pushup_reminder
end
# This will handle getting the user from the params, thanks to the `before_filter`.
def get_user
#user = User.find(params[:user_id])
end
end
Of course, you'll have a new action that will present a web form to a user, etc. etc.
For the second use case, the cron task, set it up as a Rake task in your lib/tasks directory of your project. This gives you free reign to setup an action that gets hit whenever you need, via a cron task. You'll have full access to all your Rails models and so forth, just like a controller action. The real trick is this: if you've got crazy custom logic for setting up reminders, move it to an action in the PushupReminder model. That way you can fire off a creation method from a rake task, and one from the controller, and you don't have to repeat writing any of your creation logic. Remember, don't repeat yourself (DRY)!
One gem I've found quite useful in setting up cron tasks is the whenever gem. Write your site-specific cron jobs in Ruby, and get the exact output of what you'd need to paste into a cron tab (and if you're deploying via Capistrano, total hands-off management of cron jobs)!

Try setting your attr_accessible to :user instead of :user_id.
attr_accessible :user
An even better way to do this however would be to do
#user.pushup_reminders.create
That way the user_id is automatically assigned.
Use nested routes like this:
:resources :users do
:resources :pushup_reminders
end
This will give you params[:user_id] & params[:id] so you can find your objects in the db.
If you know your user via sessions, you won't need to nest your routes and can use that to save things instead.
Using restful routes, I would recommend using the create action in the pushup_reminders controller. This would be the most conventional and Restful way to do this kind of object creation.
def create
#user = User.find(params[:user_id]
#reminder = #user.pushup_reminders.create()
end
If you need to check whether object creation was successful, try using .new and .save

Related

Ruby on Rails 7 Multistep form with multiple models logic

I am currently struggling with building up a multi step form where every step creates a model instance.
In this case I have 3 models:
UserPlan
Connection
GameDashboard
Since the association is like that:
An user has an user_plan
A connection belongs to an user_plan
A game_dashboard belongs to a connection
I would like to create a wizard to allow the current_user to create a game_dashboard going through a multi-step form where he is also creating connection and user_plan instance.
For this purpose I looked at Wicked gem and I started creating the logic from game_dashboard (which is the last). As soon as I had to face with form generating I felt like maybe starting from the bottom was not the better solution.
That’s why I am here to ask for help:
What would be the better way to implement this wizard? Starting from the bottom (game_dashboard) or starting
from the top (use_plan)?
Since I’m not asking help for code at the moment I didn’t write any controller’s or model’s logic, in case it would be helpful to someone I will put it!
Thanks a lot
EDIT
Since i need to allow only one process at a time but allowing multiple processes, to avoid the params values i decided to create a new model called like "onboarding" where i handle steps states there, checking each time the step
The simplest way would be to rely on the standard MVC pattern of Rails.
Just use the create and update controller methods to link to the next model's form (instead of to a show or index view)
E.g.
class UserPlansController < ApplicationController
...
def create
if #user_plan = UserPlan.create(user_plan_params)
# the next step in the form wizard process:
redirect_to new_connection_path(user_id: current_user, user_plan_id: #user_plan.reload.id)
else
#user_plan = UserPlan.new(user: current_user)
render :new
end
end
...
# something similar for #update action
end
For routes, you have two options:
You could nest everything:
# routes.rb
resources :user do
resources :user_plan do
resources :connection do
resources : game_dashboard
end
end
end
Pro:
This would make setting your associations in your controllers easier because all your routes would have what you need. E.g.:
/users/:user_id/user_plans/:user_plan_id/connections/:connection_id/game_dashboards/:game_dashboard_id
Con:
Your routes and link helpers would be very long and intense towards the "bottom". E.g.
game_dashboard_connection_user_plan_user_path(:user_id, :user_plan_id, :connection_id, :game_dashboard)
You could just manually link your wizard "steps" together
Pro:
The URLs and helpers aren't so crazy. E.g.
new_connection_path(user_plan_id: #user_plan.id)
With one meaningful URL variable: user_plan_id=1, you can look up everything upstream. e.g.:
#user_plan = UserPlan.find(params['user_plan_id'])
#user = #user_plan.user
Con:
(not much of a "con" because you probably wind up doing this anyway)
If you need to display information about "parent" records, you have to perform model lookups in your controllers first:
class GameDashboardController < ApplicationController
# e.g. URL: /game_dashboards/new?connection_id=1
def new
#connection = Connection.find(params['connection_id'])
#user_plan = #connection.user_plan
#user = #user_plan.user
#game_dashboard = GameDashboard.new(connection: #connection)
end
end

How to populate example data to my Rails application in production?

I have a n00b question. I'm using Rails 5, and would like to have example data in the application. When a user creates a new project, the project should already contain sample "tasks" that the user can delete or edit.
I know I can use seeds.rb to create sample data in my development environment. What is the best way to do it in a production environment for new users, and how? Should I use seeds.rb, a migration, or a rake task?
Example controller:
def create
#project = Project.new(project_params)
#project.user = current_user
if #project.save
// add sample content
redirect_to #project
else
render :new
end
end
In the Project model:
belongs_to :user
has_many :tasks, dependent: :destroy
When a new user joins and creates a new project, how do I add sample "tasks" automatically on the new project that the user creates?
UPDATE:
To create a task, I need a description and the current user's id (I'm using Devise, so I can use the current_user helper), for example:
#project.tasks.create!(description: "hello", user_id: current_user.id)
You could build a simple ServiceObject that does the job. It allows you to keep your controller skinny and you can user your current_user Devise helper to keep track of which user is creating the project
if #project.save
SetupBaseProject.new(project).call
redirect_to #project
else
# handle errors
end
In app/services/setup_base_project.rb
class SetupBaseProject
def initialize(project, user)
#project = project
end
def call
# Create example tasks and any additional setup you want to add
#project.tasks.create(description: 'Hello World', user: #project.user)
end
end
There are two possible scenarios considering your question.
The first project created by a user needs to have sample tasks included by default
Whenever a new project is created, sample tasks are created by default. Irrespective of the user is new user/existing user.
For first scenario,
We need to track whether project is created by new user by adding a boolean field to user, for example: new_user which defaults true.
We can use active record callbacks for generating sample tasks after project is created.
For Example,
Project Model :
belongs_to :user
has_many :tasks, dependent: destroy
after_create :generate_tasks
def generate_tasks
if self.user.new_user #This conditional block can be modified as needed
(1..3).each do |n|
self.tasks.create!(description: "Sample task #{n}", user_id: self.user.id)
end
end
end
For the second scenario,
We can use the same projects model file and just remove the conditional statement which will help create sample tasks by after project is created.
If you need any clarification, please comment out.
I've done this quite a few times in the past.
From my experience, you eventually have to give other people the ability to manage those defaults (product owners, marketing, etc)
What I've done in the past is to have a test user with a project that acts as 'the default' project.
Whenever anyone wants to create a new project, you clone it.
I used https://github.com/amoeba-rb/amoeba for that. It offers out of the bow way to override attributes that I'd want to change and can cascade the cloning to any associations you'd want to clone.
Say sample data is on model Detail which was populated with seeds.rb and belongs to 'Project'. You can dup that record and asign it to the new project (not tested):
def create
#project = Project.new(project_params)
#project.user = current_user
#project.details << Detail.find_by_name('sample').dup
if #project.save
redirect_to #company
else
render :new
end
end
Also, consider use a transaction when saving data on more than one model.
Full disclosure, I work in Rails 4...
If it were me, I would use FactoryBot to get the dummy data you want. Factories are great for testing so if you use them for testing, why not borrow them for this? This post shows an example where someone wanted to mock dummy data in console, same ideas could apply for you here.
Once you've got your factories mocked up... maybe for tasks something like:
require 'faker'
FactoryBot.define do
factory :task do
transient do
parent_project { nil }
end
description { Faker::Hacker.say_something_smart }
project_id { parent_project.id }
end
end
Maybe create a method in the project model like:
def create_dummy_data
require 'factory_bot'
require 'faker'
include FactoryBot::Syntax::Methods
# create_list will spit out 3 tasks associated with your project
create_list(:task, 3, parent_project: self)
end
Then in your example: after calling save...
if #project.save
#project.create_dummy_data
redirect_to #company
else
I can't think of a reason you couldn't go this route... noodling around in console I didn't have any problems, but I'd look at this answer as a starting point and not a final solution =P

Rspec Rails: testing controller method 'create' with a multi-model form

I am building a Ruby on Rails app with the usual assortment of models, views and controllers.
The 'create' action in one of my controllers is supposed to create an instance of two different models. Here's my code:
def create
#league = League.new(params[:league])
#user = #league.users.build(params[:user])
... .save conditions appended ...
end
So, when you call 'create' through the LeaguesController via a POST request to /leagues, you get a new instance of League and a new instance of User. I also want the new User instance to inherit the ID of the new League instance, so it can be used as the foreign key to link the instances together. This is accomplished with:
def create
#league = League.new(params[:league])
#user = #league.users.build(params[:user])
#league_id = #league.id
#user.update_attribute('league_id', #league_id)
... .save conditions appended ...
end
The User model already belongs_to the League model, which has_many users.
The above code works just fine and dandy, verified via manual testing. However, I can't for the life of me figure out how to automate these tests with Rspec. I'm trying to be a good boy and use test-driven design, but this has me stumped.
The issue is that I can't figure out how to access the attributes of the newly created instances of League and User in my tests. I am attempting to do so using the following code:
describe LeaguesController do
describe 'new league and user' do
it 'should create a new user with a league_id equal to the new leagues id'
#league_attr = { :name => "myleague", :confirmation_code => "mycode", :id => 5}
#user_attr = { :username => "myname", :password => "mypass"}
post :create, :user => #user_attr, :league => #league_attr
assigns(:league_id).should eql(5)
end
end
end
But the test returns nil for the value of :league_id
I'm new to both programming in general and Rspec in particular, so I really appreciate any help someone might offer!
You cannot assign :id with new. Try this:
def create
#league = League.new(params[:league])
#league.id = params[:league][:id] if params[:league][:id]
#user = #league.users.build(params[:user])
#league_id = #league.id
#user.update_attribute('league_id', #league_id)
... .save conditions appended ...
end
That said, I wonder how come it works in the browser.
Also, you better off using FactoryGirl or Fixtures to assign data to models when testing.
Have you tried pry?
Whenever I discover something like this I find it very handy to be able to insert a brakepoint via Pry (or Ruby-Debug) so I can inspect the variables and their behavior.
I suspect putting in a binding.pry between #league_id = #league.id and #user.update_attribute('league_id', #league_id) may very well shed some light on the issue.
Also note that user will automatically inherit the #league_id when you persist it via the #league.save call. (that's the idea behind #league.users.build(..) - it will set the required relationships correctly upon persistance.

What is the best way of accessing routes in ActiveRecord models and observers

I have a situation where I want to make a request to third-party API(url shortening service) after creating a record in the database (updates a column in the table which stores the short url), in order to decouple the API request from the Model, I have set up an ActiveRecord Observer which kicks in every time a record is created, using after_create callback hook, here is the relevant code:
class Article < ActiveRecord::Base
has_many :comments
end
class ArticleObserver < ActiveRecord::Observer
def after_create(model)
url = article_url(model)
# Make api request...
end
end
The problem in the above code is article_url because Rails Routes are not available in either Model or ModelObservers, same as ActionMailer (similar problem exists in Mails where if we need to put an URL we have to configure "ActionMailer::default_options_url"). In theory accessing routes/request object in Model is considered a bad design. To circumvent the above issue I could include the url_helpers module as described in the following URL:
http://slaive-prog.tumblr.com/post/7618787555/using-routes-in-your-model-in-rails-3-0-x
But this does not seem to me a clean solution, does anybody have a pointer on this issue or any advice on how it should be done?
Thanks in advance.
I would definitely not let your models know about your routes. Instead, add something like attr_accessor :unshortened_url on your Article class. Set that field in your controller, and then use it from your observer. This has the added benefit of continuing to work if you later decide to set your shortened URL asynchronously via a background task.
Edit
A couple of things, first of all.
Let's get the knowledge of creating a short_url out of the model
entirely.
We could nitpick and say that the short_url itself doesn't belong in the model at all, but to remain practical let's leave it in there.
So let's move the trigger of this soon-to-be-background task into the controller.
class ArticlesController < ApplicationController
after_filter :short_url_job, :only => [:create]
# ...
protected
def short_url_job
begin
#article.short_url = "I have a short URL"
#article.save!
rescue Exception => e
# Log thy exception here
end
end
end
Now, obviously, this version of short_url_job is stupid, but it illustrates the point. You could trigger a DelayedJob, some sort of resque task, or whatever at this point, and your controller will carry on from here.

Rails plugin for Group of users

My Rails application have a User model and a Group model, where User belongs to a Group. Thanks to this, a user can be a admin, a manager, a subscriber, etc.
Until recently, when for example a new admin need to be create on the app, the process is just to create a new normal account, and then an admin sets the new normal account's group_id attribute as the group id of the admin... using some condition in my User controller. But it's not very clean, I think. Because for security, I need to add this kind of code in (for example) User#update:
class UsersController < ApplicationController
# ...
def update
#user = User.find(params[:id])
# I need to add some lines here, just as on the bottom of the post.
# I think it's ugly... in my controller. But I can not put this
# control in the model, because of current_user is not accessible
# into User model, I think.
if #user.update_attributes(params[:user])
flash[:notice] = "yea"
redirect_to root_path
else
render :action => 'edit'
end
end
# ...
end
Is there a clean way to do it, with a Rails plugin? Or without...
By more clean, I think it could be better if those lines from User#update:
if current_user.try(:group).try(:level).to_i > #user.try(:group).try(:level).to_i
if Group.exists?(params[:user][:group_id].to_i)
if Group.find(params[:user][:group_id].to_i).level < current_user.group.level
#user.group.id = params[:user][:group_id]
end
end
end
...was removed from the controller and the application was able to set the group only if a the current user's group's level is better then the edited user. But maybe I'm wrong, maybe my code is yet perfect :)
Note: in my User model, there is this code:
class User < ActiveRecord::Base
belongs_to :group
attr_readonly :group_id
before_create :first_user
private
def first_user
self.group_id = Group.all.max {|a,b| a.level <=> b.level }.id unless User.exists?
end
end
Do you think it's a good way? Or do you process differently?
Thank you.
i prefer the controller methods to be lean and small, and to put actual model logic inside your model (where it belongs).
In your controller i would write something along the lines of
def update
#user = User.find(params[:id]
if #user.can_be_updated_by? current_user
#user.set_group params[:user][:group_id], current_user.group.level
end
# remove group_id from hash
params[:user].remove_key(:group_id)
if #user.update_attributes(params[:user])
... as before
end
and in your model you would have
def can_be_updated_by? (other_user)
other_user.try(:group).try(:level).to_i > self.try(:group).try(:level).to_i
end
def set_group(group_id, allowed_level)
group = Group.find(group_id.to_i)
self.group = group if group.present? && group.level < allowed_level
end
Does that help?
Well if you have a User/Groups (or User/Roles) model there is no other way to go than that you have underlined.
If it is a one-to-many association you can choose to store the user group as a string and if it is a many-to-many association you can go for a bitmask but nonetheless either through business logic or admin choice you need to set the User/Group relation.
You can have several choices on how to set this relationship in a view.
To expand your model's capability I advice you to use CanCan, a very good authorization gem which makes it super easy to allow fine grain access to each resource in your rails app.

Resources