Submit data for multiple controller actions simultaneously without duplicating logic - ruby-on-rails

I want to include the logic from one controller action (submitting recipes a chef is allowed to cook) while also adding extra data to submit and validate (editing a chef's contact information). How would I accomplish this without duplicating all the logic?
Example controllers/actions:
ChefsController
#recipes_allowed_to_cook
cooks have long list of recipes, and here we decide 1-5 recipes of their list that they're allowed to cook at our restaurant
HiringController
#recipes_and_contact_info
Edit/submit recipes via CooksController#recipes_allowed_to_cook logic
while also adding/editing a chef's contact information
i.e. On submit, both recipes and contact info will be validted, but
recipes will be validated using same code as
CooksController#recipes_allowed_to_cook
The problem is, #recipes_allowed_to_cook has many instance variables and two different sections (one for :get and another for :post). I want to be able to use this logic simultaenously as I'm also submitting the data for a chef's contact info, so if either portion has errors we render the #recipes_and_contact_info.

You can use a Service Class:
# lib/controllers_logic/allowed_recipes_computor.rb
class ControllersLogic::AllowedRecipesComputor
attr_reader :chief, :recipes
def initialize(chief)
#chief = chief
end
def execute
#recipes = #chief.recipes.where(some_logic_to_filter_recipes)
self
end
end
Then in your controllers' actions:
def recipes_allowed_to_cook
#chief = Chief.find(params[:id])
computor = ControllersLogic::AllowedRecipesComputor.new(#chief).execute
#recipes = computor.recipes
end

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

Filter by multiple topics in rails

I’ve had a look through the form and have been unsuccessful in finding a solution for this. I currently have a page that displays a bunch of posts, with a simple #post = Post.all
I’m trying to create form that can filter by one of the columns, topics that Post have.
So basically on the left side it will have all the posts,
The right side will have all the unique topics of posts. The user can select one or more of the topics, then click submit, and the posts With the selected topics will be shown.
I’m able to show the unique topic, but unsure how to organise it so it filters after submitting. My current thought process is to create a form with all the topics. When the user presses a few topics, then submits, it filters by the selected topics. But I’m unsure on how to do the filtering in the controller as the amount of topics selected is dynamic. For example if it was just one post. It’s a simple #post = Post.where(:topic params[:chosen]) but I’m unsure how to filter it dynamically on different amounts of topics. Like if 2 or more topics are chosen.
Any help would be greatly appreciated.
You can implement the index method of controller something similar to this (Note: The code is not tested)
class PostsController < ApplicationController
before_action :construct_filters, only:[:index]
def index
#posts = Post.where(#query) # => Posts.where({"topic" => ["topic1","topic2"]}) or Post.where({}) in the case of no params passed(return all posts)
respond_to do |format|
format.html
format.js # In case of remote true submit, respond with index.js.erb and update the listing
end
end
private
# Generic method to construct query for listing
def construct_filters
#query = {}
# Pass the chosen from the form as comma separated string inside filter hash.
# Example params received: filters: {"topic" => "topic1,topic2"}
if params["filters"].present?
params["filters"].each do |k,v|
# You can modify the below line to suit your needs if you are not passing as comma seperated
filter_value = filter_value.split(',')
#query[k] = filter_value if filter_value.present?
end
end
# #query = {"topic" => ["topic1","topic2"]}
end
end
Having a generic filter method and a constructing parameter as filter hash will allow you to add more filters in future.
You can implement checkbox or multi select dropdown for selecting topics in your views, may be with input name as filters[topic]. Hope this helps!

Display unpersisted data in same list as persisted data

I have two data sources: my database and a third party API. The third-party API is the "source of truth" but I want a user to be able to "bookmark" an item from the third-party API which will then persist it in my database.
The challenge I'm facing is displaying both sets of items in the same list without too much complexity. Here's an example:
Item 1 (not bookmarked, from third-party API)
Item 2 (bookmarked, persisted locally)
Item 3 (bookmarked, persisted locally)
Item 4 (not bookmarked, from third-party API)
...etc
I want the view to fetch the list of all items from the controller and have 0 idea of where the items came from, but should only know whether or not each item is bookmarked so that it can be displayed (e.g. so the user can mark an unbookmarked item as bookmarked).
Generics would be one way to solve this in other languages, but alas, Ruby doesn't have generics (not complaining). In Ruby/Rails, what's the best way to wrap/structure these models so the view only has to worry about one type of item (when in reality there are two types behind the scenes?)
I'd suggest coming up with an object that takes care of fetching the items from both the third-party API and your database, the result of such operation would be an array of items that respond to the same methods, no matter where they came from.
Here's an example on how I'd go about it:
class ItemsController < ApplicationController
def index
#items = ItemRepository.all
end
end
In the code above ItemRepository is responsible for fetching items from both the database and the third party API, the view would then iterate over the #items instance variable.
Here's a sample implementation of the ItemRepository:
class ItemRepository
def self.all
new.all
end
# This method merges items from the API and
# the database into a single array
def all
results_from_api + local_results
end
private
def results_from_api
api_items.map do |api_item|
ResultItem.new(name: api_item['name'], bookmarked: false)
end
end
# This method fetches the items from the API and
# returns an array
def api_items
# [Insert logic to fetch items from API here]
end
def local_results
local_items.map do |local_item|
ResultItem.new(name: local_item.name, bookmarked: true)
end
end
# This method is in charge of fetching items from the
# database, it probably would use the Item model for this
def local_items
Item.all
end
end
The final piece of the puzzle is the ResultItem, remember ItemRepository.all will be returning an array containing objects of this type, so all you need to do is store the information that the view needs from each item on this class.
In this example I assume that all the view needs to know about each item is its name and whether it has been bookmarked or not, so ResultItem responds to the bookmarked? and name methods:
class ResultItem
attr_reader :name
def initialize(name:, bookmarked:)
#name = name
#bookmarked = bookmarked
end
def bookmarked?
!!#bookmarked
end
end
I hope this helps :)
PS. Sorry if some of the class names are too generic I couldn't come up with anything better

Ruby on Rails: What is the convention (is there one?) for creating a model instance programmatically

I have a model: 'event' and it has a controller: 'event_controller'
The event_controller handles the following route: events/:id/complete_event
In the controller, I need to trigger the creation a couple other model objects in the system, which are calculated and not inputted via a web form.
In this case the models to create are:
score (which belongs_to: user and event)
stats (which belongs_to: event)
standing (which belongs_to: user | and is based on the new score/stats object)
What is the convention for this type of model creation for Ruby on Rails?
Is it okay for the event_controller to create these (somewhat unrelated) model objects?
or,
Should the event_controller call into the score_controller, stats_controller and standing_controller?
With the second option, I am concerned that it will not work to dispatch 2-3 routes in a chain to create all the objects in their corresponding controllers but is that is the convention.
In the end, it's ideal to redirect the user back to show_event view, which will display the event and its associated scores and stats objects.
Code for the event_controller method: complete_event
def complete_event
event = Event.find(params[:id])
if event.in_progress?
event.complete!
end
# 1. create score for each user in event.users
# 2. create stats for the event
# 3. update the overall standings for each score (per user)
redirect_to event
end
As you can see, the event is not being creating on this action, rather the event state is updated to 'complete' this is the trigger to create the associated records.
The commented lines above represent what I need to do after event is complete; I am just not sure that this is where I go ahead and directly create the objects.
E.g. To create score will I have to calculate a lot of data that starts in event, but uses many models to get all the relevant data to create it.
You can move the whole logic out of controller so what i can understand when you call even.completed! you need to create score for users and update the over all ratings. So add call back in your model after_update is_event_completed! assuming evet.completed! will mark the event complete at db level. Then just place the corresponding logic in your model. It's the best practice to place your business logic into your model. Here is a nice gem to manage states of a model github.com/pluginaweek/state_machine and do specific stuff on specific events.
It feels to me that all the steps are part of completing an event, therefore I think it belongs into the Event#complete! method.
# in the controller
def complete_event
event = Event.find(params[:id])
event.complete! if event.in_progress?
redirect_to event
end
# in the model
def complete!
# complete event
# 1. create score for each user in event.users
# 2. create stats for the event
# 3. update the overall standings for each score (per user)
end
Or you could use a small Ruby class that performs the completion:
# in the controller
def complete_event
event = Event.find(params[:id])
EventCompleter.new(event).perform
redirect_to event
end
# in models/event_completer.rb
class EventCompleter < Struct.new(:event)
def perform
event.complete!
# 1. create score for each user in event.users
# 2. create stats for the event
# 3. update the overall standings for each score (per user)
end
end
The second seems a bit too complex for this simple example, but is an interesting pattern for more complex tasks. Main benefit is that this simple Ruby class encapsulates all business and domain logic that need to be done, when completing an event (this logic doesn't not really belong to events). And it it easier to test.

How to efficiently combine one link calling on two methods?

A User can have multiple Organizations. In a session hash it is defined for which organization a user currently is logged in for (which can only be 1 organization). There's also a view that displays an overview with a table of organizations the user is part of. On this screen the user can switch to another organization, which resets the session hash to that organization. Simplified version (removed validations):
def set_session(organization)
session[:logged_in_for] = organization.id
end
def logged_in_organization
set_session(user.organizations.first) if session[:logged_in_for].nil?
#organization = Organization.find(session[:logged_in_for])
end
The overview screen also needs to contain other functions for each of the organizations (these functions are also available from other screens such as the profile of each organization, so their methods are already in place). An example could be the function to add a new user for the organization.
But these other function use logged_in_organization as the organization for which they perform the function (there are security reasons why I want to keep it that way).
Therefore, what I would like is that if the function in the overview screen is clicked it executes the set_session(organization) method before it executes the function:
executes set_session(organization) for that organization
executes for example add_user_path
Is this possible? I though about adding additional routes and methods. These would then first calls on set_session(organization) and after that on the function itself. But that would require quite a few additional routes and methods, and perhaps there's an easier way? (perhaps tell the link_to in the view to execute two methods?, although the link_to documentation does not seem to facilitate such)
Update: Would it be possible to add the second route as a param in the overview page; for example (other functions will have other syntax than organization_id: organization.id so that's not generic):
<%= link_to "Add user", move_on_path( organization, adduser_path(organization_id: organization.id) ) %>
With as move_on method:
def move_on(organization, url)
set_session(organization)
redirect_to url
end
Clicking the link currently produces an error pointing to def move_on(organization, url): wrong number of arguments (0 for 2). What am I doing wrong here? P.S. I have the following route:
get 'move_on' => 'user#move_on', as: 'move_on'
It seems to me that the simplest way to handle that is just to modify your logged_in_organization method:
def logged_in_organization
#organization ||= begin
set_session(user.organizations.first) if session[:logged_in_for].nil?
#organization = Organization.find(session[:logged_in_for])
end
end
That way it won't overwrite #organization if it's already set. And then you just set it in before_action of relevant controllers, like so:
before_action :set_organization
def set_organization
#organization = current_user.organizations.find(params[:organization_id])
end

Resources