Cucumber and webrat - How to handle dynamic URLs in the paths.rb? - ruby-on-rails

I am using Cucumber for BDD development in my Ruby on Rails project and I'm running into some confusion on how the path.rb handles paths used in rails applications.
Given I have:
class Parent < ActiveRecord::Base
has_many :children
end
class Child < ActiveRecord::Base
belongs_to :parent
end
and I have the following Cucumber feature:
Scenario: A test feature
Given I am on the parent page
When I follow "Link to Children"
Then I should be on the children list page
with the path defined as:
def path_to(page_name)
case page_name
when /the children list page/
'/parents/:id/children'
end
The problem I come across is the following error when running the feature:
Spec::Expectations::ExpectationNotMetError: expected: "/parents/:id/children",
got: "/parents/1726/children" (using ==)
I don't really care what the :id is. What should I do instead? Is this even possible with the default web steps? Am I thinking about the problem in the wrong way?

The way I do it, which may not be the best way is as follows:
when /the children list page for "(.+)"/
p = Parent.find_by_name($1)
parent_children_path(p)

In our app, we always wanted a new record in the database whenever a user clicked the "new" button. Thus, our controller's new action automatically calls create and then redirects to the edit action.
We faced a similar problem in testing, when we didn't care so much about what the ID was -- just that it got to the edit page for the app.
Here's what I came up with.
(NOTE: The step definition is written using capybara, but it shouldn't be too different from webrat)
Then /^(?:|I )should now be editing the (.*)$/ do |model|
id = find_by_id("#{model}_id").value
Then "I should be on the edit #{model} page for \"#{id}\""
end
The basic premise is that when you're on a Rails edit page, there will be a form for the model you're editing. That form always contains a hidden field with the ID of the specific record you're editing.
The step finds the hidden field, extracts the ID from it, and then looks for a web_step to resolve the path for that model.
Just make sure you have a path that matches for the model you're looking up.
when /the edit person page for "([^\"]*)"/
edit_person_path($1)

Related

Ruby on Rails validation bypass and confusion

I am facing a strange thing on Ruby on Rails and I don't know what is the correct way to deal with this situation.
I have two models, Book and Page.
Book(name:string)
Page(page_number: integer, book_id: ID)
class Book < ActiveRecord::Base
has_many :pages
accepts_nested_attributes_for :pages
end
class Page < ActiveRecord::Base
belongs_to :book
validates_uniqueness_of :page_number, scope: :book_id
end
I have created a view from where I can update a book. I accept nested attributes for pages and there is a section where I can update book pages as well as add new pages (by a Javascript function that lets the user to add a new page row by clicking + button). As you can see I have a validation that requires the page number to be unique for a certain book to prevent duplication. I have also defined an unique index on the database level (using Postgres)
On my update action in Book's Controller I have:
def update
#book = Book.find(params[:id])
if #book.update_attributes(params[:book])
flash[:notice] = 'Book successfully modified!'
end
end
The problem with my approach is that sometimes the validation about the page_number that I have defined on Pages model is bypassed and I get an error directly from PG ("PG::UniqueViolation: ERROR: duplicate key value violates unique constraint").
This scenario happens on 2 cases:
Trying to create two or more pages with the same number directly on one form submission
Updating existing page_number (ex from page_number: 4 to nr: 5) and creating one new page with number 4 on one form submission.
It seems that there is some problem of concurrency and order of processing the updates/creates.
For the point 2, we should somehow tell Rails to look over all records and see if there we are trying to do any duplication by combining updates with creates. Point 2 is a valid option and there should be no validation error thrown but because Rails is doing creation first it laments that a page with page_number 4 already exists (without taking into considerance that page_number 4 is being updated to 5)
I would be thankful I you could advice me how to handle this situation and be able to predict all use cases so that I do not hit the database in case of a validation error.
If this is impossible, is there a way that I can catch the error from Postgres, format and display it to the user?
I appreciate any advice!

Rails: MVC How to use custom actions

I have a table output from entries using the rails generated scaffold: CRUD ops.
If I want to make another action on the table like the default "Show, Edit, Destory" like a library book "check in", that will update the status to "checked in"...
What would be the proper way to use the model and controller to update? (Using mongodb)
Better stated: What's the best way to have many custom actions? Think of it like many multi purpose "Facebook Likes".
On the table, list of actions "Punch this", "Check out this"...
There are lots of ways to handle this, but I typically like to isolate actions like this in their own controller action with it's own route.
Model
To keep things tidy I recommend adding a method to the model that updates the attribute you are concerned about. If you aren't concerned with validation you can use update_attribute. This method skips validations and saves to the database
class LibraryBook < ActiveRecord::Base
def check_in!
self.update_attribute(:checked_in, true)
end
end
View
You'll need to update the index.html.erb view to add the link to update the individual record. This will also require adding a route. Since you are updating the record you will want to use the PUT HTTP verb.
routes.rb
resources :library_books do
match :check_in, on: :member, via: :put # creates a route called check_in_library_book
end
index.html.erb
Add the link
link_to check_in_library_book_path(library_book), method: :put
Controller
Now you need to add the action within the controller that calls the #check_in! method.
class LibraryBooksController < ApplicationController
def check_in
#library_book = LibraryBook.find(params[:id])
if #library_book.check_in!
# Handle the success
else
# Handle the Failure
end
end
end
In my opinion, the best way to handle status workflows like this is to think about it in terms of events, and then just think of status as most recent event. I usually create an event_type table with a name and code (so, e.g. Check In and CHECK_IN for name and code, respectively), and then an event table with an event_type_id, timestamp, and usually some kind of user id, or IP address, or both.
Then you could say something like this:
class Thing < ActiveRecord::Base
has_many :events
def status
events.order("created_at DESC").first.event_type.name
end
end
There are also "audit trail" gems out there, but in my (limited) experience they aren't very good.
This doesn't speak to MongoDB, and may in fact be incompatible with Mongo, but hopefully it at least points you in the right direction or gives you some ideas.

Rails has_one relationship safe resource.build_resource in controller

I have a page that takes a user through a short sign up tutorial when they create their account in order to create their first resource. In my app, :hotel belongs to :user, and :user has_one hotel. For the tutorial page, in my controller, I have:
#hotel = current_user.build_hotel
Which works, except that it a user somehow finds him back on the tutorial page that command disassociates their previously created hotel. In other words, the second time the user accesses the page with:
#hotel = current_user.build_hotel
The user_id field in the hotel they created the first time becomes nil. Obviously that is a serious problem. I can do a before_filter on that page, but I'm not very happy about having a way for the user to screw everything up simply by visiting a page. How should I properly use the build command for a has_one relationship?
You can test for the existence of a hotel before building it:
#hotel = current_user.hotel || current_user.build_hotel

Mongoid + Cucumber

I try to run a scenario with Cucumber, through Capybara, in a Rails 3.2.3 app supported by Mongoid. The aim is to have current user add a book to his collection.
Everything goes ok, but the final step definition, where I check that the amount of books is now one, fails.
But if I check on the app controller, the size actually increased. And actually, when I send reload to the user in the step definition, it passes:
user.reload.books(true).size.should == 1
I'm afraid this behavior could harm my app once in production. Any advice how to make sure all tests and app behaviors are consistent?
UPDATE
I checked the test.log to see what's going on.
Calling reload I get this query to MongoDB:
find({"count"=>"books",
"query"=>{:_id=>{"$in"=>[BSON::ObjectId('4f889b473dffd63235000004')]}},
"fields"=>nil}).limit(-1)
while without the reload I get this:
find({"count"=>"books", "query"=>{:_id=>{"$in"=>[]}}, "fields"=>nil}).limit(-1)
It practically doesn't query against the user if I don't reload the model, which doesn't make much sense to me.
The following works for me (updated with actual Cucumber example)
I built a Rails project to test out your issue, rails 3.2.3, mongoid 2.4.8, mongo 1.6.2, mongodb 2.0.4, cucumber 1.1.9.
The following (association generated methods) work as expected, without need for refresh:
user.books << book
book.users << user
Then I tried to bypass the association, which was what I thought that you were doing.
user.push(:book_ids, book.id)
book.push(:user_ids, user.id)
These DO bypass the association, resulting in incomplete (one-way instead of two-way) references, but the memory and db state is consistent. So my guess in my previous answer about what you were experiencing was wrong, there's no refresh needed and you are probably doing something else. Note that you/we do not want the incomplete references, please do not push directly to the internals for Mongoid referenced relations.
Are you using the association append "<<" for adding a user or a book? My current conclusion is that Mongoid referenced relations work as advertized for my test of your issue. There's no need for refresh.
Here's the model:
class User
include Mongoid::Document
field :first_name, type: String
field :last_name, type: String
has_and_belongs_to_many :books
end
class Book
include Mongoid::Document
field :title, type: String
field :author, type: String
has_and_belongs_to_many :users
end
Cucumber feature
Feature: UserAndBook
Test adding a book to a user_s books
Scenario: add_book_to_user
Given starting with no users and no books
And a new user
And that the new user has no books
And a new book
And add book to user
Then I can check that the user has a book
Cucumber steps
require 'test/unit/assertions'
require File.expand_path('../../../test/test_helper', __FILE__)
World(Test::Unit::Assertions)
Given 'starting with no users and no books' do
User.delete_all
Book.delete_all
assert_equal(0, User.count)
assert_equal(0, Book.count)
end
Given 'a new user' do
#user = User.create(first_name: 'Gary', last_name: 'Murakami')
end
Given 'that the new user has no books' do
assert_equal(0, #user.books.count)
end
Given 'a new book' do
#book = Book.create(title: 'A Tale of Two Cities', author: 'Charles Dickens')
end
Given 'add book to user' do
#user.books << #book
end
Then 'I can check that the user has a book' do
assert_equal(1, #user.books.count)
end
I'm open to further exchange of info to help to address your issue.
Blessings,
-Gary
P.S. Looking at the log, it is interesting to see that user.books.length does an actual db "find count $in" query rather than a local array length.
Previous answer
You've pretty much answered your own question. In Rails, you need to use the reload method whenever data for your model has changed in the database, otherwise you will just be looking at the previously loaded/instantiated/cached state of your model. With update of just an attribute, things look pretty consistent, but associations are more complicated and the inconsistency becomes more obvious.

How to let user modifying content of pages with rails ? Query for text?

I'm developing a website with Ruby on Rails.
I want to find the better way to let users (not developers) to edit text on some pages (like the index...). (like a CMS ?)
Actually they had to get the page through FTP, to edit the text and to put the new file on the server (through FTP).
It's a very very bad practice and I wanted to know if someone has an idea to solve this problem ?
Many thanks
It would be the same as the basic Rails CRUD operations. Just make a model/controller representing page content, and an edit view for the controller. Then on the pages you want text to be editable, instead of having the content directly on the page just use a view partial.
Of course, you would probably also want to implement some type of authentication to make sure not just everyone can edit pages.
Well, one thing you could do is add a model to your database called "Content" or "Copy" which represents some text on a page. Then you could use polymorphic association to link the content/copy to your actual model. For instance, if you had a page with a list of products on it, you'd likely have a Product model in your database. You could do something like this:
class Content < ActiveRecord::Base
belongs_to :contentable, :polymorphic => true # excuse my lame naming here
# this model would need two fields to make it polymorphic:
# contentable_id <-- Integer representing the record that owns it
# contentable_type <-- String representing the kind of model (Class) that owns it
# An example would look like:
# contentable_id: 4 <--- Product ID 4 in your products table
# contentable_type: Product <--- Tells the rails app which model owns this record
# You'd also want a text field in this model where you store the page text that your
# users enter.
end
class Product < ActiveRecord::Base
has_many :contents, :as => :contentable # again forgive my naming
end
In this scenario, when the product page renders, you could call #product.contents to retrieve all the text users have entered for this product. If you don't want to use two separate models like this, you could put a text field directly on the Product model itself and have users enter text there.

Resources