What's the best way to refactor this Rails controller? - ruby-on-rails

I'd like some advice on how to best refactor this controller. The controller builds a page of zones and modules. Page has_many zones, zone has_many modules. So zones are just a cluster of modules wrapped in a container.
The problem I'm having is that some modules may have some specific queries that I don't want executed on every page, so I've had to add conditions. The conditions just test if the module is on the page, if it is the query is executed. One of the problems with this is if I add a hundred special module queries, the controller has to iterate through each one.
I think I would like to see these module condition moved out of the controller as well as all the additional custom actions. I can keep everything in this one controller, but I plan to have many apps using this controller so it could get messy.
class PagesController < ApplicationController
# GET /pages/1
# GET /pages/1.xml
# Show is the main page rendering action, page routes are aliased in routes.rb
def show
#-+-+-+-+-Core Page Queries-+-+-+-+-
#page = Page.find(params[:id])
#zones = #page.zones.find(:all, :order => 'zones.list_order ASC')
#mods = #page.mods.find(:all)
#columns = Page.columns
# restful params to influence page rendering, see routes.rb
#fragment = params[:fragment] # render single module
#cluster = params[:cluster] # render single zone
#head = params[:head] # render html, body and head
#-+-+-+-+-Page Level Json Conversions-+-+-+-+-
#metas = #page.metas ? ActiveSupport::JSON.decode(#page.metas) : nil
#javascripts = #page.javascripts ? ActiveSupport::JSON.decode(#page.javascripts) : nil
#-+-+-+-+-Module Specific Queries-+-+-+-+-
# would like to refactor this process
#mods.each do |mod|
# Reps Module Custom Queries
if mod.name == "reps"
#reps = User.find(:all, :joins => :roles, :conditions => { :roles => { :name => 'rep' } })
end
# Listing-poc Module Custom Queries
if mod.name == "listing-poc"
limit = params[:limit].to_i < 1 ? 10 : params[:limit]
PropertyEntry.update_from_listing(mod.service_url)
#properties = PropertyEntry.all(:limit => limit, :order => "city desc")
end
# Talents-index Module Custom Queries
if mod.name == "talents-index"
#talent = params[:type]
#reps = User.find(:all, :joins => :talents, :conditions => { :talents => { :name => #talent } })
end
end
respond_to do |format|
format.html # show.html.erb
format.xml { render :xml => #page.to_xml( :include => { :zones => { :include => :mods } } ) }
format.json { render :json => #page.to_json }
format.css # show.css.erb, CSS dependency manager template
end
end
# for property listing ajax request
def update_properties
limit = params[:limit].to_i < 1 ? 10 : params[:limit]
offset = params[:offset]
#properties = PropertyEntry.all(:limit => limit, :offset => offset, :order => "city desc")
#render :nothing => true
end
end
So imagine a site with a hundred modules and scores of additional controller actions. I think most would agree that it would be much cleaner if I could move that code out and refactor it to behave more like a configuration.

You should check out this gem:
http://github.com/josevalim/inherited_resources/tree/master
It is very elegant, and solves all the problems you have.

I'd move your snippet-specific queries into helper methods and get them out of the controller so that the snippets themselves can execute the query via erb and kept DRY and readable via a helper. So instead of referring to #refs in your module, you can instead refer to find_all_refs or somesuch in a module and have that execute and possibly memoize the response.

Related

ActiveAdmin limit records on Index page

I have some 10,000+ records in my model. In active_admin index page for that model I have set config.paginate = false. So all the 10,000+ records are shown by default.
How can I limit the number to say last 500 records. I have tried using the below method described here, but its not doing anything to the index page.
ActiveAdmin.register Post do
controller do
def scoped_collection
Post.all.limit(500)
end
end
end
set custom # of rows on page with controller before_filter
controller do
before_filter :set_per_page_var, :only => [:index]
def set_per_page_var
session[:per_page]=params[:per_page]||30
#per_page = session[:per_page]
end
end
and render sidebar with corresponding text input (you can render it as a drop-list)
#...
sidebar('Rows on page', :only => :index) do
form do |f|
f.text_field nil, 'per_page', :value => session[:per_page]
end
end
The issue is this code in Active Admin:
module ActiveAdmin
class ResourceController < BaseController
module DataAccess
def per_page
return max_csv_records if request.format == 'text/csv'
return max_per_page if active_admin_config.paginate == false
#per_page || active_admin_config.per_page
end
def max_csv_records
10_000
end
def max_per_page
10_000
end
end
end
end
When the paginate config option is set to false, it defaults to the number value returned by max_per_page. If you're fine with overriding it globally, you can put this in an initializer:
# config/initializers/active_admin_data_access.rb
module ActiveAdmin
class ResourceController < BaseController
module DataAccess
def max_per_page
500 # was 10,000
end
end
end
end
I was looking for an answer to this same question. I was unable to limit the number of records, so instead I have opted for putting a default value in one of my filters that guarantees an empty page when it loads.
(NOTE: I stole this idea from this stackoverflow question here:: Set ActiveAdmin filter default value )
Example::
In this example, I set a filter called "my_filter_id" equal to "0" in the "before_filter" method if all of the parameters are blank.
ActiveAdmin.register MyModel do
before_filter my_filter_id: :index do
params[:q] = {my_filter_id_eq: 0} if params[:commit].blank?
end
end
Use
Post.limit(500) instead of Post.all.limit(500) so it will minimize the latency.
controller do
def scoped_collection
Post.limit(500)
end
end
index :pagination_total => false do
selectable_column
column :id
column :user_name
column :country
column :city
end
Hope this will help someone.
Try below code. Replace something with your model name.
result = Something.find(:all, :order => "id desc", :limit => 5)
while !result.empty?
puts result.pop
end

Where do I put a rails method/scope that uses params?

I have a scope that uses RubyGeocoder method, near, to filter events by location using param[:searchCity]. The param gets the user's geolocation so it shows events only near them. I currently have it working in my events_controller index action, but I also need to call it on my home page.
Considering it's a filter that gets data from the database, I thought it would be best to go in the model, but I'm finding conflicting information on whether having a param in the model is ok or bad practice. Also, I can't get it to work in the model with the param present.
What's the best practice for something like this? Where should I place the scope, the model, controller, helper, or somewhere else?
Here's my code:
Model:
class Event < ActiveRecord::Base
# attr, validates, belongs_to etc here.
scope :is_near, self.near(params[:searchCity], 20, :units => :km, :order => :distance) #doesn't work with the param, works with a "string"
end
Controller:
def index
unless params[:searchCity].present?
params[:searchCity] = request.location.city
end
#events = Event.is_near
# below works in the controller, but I don't know how to call it on the home page
# #events = Event.near(params[:searchCity], 20, :units => :km, :order => :distance)
respond_to do |format|
format.html # index.html.erb
format.json { render json: #events }
end
end
The line I'm calling in my home page that gets how many events are in the area
<%= events.is_near.size %>
Edit: Using a lambda seems to be working. Is there any reason I shouldn't do it this way?
Model:
class Event < ActiveRecord::Base
scope :is_near, lambda {|city| self.near(city, 20, :units => :km, :order => :distance)}
end
Controller:
def index
#events = Event.is_near(params[:searchCity])
...
home.html.erb
<%= events.is_near(params[:searchCity]).size %>
Accessing the params in model is not possible. Params is something which is made to exist only at controller and view level.
So best way is to write some helper method in controller to perform this.
Class Mycontroller < ApplicationController
before_action fetch_data, :only => [:index]
def fetch_data
#data = Model.find(params[:id])#use params to use fetch data from db
end
def index
end

displaying a template/layout from another controller

I have the following associations
Group:
has_many :group_links, :dependent => :destroy
GroupLink:
belongs_to :group
I want to display all the group links which belong to a particular group inside a layout which includes other objects which are not visible from inside GroupLInks views. SO I want to render a template from the GroupController as follows:
def group_links
#group_links = #group.group_links.all
render :template => 'group_links/group_links', :layout =>
'/layouts/sponsored_group_manage_sub_menu'
end
But I get the following error:
The action 'index' could not be found for GroupLinksController
If I create a 'index' view for GroupLinks and try to display it inside a layout with other objects, it throws the error
"You have a nil object"
I have the following method to initialize inside my groupscontroller:
def init_group
#group = Group.find_by_id(params[:id])
#group_blog_tags=#group.blog.blog_posts.tag_counts
#booth_links = #group.group_links.all
max_id = Group.count_by_sql("select min(profile_id) from (select profile_id from
group_memberships where group_id = #{#group.id} order by profile_id desc
limit 200) as x")
#booth_members = #group.members.all(:conditions => "profiles.id >= #
{rand(max_id)+1}", :limit => 20).to_a.sort! { |a,b| rand(3)-1 }
redirect_to groups_explorations_path unless #group
end
These other objects are used in the groups layouts to display the other objects. The thing is I was able to display another object from the GroupsController, without needing any index action inside that objects Controller. I have the exact same setup for GroupLinks as well but it does not work in this case...Please can you help me resolve this?
I think the problem is that #group is nil in your controller method group_links.
You can try to make like this:
def group_links
#group = Group.find(params[:id])
#group_links = #group.group_links.all
render :template => 'group_links/group_links', :layout =>
'/layouts/sponsored_group_manage_sub_menu'
end
But I'm not sure that this will work correctly:
render :template => 'group_links/group_links', :layout =>
'/layouts/sponsored_group_manage_sub_menu'
You can try just put your template in correct view folder with name "group_links" and match in your routes.rb file.
It turns out I had not put the proper path for this method in my layout. The routes were fine but the call to this method was not - I had to break my head to find it :(...Thanks for your time #Mosin and #RubyMan, appreciate it!

How to create a full Audit log in Rails for every table?

We recently began a compliance push at our company and are required to keep a full history of changes to our data which is currently managed in a Rails application. We've been given the OK to simply push something descriptive for every action to a log file, which is a fairly unobtrusive way to go.
My inclination is to do something like this in ApplicationController:
around_filter :set_logger_username
def set_logger_username
Thread.current["username"] = current_user.login || "guest"
yield
Thread.current["username"] = nil
end
Then create an observer that looks something like this:
class AuditObserver < ActiveRecord::Observer
observe ... #all models that need to be observed
def after_create(auditable)
AUDIT_LOG.info "[#{username}][ADD][#{auditable.class.name}][#{auditable.id}]:#{auditable.inspect}"
end
def before_update(auditable)
AUDIT_LOG.info "[#{username}][MOD][#{auditable.class.name}][#{auditable.id}]:#{auditable.changed.inspect}"
end
def before_destroy(auditable)
AUDIT_LOG.info "[#{username}][DEL][#{auditable.class.name}][#{auditable.id}]:#{auditable.inspect}"
end
def username
(Thread.current['username'] || "UNKNOWN").ljust(30)
end
end
and in general this works great, but it fails when using the "magic" <association>_ids method that is tacked to has_many :through => associations.
For instance:
# model
class MyModel
has_many :runway_models, :dependent => :destroy
has_many :runways, :through => :runway_models
end
#controller
class MyModelController < ApplicationController
# ...
# params => {:my_model => {:runways_ids => ['1', '2', '3', '5', '8']}}
def update
respond_to do |format|
if #my_model.update_attributes(params[:my_model])
flash[:notice] = 'My Model was successfully updated.'
format.html { redirect_to(#my_model) }
format.xml { head :ok }
else
format.html { render :action => "edit" }
format.xml { render :xml => #my_model.errors, :status => :unprocessable_entity }
end
end
end
# ...
end
This will end up triggering the after_create when new Runway records are associated, but will not trigger the before_destroy when a RunwayModel is deleted.
My question is...
Is there a way to make it work so that it will observe those changes (and/or potentially other deletes)?
Is there a better solution that is still relatively unobtrusive?
I had a similar requirement on a recent project. I ended using the acts_as_audited gem, and it worked great for us.
In my application controller I have line like the following
audit RunWay,RunWayModel,OtherModelName
and it takes care of all the magic, it also keeps a log of all the changes that were made and who made them-- its pretty slick.
Hope it helps
Use the Vestal versions plugin for this:
Refer to this screen cast for more details. Look at the similar question answered here recently.
Vestal versions plugin is the most active plugin and it only stores delta. The delta belonging to different models are stored in one table.
class User < ActiveRecord::Base
versioned
end
# following lines of code is from the readme
>> u = User.create(:first_name => "Steve", :last_name => "Richert")
=> #<User first_name: "Steve", last_name: "Richert">
>> u.version
=> 1
>> u.update_attribute(:first_name, "Stephen")
=> true
>> u.name
=> "Stephen Richert"
>> u.version
=> 2
>> u.revert_to(10.seconds.ago)
=> 1
>> u.name
=> "Steve Richert"
>> u.version
=> 1
>> u.save
=> true
>> u.version
=> 3
Added this monkey-patch to our lib/core_extensions.rb
ActiveRecord::Associations::HasManyThroughAssociation.class_eval do
def delete_records(records)
klass = #reflection.through_reflection.klass
records.each do |associate|
klass.destroy_all(construct_join_attributes(associate))
end
end
end
It is a performance hit(!), but satisfies the requirement and considering the fact that this destroy_all doesn't get called often, it works for our needs--though I am going to check out acts_as_versioned and acts_as_audited
You could also use something like acts_as_versioned http://github.com/technoweenie/acts_as_versioned
It versions your table records and creates a copy every time something changes (like in a wiki for instance)
This would be easier to audit (show diffs in an interface etc) than a log file

Ruby on Rails: Equating items in controllers (or maybe models?)

I'm trying to make attributes equal predetermined values, and I'm not sure if I'm doing that efficiently with the following (in my orders controller):
def create
#order = Order.find(params[:id])
#order.price = 5.99
#order.representative = Product.find(params[:product_id]).representative
#order.shipping_location = SHIPPING_LOCATION
#order.user = current_user
respond_to do |format|
...
end
end
Is there a more efficient way to equate attributes in Rails (maybe using models)? If I'm using two different controllers, do I just repeat what I did above for the new controller?
Use before_create callback in model to assign default values.
Your code is a little off, it looks like a controller action for create, but the code reads like it's for an update.
Regardless...
You could use a parameter hash to update everything at once.
In the case where you're creating:
order_update = {:price => 5.99, :representative =>
Product.find(params[:product_id]).representative,
:shipping_location => SHIPPING_LOCATION,
:user => current_user}
#order = Order.new(order_update)
In the case where you're updating:
#order.update_attributes(order_update) #attempts to save.
Mixing it into your controller code we get:
def create
#order = Order.find(params[:id])
order_update = {:price => 5.99, :representative =>
Product.find(params[:product_id]).representative,
:shipping_location => SHIPPING_LOCATION,
:user => current_user}
respond_to do |format|
if #order.update_attributes(order_update)
# save succeeded. Redirect.
else
# save failed. Render with errors.
end
end
end
Another solution:
class Example < ActiveRecord::Base
DEFAULTS = HashWithIndifferentAccess.new(:some => 'default', :values => 'here')
def initialize(params = {})
super(DEFAULTS.merge(params))
end
end
Either use initialize and merge with params, or use an ActiveRecord hook like before_create etc.

Resources