I have a Rails3 app that has Workflows, and they have many WorkAssignments. WorkAssignments have a field, work_sent_date, the date work was sent to the person. On the Workflow edit screen I display a work sent date field, but Workflow does not have an attribute work_sent_date. If any work assignments have a date, I display the most recent one and it can't be edited. If all are blank, I display a text box that is editable and in WorkflowController#update, if that date is filled it, the work assignments' work_sent_date field get that date.
It works when I test it manually. I suppose I could just create an attribute for it, but I'm trying to be slick and not have redundant data.
I'm trying to write a test for this. First I assert that the WorkAssignment#work_sent_date is blank for all work assignments. Then I try a "post :update" and I can't figure out how to pass in the work_sent_date value, which is a form field but not an attribute. What I want to do is something like.
test "setting work_sent_date in wf sets it in wa" do
#workflow.work_assignments.each do |wa|
assert wa.work_sent_date.blank?
end
get :edit, :id => #workflow.id
assert_response :success
post :update, :workflow => #workflow.attributes, :parameters => {'work_sent_date' => Date.today.to_s}
#workflow.work_assignments.each do |wa|
assert_equal(wa.work_sent_date, Date.today)
end
end
But that parameters field doesn't work. There's no error, but I keep getting failures because wa.work_sent_date is still nil, so it's not getting passed in correctly. How do I pass today's date in as an extra parameter?
(Or maybe there's a better way to do the whole thing, which I would gladly consider.)
I know this is complicated. I hope I explained it well. Any suggestions would be appreciated. I've googled to death and can't find anything.
Found my problem. Syntax way wrong. Here's what it should be. This works.
put :update, :id => #workflow.id, :workflow => #workflow.attributes, :work_sent_date => Date.today.to_s
You can also refactor out the create and edit as follows:
protected
def edit_workflow(workflow, options = {})
post :update, :id => workflow.id, :workflow => workflow.attributes.merge(options)
end
def create_workflow(options = {})
post :create, :workflow => {}.merge(options)
end
end
Related
I'm trying to track user login history for stat purposes but its not clear to me what the best way to go about it would be. I could have a separate table that records users and their login stats with a date, but that table could get REALLY big. I could track some historic fields in the User model/object itself in a parse-able field and just update it (them) with some delimited string format. e.g. split on :, get the last one, if an included date code isn't today, add an item (date+count) otherwise increment, then save it back. At least with this second approach it would be easy to remove old items (e.g. only keep 30 days of daily logins, or IPs), as a separate table would require a task to delete old records.
I'm a big fan of instant changes. Tasks are useful, but can complicate things for maintenance reasons.
Anyone have any suggestions? I don't have an external data caching solution up or anything yet. Any pointers are also welcome! (I've been hunting for similar questions and answers)
Thanks!
If you have the :trackable module, I found this the easiest way. In the User model (or whichever model you're authenticating)
def update_tracked_fields!(request)
old_signin = self.last_sign_in_at
super
if self.last_sign_in_at != old_signin
Audit.create :user => self, :action => "login", :ip => self.last_sign_in_ip
end
end
(Inspired by https://github.com/plataformatec/devise/wiki/How-To:-Turn-off-trackable-for-admin-users)
There is a nice way to do that through Devise.
Warden sets up a hook called after_set_user that runs after setting a user. So, supposed you have a model Login containing an ip field, a logged_in_at field and user_id field, you can only create the record using this fields.
Warden::Manager.after_set_user :except => :fetch do |record, warden, options|
Login.create!(:ip => warden.request.ip, :logged_in_at => Time.now, :user_id => record.id)
end
Building upon #user208769's answer, the core Devise::Models::Trackable#update_tracked_fields! method now calls a helper method named update_tracked_fields prior to saving. That means you can use ActiveRecord::Dirty helpers to make it a little simpler:
def update_tracked_fields(request)
super
if last_sign_in_at_changed?
Audit.create(user: self, action: 'login', ip: last_sign_in_ip)
end
end
This can be simplified even further (and be more reliable given validations) if audits is a relationship on your model:
def update_tracked_fields(request)
super
audits.build(action: 'login', ip: last_sign_in_ip) if last_sign_in_at_changed?
end
Devise supports tracking the last signed in date and the last signed in ip address with it's :trackable module. By adding this module to your user model, and then also adding the correct fields to your database, which are:
:sign_in_count, :type => Integer, :default => 0
:current_sign_in_at, :type => Time
:last_sign_in_at, :type => Time
:current_sign_in_ip, :type => String
:last_sign_in_ip, :type => String
You could then override the Devise::SessionsController and it's create action to then save the :last_sign_in_at and :last_sign_in_ip to a separate table in a before_create callback. You should then be able to keep them as long you would like.
Here's an example (scribd_analytics)
create_table 'page_views' do |t|
t.column 'user_id', :integer
t.column 'request_url', :string, :limit => 200
t.column 'session', :string, :limit => 32
t.column 'ip_address', :string, :limit => 16
t.column 'referer', :string, :limit => 200
t.column 'user_agent', :string, :limit => 200
t.column 'created_at', :timestamp
end
Add a whole bunch of indexes, depending on queries
Create a PageView on every request
We used a hand-built SQL query to take out the ActiveRecord overhead on
this
Might try MySQL's 'insert delayed´
Analytics queries are usually hand-coded SQL
Use 'explain select´ to make sure MySQL isusing the indexes you expect
Scales pretty well
BUT analytics queries expensive, can clog upmain DB server
Our solution:
use two DB servers in a master/slave setup
move all the analytics queries to the slave
http://www.scribd.com/doc/49575/Scaling-Rails-Presentation-From-Scribd-Launch
Another option to check is Gattica with Google Analytics
I hate answering my own questions, especially given that you both gave helpful answers. I think answering my question with the approach I took might help others, in combination with your answers.
I've been playing with the Impressionist Gem (the only useful page view Gem since the abandoned RailStat) with good results so far. After setting up the basic migration, I found that the expected usage follows Rail's MVC design very closely. If you add "impressionist" to a Controller, it will go looking for the Model when logging the page view to the database. You can modify this behaviour or just call impressionist yourself in your Controller (or anywhere really) if you're like me and happen to be testing it out on a Controller that doesn't have a Model.
Anyways, I got it working with Devise to track successful logins by overriding the Devise::SessionsController and just calling the impressionist method for the #current_member: (don't forget to check if it's nil! on failed login)
class TestSessionController < Devise::SessionsController
def create
if not #current_member.nil?
impressionist(#current_member)
end
super
end
end
Adding it to other site parts later for some limited analytics is easy to do. The only other thing I had to do was update my routes to use the new TestSessionController for the Devise login route:
post 'login' => 'test_session#create', :as => :member_session
Devise works like normal without having to modify Devise in anyway, and my impressionist DB table is indexed and logging logins. I'll just need a rake task later to trim it weekly or so.
Now I just need to work out how to chart daily logins without having to write a bunch of looping, dirty queries...
There is also 'paper_trail' gem, that allows to track model changes.
There are a couple of places where I could do what I need, but I'm not sure where the best place is in line with good practices.
I have an Orders controller, and after a successful order is created I want to create a subscription (but only if the order is a success), and a referral (but only if the order is associated with one).
Now the obvious choice is to use after_create on the Order model... but... how can I get session data into that? (The referral ids, friend ids and voucher ids are only in the session as there's no need to store them in the Order db).
So should I just create the Subscription and Referral objects in the create action (how I have it at the mo) or is there a better way?
Here's my create action:
(#order.purchase only returns true if the payment was successful)
def create
if #order.save
if #order.purchase
Subscription.create(:order_id => #order.id, :product_id => #order.product_id)
if #order.voucher
Referral.create(:user_id => session[:friend_id], :order_id => #order.id,
:voucher_amount => #voucher_value)
end
render :action => "success"
else
render :action => "failure"
end
else
render :action => 'new'
end
end
Any help would be appreciated - I really would like to do this properly so I hope no one minds me asking what is probably a simple question.
I recently had a similar question, please take a look, i think that a simple virtual attribute in a callback will do it for you as well.
Fetch current user in after_create filter
use of callbacks will make your life easy, you need to use after_save
do all your stuff in after_save callback of order model. see rails api doc for callback here
Edit: If the session variable is not available with model, you can have a post_save method to deal with all logic which also accepts all require params like
like
class Order < ActiveRecord::Base
def post_save require_attr
#create subscriptions
# create referral
end
end
For certain models, I wish to provide functionality that allows a user to create a new record with default attributes based on copy of an existing record.
I'm wondering what would be the correct restful route for this.
My initial thinking is that it could be a parameter to the new action. I.e. to borrow from the the Rails Guides examples, instead of just:
GET : /photos/new
Also allow:
GET : /photos/new/:id
...where :id is the id of the record to use as a template. The response would be a new/edit form, same as with a plain old new but the values would be pre-filled with data from the existing record. The parameter (or absense of it) could be easily handled by the new controller method.
The alternative seems to be to create a new controller method, for example copy which would also accept an id of an existing record and response with the new form as above. This seems a little 'incorrect' to me, as the record is not actually being copied until the user saves the new record (after probably editig it somewhat).
TIA...
UPDATE: my question is not "how do I do this in rails?", it's "is it RESTful?"
my question is not "how do I do this in rails?", it's "is it RESTful?"
No, it isn't. For that matter, neither is GET /photos/new. Rails seems to be hopelessly mired in the past, where it was considered haute programme for a GET on a URI to return an HTML form which would then POST x-www-form-urlencoded data back to that same URI. The opacity of that POST forces them to invent new verbs-as-URI's like /photos/new, when you could be using PUT instead, or at least POST with the same media type.
The simplest way to make a copy of an HTTP resource RESTfully is:
GET /photos/{id}/ -> [representation of a photo resource]
...make modifications to that representation as desired...
POST /photos/ <- [modified representation]
If you're implementing this for browsers, you should be able to perform those actions via Ajax quite easily, using an HTML page sitting perhaps at /photos/manager.html/ to drive the interaction with the user.
You can try to use nested resources. I'm not exactly sure about structure of you application, but in general using nested photos will look somehow like this:
routes.rb
resources :photos do
resources :photos
end
photos_controller.rb
before_filter :find_parent_photo, :only => [:new, :create]
def create
#photo = Photo.new params[:photo]
if #parent_photo.present?
# fill some #photo fields from #parent_photo
end
#photo.save
respond_with #photo
end
def find_parent_photo
#parent_photo = Photo.find(params[:photo_id]) if params[:photo_id].present?
end
new.html.haml
= form_for [#parent_photo, #photo] do |f|
-# your form code
previously when you wanted to add a link to photo creation you wrote something like that
= link_to "new photo", [:new, :photo]
now if you want to add a link to photo creation based on foto #photo1
= link_to "new photo based on other one", [:new, #photo1, :photo]
You should be able to match a route like so:
match 'photos/new/:photo_id' => 'photos#new
or you could just pass a :photo_id parameter in the url and handle it in the controller:
'/photos/new?photo_id=17'
Example using helper method: new_photo_path(:photo_id => 17)
Edit: I don't know if this conforms to REST
It may be over the top, but you could do something like this:
class PhotoCopiesController < ApplicationController
def new
#photo = Photo.find(params[:photo_id]).dup
end
def create
end
end
and
resources :photo_copies, :only => [:new, :create]
and
= link_to 'Copy', photo_copy_path(:photo_id => #photo.id)
What I am trying to achieve is something similar to Github's way for routes. E.g. I have a project with the name 'question' results in the URL /hjuskewycz/question. So my goal is to have routes where the first segment is the username and the second the project's name.
I tried a couple of different approaches, this is the one I am stuck with right now:
scope ":username" do
resources :projects, :path => "" do
resources :pictures
end
end
Using
project_path :username => project.owner.username, :id => project.to_param
works as expected. However, it's tedious to always specify the username although it's always the owner's username. I would very much prefer
project_path(:id => project.to_param)
I know about default_url_options and url_for and I digged in the code. However, polymorphic_url doesn't use default_url_options.
I tried in routes.rb:
resources :projects, :path => "", :defaults => {:username => Proc.new { "just_testing" }}
since you can use a proc for constrains, but haven't got it working either.
I tried in project.rb
def to_param
"#{owner.username"/#{project.title}"
end
I spent already too much time on this problem and my current approach uses a convenience method to add the :username parameter. Nevertheless, I think using this method all over the place just to add an entry stinks (bad code smell). I wonder if there is a more elegant solution to this problem.
I think you should not make things complicated here, just use something like this:
In Routes.rb
match ':username/:projectname/' => 'projects#show_project' , :as => :show_project
and in project_controller, just define this
def show_project
#user =User.find_by_username(params[:username])
#project =Project.find_by_slug(params[:projectname])
end
Simpler is better, it saves time and easy to understand for others
You want to do something like this in your controller:
before_filter :set_username
def set_username
Rails.application.routes.default_url_options[:username] = #user.name
end
I am in the process of trying to use the update_attribute command, but struggling to get it working (at all) and hoped someone could point me in the right direction?
I have previously posted a question about this issue, it was very useful in terms of giving a feel for the mechanics of what is going on, but unfortunately it didn't actually get it working.
I have a database of items (Items), which among other things contains ':item_name', ':click_count' and ':external_url'.
Currently I have a view (Showselecteditems) in which there is a list of all the items, when a user clicks on an item name, they are directed to the appropriate external url. This works using the code:
<%= link_to selecteditem.item_name.to_s, selecteditem.external_url %>
I would now like to add the ability to count the number of times a particular item name has been clicked on (i.e. in total for all users, not individual users) and therefore the number of times each external url has been visited in order to work out which is most popular.
Reading around, I believe i need to modify the code above to something of the form:
<%= link_to selecteditem.item_name.to_s, selecteditem.external_url, {:controller => params[:controller], :action => clickcountplusone, :identifier => selecteditem.item_name} %>
And need to define this function somewhere - it seems to only be found if located in 'application_helper'?
def clickcountplusone
clickeditem = Items.find(params[:identifier])
clickeditem.update_attribute(:click_count, clickeditem.click_count + 1)
rescue ActiveRecord::RecordNotFound # to avoid error if no identifier value
end
Needless to say, I cannot get this to work... My question is therefore, how can I set things up correctly so that when the link is clicked on the count is incremented? The other common problem people seem to report is that the number will be incremented each time the page is refreshed, which I would like to avod if possible.
Previously people have suggested adding to the 'show' section of the 'Items' controller, however, i don't know how this would work as the links are being clicked on the Showselecteditems view page, not the database itself where you get the show, edit, destroy commands. Any advice greatly appreciated.
This
<%= link_to selecteditem.item_name.to_s, selecteditem.external_url, {:controller => params[:controller], :action => clickcountplusone, :identifier => selecteditem.item_name} %>
will not point user to the some_controller#clickcountplusone, because you already specified an external link.
The easiest way to do this job is to modify your link_to like:
<%= link_to selecteditem.item_name.to_s, {:controller => params[:controller], :action => clickcountplusone, :identifier => selecteditem.item_name} %>
And then to modify your actions source:
def clickcountplusone
clickeditem = Items.find(params[:identifier])
redirect_to clickeditem.external_url if clickeditem.update_attribute(:click_count, clickeditem.click_count + 1)
rescue ActiveRecord::RecordNotFound # to avoid error if no identifier value
end