I'm working on a fitness tracking app where we encourage you to track a habit for 30 days.
Every user has_many projects, projects belong_to user, projects has_many tasks, and tasks belong_to projects.
What I'm looking to do is when a project is created, I want to populate 30 empty tasks which will be displayed in order of day, and allow the user to click on a day and update the task. (see image)
enter image description here
I'm happy to post any of the code/views if you need reference.
Thanks for the help!
class Project < ActiveRecord::Base
has_many :tasks
def create_tasks!(n = 30)
self.class.transaction do
1..n.each do |day|
self.tasks.create(day: day)
end
end
end
end
Wrapping a mass insert in a single transaction is vital for performance - otherwise each insert will be run in its own transaction.
You could use a after_create model callback to call create_tasks! - but this can be problematic since the callback will fire every time you create a project which can make tests slow.
class Project < ActiveRecord::Base
has_many :tasks
after_create :create_tasks!
def create_tasks!(n = 30)
self.class.transaction do
1..n.each do |day|
self.tasks.create(day: day)
end
end
end
end
Another way to this would be to call it in your controller:
class ProjectsController < ApplicationController
def create
#project = Project.new(project_params)
if #project.save
#project.create_tasks!
redirect_to #project
else
render :new
end
end
end
Which gives you better control over exactly where in the application it happens.
You can make use of Active Record's after_create callback, which allows you to perform a task whenever a new record is created for a certain model:
class Project < ActiveRecord::Base
has_many :tasks
after_create :create_empty_tasks
private
def create_empty_tasks
# Create your 30 Task objects
30.times do |i|
Task.create(day: (i + 1), project: self) # Update to match your schema
end
end
end
You'll of course need to update that code to pass any user-specific data into the Task, but hopefully the callback is a good starting point.
Related
I have the following models:
class Page < ApplicationRecord
has_one :architecture
end
class Architecture < ApplicationRecord
belongs_to :page
end
And after a new page is saved I need to capture it's architecture (number of paragraphs por example). I would like to know what is the proper way to do that. I'm not sure if I should leave that responsible for the Page model:
class Page < ApplicationRecord
has_one :architecture
after_create :scrape_architecture
private
def scrape_architecture
data = call_something_to_capture_architecture(url)
create_architecture(data)
end
end
class Architecture < ApplicationRecord
belongs_to :page
end
or if it should be the responsibility of the Architecture model:
class Page < ApplicationRecord
has_one :architecture
after_create :create_architecture
end
class Architecture < ApplicationRecord
belongs_to :page
before_create :scrape_page
private
def scrape_page
data = call_something_to_capture_architecture(page.url)
create(data)
end
end
Which is actually incorrectly because before_create runs after the validation – causing MySQL errors duo to non null constraints
Thank you.
I would just create a job or service object that handles scraping.
class PageScrapingJob < ApplicationJob
queue_as :default
def perform(page)
data = call_something_to_capture_architecture(page.url)
architecture = page.create_actitecture(data)
# ...
end
end
You would then call the service/job in your controller after saving the page:
class PagesController < ApplicationController
def create
#page = Page.new(page_params)
if #page.save
PageScrapingJob.perform_now(#page)
redirect_to #page
else
render :new
end
end
end
This gives you a perfect control of exactly when this is fired and avoids placing even more responsibilities onto your models. Even though your models may contain little code they have a huge amount of responsibilities such as validations, I18n, form binding, dirty tracking etc that are provided by ActiveModel and ActiveRecord. The list really goes on and on.
This instead creates a discrete object that does only one job (and hopefully does it well) and that can be tested in isolation from the controller.
For such things you could use a service pattern
class PageScrapper
Result = Struct.new(:success?, :data)
def initialize(url)
#url = url
end
def call
result = process(#url)
...
if result.success? # pseudo methods
Result.new(true, result)
else
Result.new(false, nil)
end
end
end
class Fetcher
...
def call
scrapper = PageScrapper.new(url)
result = scrapper.call
if scrapper.success?
page = Page.build(parsed_result_if_needed(result)
page.architecture.build(what_you_need)
page.save # here you need to add error handling if save fails
else
# error handling
end
end
There are a lot of resources about why callbacks are bad.
Here is one from Marcin Grzywaczewski but you can also google it "why callbacks are bad ruby on rails".
By using service you are liberating models from having too much business logic and they do not need to know about other parts of your system.
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).
I have Rails 4 app with two models.
class User
has_many :bids
end
class Bid
belongs_to :user
end
A User can only create one bid per week, so I added the following to the Bid table
add_column :bids, :expiry, :datetime, default: DateTime.current.end_of_week
and the following scopes to the Bid model
scope :default, -> { order('bids.created_at DESC') }
scope :active, -> { default.where('expiry > ?', Date.today ) }
I can now prevent a User creating multiple Bids at the controller level like so:
class BidsController
def new
if current_user.bids.active.any?
flash[:notice] = "You already have an active Bid. You can edit it here."
redirect_to edit_bid_path(current_user.bids.active.last)
else
#bid = Bid.new
respond_with(#bid)
end
end
end
But what is the best approach for validating this at the model level?
I've been trying to set up a custom validation, but am struggling to see the best way to set this up so that the current_user is available to the method. Also, am I adding errors to the correct object?
class Bid
validate :validates_number_of_active_bids
def validates_number_of_active_bids
if Bid.active.where(user_id: current_user).any?
errors.add(:bid, "too much")
end
end
end
In order to maintain separation of concerns, keep the current_user knowledge out of the model layer. Your Bid model already has a user_id attribute. Also, I'd add an error like so since the validation is not checking a "bid" attribute on Bid, but rather the entire Bid may be invalid.
class Bid
validate :validates_number_of_active_bids
def validates_number_of_active_bids
if Bid.where(user_id: user_id).active.any?
errors[:base] << "A new bid cannot be created until the current one expires"
end
end
end
This seems like it should be in a collaborator service object. Create a new class that is named appropriately (something like ActiveBid, maybe think a little on the name) That class will be initialized with a current_user and either return the active bid or false.
This limits the logic for this limitation into a single place (maybe some plans in the future can have 2, etc.
Then in the controller do a before_action that enforces this logic.
before_action :enforce_bid_limits, only: [:new, create]
private
def enforce_bid_limits
active_bid = ActiveBid.new(current_user).call
if active_bid #returns false or the record of the current_bid
flash[:notice] = "You already have an active Bid. You can edit it here."
redirect_to edit_bid_path(bid)
end
end
Later on if you end up needing this logic in several places throw this stuff in a module and then you can just include it in the controllers that you want.
I'm using single table inheritance successfully like so:
class Transaction < ActiveRecord::Base
belongs_to :order
end
class Purchase < Transaction
end
class Refund < Transaction
end
The abbreviated/simplified PurchaseController looks like this:
class PurchaseController < TransactionController
def new
#transaction = #order.purchases.new(type: type)
end
def create
#transaction = #order.purchases.new secure_params
if #transaction.save
redirect_to #order
else
render :new
end
end
end
The abbreviated/simplified Purchase model looks like this:
class Purchase < Transaction
attr_accessor :cc_number, :cc_expiry, :cc_csv
end
What I'm trying to do is have different variations of a purchase, for instance a cash purchase & a cheque purchase. The issue is I'm not sure how to call the model for that variation.
For example:
class Cash < Purchase
attr_accessor :receipt_no
end
class CashController < TransactionController
def new
# This will use the Purchase.rb model so that's no good because I need the Cash.rb model attributes
#transaction = #order.purchases.new(type: type)
# This gives me the following error:
# ActiveRecord::SubclassNotFound: Invalid single-table inheritance type: Purchase is not a subclass of Cash
#transaction = Cash.new(type: 'Purchase', order: #order.id)
end
end
I'm not sure why it doesn't work for you, this works fine for me:
#order.purchases.new(type: "Cash") # returns a new Cash instance
You can also push a new Cash on to the association if you are ready to save it:
#order.purchases << Cash.new
Or you can define a separate association in Order:
class Order < ActiveRecord::Base
has_many :cashes
end
#order.cashes.new # returns a new Cash instance
Class
Maybe I'm being obtuse, but perhaps you'll be willing to not make the Purchase type an inherited class?
The problem I see is that you're calling Cash.new, when really you may be better to include all the functionality you require in the Purchase model, which will then be able to be re-factored afterwards.
Specifically, why don't you just include your own type attribute in your Purchase model, which you'll then be able to use with the following setup:
#app/controllers/cash_controller.rb
class CashController < ApplicationController
def new
#transaction = Purchase.new
end
def create
#transaction = Purchase.new transaction_params
#transaction.type ||= "cash"
end
private
def cash_params
params.require(:transaction).permit(:x, :y, :z)
end
end
The only downside to this would be that if you wanted to include different business logic for each type of purchase, you'll still want to use your inherited model. However, you could simply split the functionality in the before_create callback:
#app/models/puchase.rb
class Purchase < Transaction
before_create :set_type
private
def set_type
if type =="cash"
# do something here
else
# do something here
end
end
end
As such, right now, I think your use of two separate models (Cash and Cheque) will likely be causing much more of an issue than is present. Although I'd love to see how you could inherit from an inherited Model, what I've provided is something you also may wish to look into
I'm working on creating a survey application where I create the survey on my rails server and as long as the user hasn't taken the survey the next time they open up my Android app they'll be displayed the survey to take. Once the user takes the survey I'd like to update the survey to contain that user so that they are no longer sent the same survey each time they log in. I'm using a HABTM relationship between users/surveys.
I'm able to get the survey/let the user take it/create the user choices back on the server but I'm struggling with updating the survey with the user that took just the survey. I'm pretty sure my problem is my PUT request (the auth_token is used to identify the user).
Started PUT "/api/v1/surveys/4?auth_token=8d707d9fa2b279f381eb416f1be887c0"
Here's my controller:
module Api
module V1
class SurveysController < ApplicationController
# before_action :restrict_access
respond_to :json
def survey
#survey = Survey.check_survey params[:auth_token]
redirect_to api_v1_survey_path(#survey)
end
def update
#survey = Survey.find(params[:id])
#user = User.find_user_by_token params[:auth_token]
complete_survey #survey, #user
end
My model:
class Survey < ActiveRecord::Base
has_many :surveytizations
has_many :questions, :through => :surveytizations
has_and_belongs_to_many :users, -> { uniq }
def complete_survey (survey, user)
survey.users << user
survey.number_taken += 1
if survey.number_taken == survey.survey_limit
survey.survey_finished = true
end
end
In Using curl to test Rails routes it goes into how to update an attribute of a user. I'm pretty new to curl - if I wanted to test updating an array of users with an additional user, how would I do that? I tried survey[users]=user1 but that doesn't seem to work.
Thanks in advance for your help!