We have an app with an extensive admin section. We got a little trigger happy with features (as you do) and are looking for some quick and easy way to monitor "who uses what".
Ideally a simple gem that will allow us to track controller/actions on a per user basis to build up a picture of the features that are used and those that are not.
Anything out there that you'd recommend..
Thanks
Dom
I don't know that there's a popular gem or plugin for this; in the past, I've implemented this sort of auditing as a before_filter in ApplicationController:
from memory:
class ApplicationController < ActionController::Base
before_filter :audit_events
# ...
protected
def audit_events
local_params = params.clone
controller = local_params.delete(:controller)
action = local_params.delete(:action)
Audit.create(
:user => current_user,
:controller => controller,
:action => action,
:params => local_params
)
end
end
This assumes that you're using something like restful_authentication to get current user, of course.
EDIT: Depending on how your associations are set up, you'd do even better to replace the Audit.create bit with this:
current_user.audits.create({
:controller => controller,
:action => action,
:params => local_params
})
Scoping creations via ActiveRecord assoiations == best practice
Related
I am developing a app in ruby on rails for a local business. The pages are 'static', but changeable through a backend CMS I am building for them. Is there a best practice to creating a controller for static pages? Right now I have a sites controller with all static routes, like this.
routes.rb
get "site/home"
get "site/about_us"
get "site/faq"
get "site/discounts"
get "site/services"
get "site/contact_us"
get "site/admin"
get "site/posts"
or would I be better off creating member routes for the site controller like this without the crud, because a 'Site' will not need to have the CRUD.
resources :sites, :except => [:index, :new, :create, :update, :destroy]
member do
get :home
get :about_us
get :faq
get :discounts
get :services
get :contact_us
get :admin
get :posts
end
Or is there a best practice / better way? Any answers would be appreciated. Thanks
If the static pages list are not going to increase, then you can keep the list, but if you want a dynamic list like site/any_new_url , save the routes as
get 'site/:cms_page' => 'cms#show' # all requests matching site/any_page will go CmsController, show method
This will help reduce keep the routes from bloating, but the downside is you do not know what all routes are the valid ones. Your sample code can be
def show
#page_data = Page.find_by_page(:params[:cms_page])
end
show.html.erb
<%= #page_data.html_safe %>
Dunno yet if I consider this a best practice or an abomination but here is what I came up with when tackling the same problem.
My reasoning is that the site was providing some specified functionality (which doesn't really matter for this discussion) + a bunch of information about the organisation itself (about us, contact, FAQ, homepage blurb, whatever). Since all that data was really related to the organisation, an Organisation model seemed reasonable with each of those things as attributes. Here is the model:
class Organisation < ActiveRecord::Base
...validations stuff...
def self.attrs_regex
Regexp.new(self.attrs.join("|"))
end
def self.attrs
self.column_names.reject{|name| name =~ /id|created_at|updated_at/}
end
end
Then I use the attrs class method to generate routes based on the columns. This is in my routes.rb:
Organisation.attrs.each do |attr|
get "#{attr}" => "organisation##{attr}", :as => attr.to_sym
get "#{attr}/edit" => "organisation#edit", :as => "#{attr}_edit".to_sym, :defaults => { :attribute => attr }
post "#{attr}" => "organisation#update", :as => :organisation_update, :defaults => { :attribute => attr}, :constraints => Organisation.attrs_regex
end
The controller gets a little weird and I am not thrilled with the code here but here it is anyway. I need to make sure the attribute is set and available to the views so I can do the right thing there so I set it in the application controller:
class ApplicationController < ActionController::Base
protect_from_forgery
before_filter :set_attribute
def set_attribute
#attribute = action_name.parameterize
end
end
For the organisation controller I just set the #organisation variable to be the first and only row in the database in the before_filter and then let Rails do its usual magic of calling the method, failing, and rendering a view of the same name. The edit action just uses one view file to edit all the different attributes:
class OrganisationController < ApplicationController
before_filter :set_organisation
def edit
authorize! :edit, #organisation
#attribute = params[:attribute].parameterize
end
def update
authorize! :update, #organisation
#attribute = params[:attribute]
respond_to do |format|
if #organisation.update_attributes(params[:organisation])
format.html do
redirect_to "/#{#attribute}", notice: t('successful_update')
end
format.json { head :ok }
else
format.html { render action: "edit" }
end
end
end
private
def set_organisation
#organisation = Organisation.first
end
end
So that is where I ended up. Like you I hit up SO to tap into the seething mass of genius here but ended up with disappointing results. If there is something better out there I am still hoping to find it.
What I like about what I did is that routes are automatically generated based on the structure of the organisation table.
What I don't like about what I did is that routes automatically generated based on the structure of the organisation table.
I know I will pay for that design decision when I have to deal with i18n routing and there are probably a thousand other reasons that this is a bad idea that I have yet to discover but for the moment I have a happy client.
In the end this is not a suggestion that you should do this, but I am hoping to give you more than I got so you can advance your thinking on this and hopefully end up a little closer to that best practice.
If you are going to construct a CMS, which likely connects to a database, and allow your customer to change the text on the pages of their site, I would not recommend using static pages. In Rails terms, a static page would refer to creating html files in your /views/pages directory. If you go this route, then you're walking outside of the way that Rails was designed.
I believe that what you want to do is create tables in the database that correspond to and store the data for your posts, etc. You can pull information into the controller from the model that it corresponds to and then user a view to display the data. You can create a layout for these pages and then create controllers for each of the pages that you add.
As far as routes, I would recommend using the following:
map.resource :controller_name
you then would add the code that displays the information from the CMS in the corresponding show controller action and view for each page.
I have an offline rake job that updates my models. When that happens, I want to expire the :show action for that model.
# in lib/models/my_model.rb
after_update :expire_cache
def expire_cache
expire_action :controller => :my_models, :action => :show, :id => self
end
This doesn't work because expire_action isn't available in the model. Calling ActionController.new.expire_action gives me a lot of weird route issues, which is reasonable since none of the route logic is hooked up.
I think the common way to expire_action is with a sweeper, but that doesn't work because my model is not updated through controller actions.
NOTE: I feel like I may be using caching the wrong way since I can't find an answer to this anywhere.
You're looking for an ActionController Sweeper. You can find the official Rails documentation on how to implement them here, but likely you want something like this:
class MyModelSweeper < ActionController::Caching::Sweeper
observe MyModel
def after_update(my_model)
expire_action :controller => :my_models, :action => :show, :id => my_model
end
end
Implementing versioning for a Rails app I'd like to have a view that displays all versions of a model with some extra functionality like reverting etc.
I use the paper_trail gem for the versioning.
I know that I could do that by writing a controller function like versions and a view for every model but I'd like to do it for all models at once. This should be possible because the model.versions attribute is always structured identically.
Ideally the URL should look like /pages/testpage/versions while testpage is the page id.
This seems similar to the concept of nested routes in rails.
resources :pages do
resources :versions
end
The problems with nested routes however are:
Needs extra configuration per model
I cannot access the testpage object without knowing of which model it is an instance.
I also wasn't able to find a way to determine the model since the only thing that is provided to my versions controller is the params hash.
I'm completely open to alternative solutions that might not follow my initial ideas.
Write it in your ApplicationController and define it as a helper_method.
For example
class ApplicationController < ActionController::Base
helper_method :current_time
def current_time
Time.now
end
end
Now you can cal current_time everywhere in controllers or views.
Also you can write separate Module/Class and define there your helpers methods. Than you should include this file into your ApplicationController as well
UPD after theme is changed
I didn't think about your actual question. But I can say that your approach is nod the best here.
You should create new resource instead of creating new functionality which will hard to be tested. So create new resource (controller): versions and play around this controller.
For example how it can work:
/versions/pages/132
/versions/comments/1003
How to realize it:
match "/versions/:model/:id", :to => "versions#index"
In your controller:
class VersionsController < ActionController::Base
def index
#object = my_type.find(params[:id])
#versions = #object.versions
end
private
def my_type
params[:model].constantize
end
end
Of course you can change routes the way you want:
match "/:model/:id/versions", :to => "versions#show"
So now your pretty /pages/testpage/versions will work fine for you without any new strange logic.
UPD 2
Imagine you have got this route:
match "/:model/:id/versions", :to => "versions#index", :as => :versions
And this objects:
#page = Page.last
#hotel = Hotel.find(123)
#comment = #page.comments.first
How will we create links for versions:
<%= link_to "Versions of this page", versions_path(:model => #page.class.to_s, :id => #page.id) %>
<%= link_to "Versions of this hotel", versions_path(:model => #hotel.class.to_s, :id => #hotel.id) %>
<%= link_to "Versions of this comment", versions_path(:model => #comment.class.to_s, :id => #comment.id) %>
I would suggest passing a param such as 'type' and stuff the model name there. Then in your controller you can do:
class VersionsController < ApplicationController
def index
model = params[:type].classify.constantize
#obj = model.find(params[:id])
end
end
For your links, you can pass queries to the link_to helper
<%= link_to versions_path(#model, :type => #model.class) %>
Or something along those lines.
I am running Ruby on Rails 3 and I would like to set up my routes to show additional information in the URL using namespaces.
In the routes.rb file I have:
namespace "users" do
resources :account
end
So, the URL to show an account page is:
http://<site_name>/users/accounts/1
I would like to rewrite/redirect that URL as/to
http://<site_name>/user/1/Test_Username
where "Test_username" is the username of the user. Also, I would like to redirect all URLs like
# "Not_real_Test_username" is a bad entered username of the user.
http://<site_name>/users/accounts/1/Not_real_Test_username
to the above.
At this time I solved part of my issuelike this:
scope :module => "users" do
match 'user/:id' => "accounts#show"
end
My apologies for not answering your question (#zetetic has done that well enough), but the best practice here is to stay within the RESTful-style Rails URL scheme except for rare exceptions. The way most people make pretty URLs in this way is to use a hyphen, e.g.:
/accounts/1-username
This does not require any routing changes. Simply implement:
class Account < ActiveRecord::Base
def to_param
"#{self.id}-#{self.username}"
end
end
And handle the extra string data in your finds by calling to_i.
class AccountController < ApplicationController
def show
#account = Account.find(params[:id].to_i)
end
end
When you do link_to 'Your Account', account_path(#account), Rails will automatically produce the pretty URL.
It's probably best to do this in the controller, since you need to retrieve the account to get the username:
#account = Account.find(params[:id])
if #account && #account.username
redirect_to("/user/#{#account.id}/#{#account.username}")
return
end
As to the second issue, you can capture the remaining parameter by defining it in the route:
get "/users/accounts/:id(/:other)" => "users/accounts#show"
This maps like so:
/users/accounts/1/something # => {:id => "1", :other => "something"}
/users/accounts/1 # => {:id => "1"}
And you can simply ignore the :other key in the controller.
I have multiple user types in a system that shows each user different views and templates of the stored information, often based on whether they are logged in and what current_user.user_type is. As a result, I have lots of this:
#controller
#project = Project.find(params[:id])
if current_user.user_type == "Company"
redirect_to :controller => "companies", :action => "home"
elsif current_user.user_type == "Contractor"
#contractor = Contractor.find(current_user.user_type_id)
redirect_to :controller => "contractors", :action => "home"
elsif current_user.user_type == "Customer"
redirect_to :controller => "companies", :action => "list"
end
This is my first Rails project and I am pretty sure this is poor design. What are simple clean ways of doing this in a better way?
If you're having a lot of code like this, is a code smell that your controller is really serving multiple purposes. Assuming your controller is something like InfoController, which is a REST view for some Info model, ask yourself:
What is the central piece of your actions, the data or the user who access
it?
Are you taking decisions based on who
requests the actions? (Like saving,
deleting, etc)
Can these decisions be taken in the
Info model, rather than in the controller?
To me, seems like you should create different controllers for each model, and make just one redirect per action. In your views, you can use things like polymorphic_paths to link up to your controllers.
If you decide not to do this, I'd just put that code in a case statement instead of an if.
Can also use constraints as seen here: User-centric Routing in Rails 3
root :to => "companies#home", :constraints => UserTypeConstraint.new("Company")
Or scoped routes as seen here: Use lambdas for Rails 3 Route Constraints
scope :constraints => lambda{|req| !req.session[:company_user_id].blank? } do
# all company routes
end
When a user hit login in your application, you must have some code to redirect them somewhere.
This is where you want to put your code.
and you probably want to use:
redirect_to companies_path