Home > Articles > Programming > Ruby

This chapter is from the book Creating the Stock Portfolio Rails Application

Creating the Stock Portfolio Rails Application

The Stock Portfolio application is an online trading application that allows you to buy and sell stock. Of course, this sample application will walk you through what a RESTful Rails application is, even though it doesn’t include many aspects that a real-world trading application needs. The data we want to manage is the following: An account holds positions in stock, for example, 50 shares of Google and 20 shares of Adobe. Each position has many movements created when the stock is bought or sold. To get started, let’s create a new Rails application:

$ rails rails
$ cd rails

Now you can create the Account, Position, and Movements “resources” as follows:

$ ./script/generate scaffold Account name:string
$ ./script/generate scaffold Position account_id:integer quantity:integer ticker:string name:string
$ ./script/generate scaffold Movement price:float date:datetime quantity:integer position_id:integer operation:string 

In Rails terms, a resource is data exposed by your Rails application following a convention to access and manipulate the data via HTTP requests. From a code point of view, this translates to a controller that can be invoked to create, read, update, and delete the data, and the controller will access the active record of concern to perform the requested action. To access the controller methods, define in the routes configuration file the exposed resources; this definition will dictate which URL can be used to access these resources. We will do this step by step hereafter. Again, when we mention a resource, think of it as combination of the URLs to manipulate the data, the controller that exposes the data, and the active record used to store the data.

The script/generate command is a facility to create the files we need as a starting point. We need to apply several changes to the generated code to get a fully functional application. If you look at the script/generate commands above, we specified the Account, Position, and Movement resources, their attributes, and how the resources are linked to each other. The Movement resource has a position_id column that links the movements to the positions, and the Position resource has an account_id column that links the positions to the accounts. The script/generate command does not add the code either to associate the active records or to constrain the controllers. Let’s do that now. You can add it to the Account, Position, and Movement active records and add the has_many and belongs_to associations as follows:

class Account < ActiveRecord::Base
  has_many :positions, :dependent => :destroy
end

class Position < ActiveRecord::Base
  belongs_to :account
  has_many :movements, :dependent => :destroy
end

class Movement < ActiveRecord::Base
  belongs_to :position
end

This code will give you fully associated active records. Assuming you have some data in your database, you could, for example, find all the movements of the first position of the first account using the following Rails statement:

Account.first.positions.first.movements

Changing the active records was the easy part. The controllers will require more work because to respect and constrain the resource nesting, we want to ensure that the positions controller only returns positions for the specified account, and the movements controller only returns movements for the specified position. In other words, we want to have movements nested in positions and positions nested in accounts. Change the config/routes.rb file to the following:

ActionController::Routing::Routes.draw do |map|
  map.resources :accounts do |account|
    account.resources :positions do |position|
      position.resources :movements
    end
  end
end

Routes tells our application what URL to accept and how to route the incoming requests to the appropriate controller actions. By replacing three independent routes with nested routes, we indicate that, for example, the positions cannot be accessed outside the scope of an account. What URLs does the route file define now? From the command line, type the following rake command to find out:

$ rake routes | grep -v -E "(format|new|edit)"

The rake routes command gives you the list of all URLs as defined by your routes configuration file. We just pipe it into the grep command to remove from the list any extra URLs we don’t want at this stage. For the account resource, we now have the URLs shown in Table 3.1.

Table 3.1  URLs for the Account Resource

HTTP verb

URL

Controller

GET
/accounts
{:action=>"index", :controller=>"accounts"}
POST
/accounts
{:action=>"create", :controller=>"accounts"}
GET
/accounts/:id
{:action=>"show", :controller=>"accounts"}
PUT
/accounts/:id
{:action=>"update", :controller=>"accounts"}
DELETE
/accounts/:id
{:action=>"destroy", :controller=>"accounts"}

To access the positions, we need to prefix the URL with the account ID that nests the positions (see Table 3.2).

Table 3.2  Account IDs Added as Prefixes to the URLs

HTTP verb

URL

Controller

GET   
/accounts/:account_id/positions
{:action=>"index", :controller=>"positions"}
POST  
/accounts/:account_id/positions
{:action=>"create", :controller=>"positions"}
GET   
/accounts/:account_id/positions/:id
{:action=>"show", :controller=>"positions"}
PUT   
/accounts/:account_id/positions/:id
{:action=>"update", :controller=>"positions"}
DELETE
/accounts/:account_id/positions/:id
{:action=>"destroy", :controller=>"positions"}

Finally, we need to prefix the URL with the account and position that nests the movements (see Table 3.3).

Table 3.3  URL Prefixes to Nest the Movements

HTTP verb

URL

Controller

GET   
/accounts/:account_id/
positions/:position_id/movements
{:action=>"index", :controller=>"movements"}
POST  
/accounts/:account_id/
positions/:position_id/movements
{:action=>"create", :controller=>"movements"}
GET   
/accounts/:account_id/
positions/:position_id/movements/:id
{:action=>"show", :controller=>"movements"}
PUT   
/accounts/:account_id/
positions/:position_id/movements/:id
{:action=>"update", :controller=>"movements"}
DELETE
/accounts/:account_id/
positions/:position_id/movements/:id
{:action=>"destroy", :controller=>"movements"}

List all the movements of the first position of the first account, for example, by using the following URL: http://localhost:3000/accounts/1/positions/1/movements.

Defining the routes makes sure the application supports the nested URLs. However, we now need to modify the controllers to enforce implementation of this nesting, so we’ll add such constraints to all the controllers. But first, let’s remove the HTML support from our controllers because, in our case, we want the Rails application to only serve XML, and we don’t need to worry about supporting an HTML user interface. Let’s simply remove the respond_to block from our controllers and keep the code used in the format.xml block. For example, we change the index method from the following:

class AccountsController < ApplicationController
  def index
    @accounts = Account.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @accounts }
    end
  end
end

to the following:

class AccountsController < ApplicationController
  def index
    @accounts = Account.find(:all)
    render :xml => @accounts
  end
end

You can effectively consider the respond_to as a big switch in all your controller methods that provide support for the different types of invocations, such as rendering either HTML or XML. To constrain the positions controller, we will add before_filter, which will find the account from the request parameters and only query the positions of that account. Change the index method from the following implementation:

class PositionsController < ApplicationController
  def index
    @positions = Position.find(:all)

    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render :xml => @positions }
    end
  end
end

to this one:

class PositionsController < ApplicationController
  before_filter :get_account
  def index
    @positions = @account.positions.find(:all)
    render :xml => @positions.to_xml(:dasherize=>false)
  end
  protected
  def get_account
    @account = Account.find(params[:account_id])    
  end
end

The Position.find(:all) was changed to @account.positions.find(:all). This change ensures that only the positions for the specific account instance are returned.

The before filter loads that account for each method. We also are modifying the format of the returned XML to use underscores instead of dashes in the XML element names to better accommodate Flex, as explained in Chapter 2. When requesting the http://localhost:3000/accounts/1/positions URL, the controller now returns an XML list of all the positions with an ID of 1 that belong to the account. Now we do the same with the movements controller and scope the movements to a specific account and position, as follows:

class MovementsController < ApplicationController
  before_filter :get_account_and_position    
  def index
    @movements = @position.movements.find(:all)
    render :xml => @movements.to_xml(:dasherize => false)
  end
  protected
  def get_account_and_position  
    @account = Account.find(params[:account_id])
    @position = @account.positions.find(params[:position_id])
  end
end

So when requesting the http://localhost:3000/accounts/1/positions/1/movements URL, the controller returns an XML list of all the movements of the given position from the given account. First the account is retrieved, and then the positions from that account are queried, enforcing the scope of both the account and the position. Don’t directly query the positions by using Position.find(params[:position_id]) or a similar statement because the users could tamper with the URL and query the positions of a different account.

Before changing the rest of the methods, let’s do some planning and see how we will use all the different controllers. Table 3.4 gives an overview of all the actions for our three controllers.

Table 3.4  Overview of Actions of the Three Controllers

Controller Method

Accounts Controller

Positions Controller

Movements Controller

Index

All accounts

All positions for account

All movements for position in account

Show

Not used

Not used

Not used

New

Not used

Not used

Not used

Edit

Not used

Not used

Not used

Create

Creates an account

Buy existing stock

Not used

Update

Updates an account

Not used

Not used

Destroy

Deletes the account

Sell stock

Not used

Customer verbs

None

Buy

None

For our application, several nonrelevant methods don’t apply when rendering XML that would apply when supporting an HTML user interface. For example, the controller doesn’t need to generate an edit form because the Flex application maps the XML to a form. In the same way, we don’t need the new action, which returns an empty HTML entry form. Additionally, as in our case, since the index method returns all the attributes of each node, we don’t really need the show method because the client application would already have that data. We don’t use the show, new, and edit methods for all three controllers, so we can delete them.

For the positions controller, we won’t update a position; we will simply buy new stock and sell existing stock, meaning we are not using the update method. We also differentiate buying new stock and buying existing stock, because for existing stock, we know the ID of the position and find the object using an active record search. But, for a new stock position, we pass the stock ticker and we create the new position, which may not save and validate if the ticker is invalid. Therefore, to support these two different usage patterns, we decided to use two different actions: we use the create action for existing stock, and we add the custom buy verb to the positions controller to buy new stock.

The movements controller doesn’t enable any updates since movements are generated when buying and selling positions, so only the index method is significant. Providing such a mapping table of the verbs serves as a good overview of the work you will do next. First, you can remove all unused methods. As you already implemented the index methods earlier in the chapter, we are left with seven methods, three for the accounts controller and four for the positions controller. Let’s dive into it. For the accounts controller, in the create, update, and destroy methods, we simply remove the respond_to blocks and keep only the XML rendering.

class AccountsController < ApplicationController
      def create
        @account = Account.new(params[:account])
        if @account.save
          render :xml => @account, :status => :created, :location => @account
        else
          render :xml => @account.errors, :status => :unprocessable_entity
        end
      end

      def update
        @account = Account.find(params[:id])
        if @account.update_attributes(params[:account])
          head :ok
        else
          render :xml => @account.errors, :status => :unprocessable_entity
        end
      end

      def destroy
        @account = Account.find(params[:id])
        @account.destroy
        head :ok
      end
end

We saw earlier that the positions controller index method was relying on the @account variable set by the get_account before_filter to only access positions for the specified account. To enforce the scoping to a given account, the remaining methods of the positions controller will also use the @account active record to issue the find instead of directly using the Position.find method. Let’s go ahead and update the create and destroy methods and add a buy method, as follows:

 class PositionsController < ApplicationController
  def create
    @position = @account.positions.find(params[:position][:id])
    if @position.buy(params[:position][:quantity].to_i)
      render :xml => @position, :status => :created, 
             :location => [@account, @position]
    else
      render :xml => @position.errors, :status => :unprocessable_entity
    end
  end

  def destroy
    @position = @account.positions.find(params[:position][:id])
    if @position.sell(params[:position][:quantity].to_i)
      render :xml => @position, :status => :created, 
             :location => [@account, @position]
    else
      render :xml => @position.errors, :status => :unprocessable_entity
    end
  end

   def buy    
    @position = @account.buy(params[:position][:ticker],
                             params[:position][:quantity].to_i)  
    if @position.errors.empty?
      head :ok
    else
      render :xml => @position.errors, :status => :unprocessable_entity
    end
end

For the buy method, we simply use the ticker and invoke the buy method from the account active record:

class Account < ActiveRecord::Base
  has_many :positions, :dependent => :destroy
  def buy(ticker, quantity)
    ticker.upcase!
    position = positions.find_or_initialize_by_ticker(ticker)
    position.buy(quantity)
    position.save
    position
  end  
end

The Account#buy method in turn calls the position buy method, which in turn creates a movement for the buy operation.

 class Position < ActiveRecord::Base
  belongs_to :account
  has_many :movements, :dependent => :destroy
  
 def buy(quantity) 
    self.quantity ||= 0
    self.quantity = self.quantity + quantity;
    movements.build(:quantity => quantity, :price => quote.lastTrade,
                    :operation =>''bu'')
    save
  end
end

Now let’s extend the position active record to add a validation that will be triggered when saving the position. The first validation we add is the following:

validates_uniqueness_of :ticker, :scope => :account_id

This check simply ensures that one account cannot have more than one position with the same name. We verify that the ticker really exists by using the yahoofinance gem. Install it first:

$ sudo gem install yahoofinance

To make this gem available to our application we can create the following Rails initializer under config/initializers/yahoofinance.rb that requires the gem:

require''yahoofinanc''

That’s it. Now we can write a before_validation_on_create handler that will load the given stock information from Yahoo Finance, and then we add a validation for the name of the stock, which is set by the handler only if the stock exists.

class Position < ActiveRecord::Base
  validates_uniqueness_of :ticker, :scope => :account_id
  validates_presence_of :name, 
                        :message => "Stock not found on Yahoo Finance."
  before_validation_on_create :update_stock_information

      protected

      def quote
        @quote ||= YahooFinance::get_standard_quotes(ticker)[ticker]
      end

      def update_stock_information
          self.name = @quote.name if quote.valid?
        end
      end
end

When referring to the quote method, the instance variable @quote is returned if it exists, or if it doesn’t exist, the stock information is retrieved from Yahoo Finance using the class provided by this gem:

YahooFinance::get_standard_quotes(ticker)

The get_standard_quotes method can take one or several comma-separated stock symbols as a parameter, and it returns a hash, with the keys being the ticker and the values being a StandardQuote, a class from the YahooFinance module that contains financial information related to the ticker, such as the name, the last trading price, and so on. If the ticker doesn’t exist, then the name of the stock is not set and the save of the position doesn’t validate.

The sell method of the positions controller is similar to the buy method, but less complex. Let’s take a look:

 class Position < ActiveRecord::Base
  def sell(quantity)
    self.quantity = self.quantity - quantity
    movements.build(:quantity => quantity, :price => quote.lastTrade,
                    :operation =>''sel'')
    save
  end
end

Similar to the buy method, the sell method updates the quantity and creates a sell movement, recording the price of the stock when the operation occurs. There is one last thing: we need to add the custom buy verb to our routes. Do this by adding the :collection parameter to the positions resource.

ActionController::Routing::Routes.draw do |map|
   
   map.resources :accounts do |account|
     account.resources :positions, :collection => {:buy => :post} do |position|
       position.resources :movements
     end
   end

This indicates that no ID for the position is specified when creating the URL, thus invoking the buy verb on the positions collection. The URL would look something like this:

/accounts/1/positions/buy

If you wanted to add a custom verb that applies not only to the collection of the positions, but also to a specific position, thus requiring the position ID in the URL, you could have used the :member parameter to the positions resource.

Our application starts to be functional. By now, you certainly did a migration and started playing with your active records from the console. If not, play around a little, then keep reading because we are about to start the Flex part of our application.


© Copyright Pearson Education. All rights reserved.

InformIT Promotional Mailings & Special Offers

I would like to receive exclusive offers and hear about products from InformIT and its family of brands. I can unsubscribe at any time.

Overview


Pearson Education, Inc., 221 River Street, Hoboken, New Jersey 07030, (Pearson) presents this site to provide information about products and services that can be purchased through this site.

This privacy notice provides an overview of our commitment to privacy and describes how we collect, protect, use and share personal information collected through this site. Please note that other Pearson websites and online products and services have their own separate privacy policies.

Collection and Use of Information


To conduct business and deliver products and services, Pearson collects and uses personal information in several ways in connection with this site, including:

Questions and Inquiries

For inquiries and questions, we collect the inquiry or question, together with name, contact details (email address, phone number and mailing address) and any other additional information voluntarily submitted to us through a Contact Us form or an email. We use this information to address the inquiry and respond to the question.

Online Store

For orders and purchases placed through our online store on this site, we collect order details, name, institution name and address (if applicable), email address, phone number, shipping and billing addresses, credit/debit card information, shipping options and any instructions. We use this information to complete transactions, fulfill orders, communicate with individuals placing orders or visiting the online store, and for related purposes.

Surveys

Pearson may offer opportunities to provide feedback or participate in surveys, including surveys evaluating Pearson products, services or sites. Participation is voluntary. Pearson collects information requested in the survey questions and uses the information to evaluate, support, maintain and improve products, services or sites, develop new products and services, conduct educational research and for other purposes specified in the survey.

Contests and Drawings

Occasionally, we may sponsor a contest or drawing. Participation is optional. Pearson collects name, contact information and other information specified on the entry form for the contest or drawing to conduct the contest or drawing. Pearson may collect additional personal information from the winners of a contest or drawing in order to award the prize and for tax reporting purposes, as required by law.

Newsletters

If you have elected to receive email newsletters or promotional mailings and special offers but want to unsubscribe, simply email information@informit.com.

Service Announcements

On rare occasions it is necessary to send out a strictly service related announcement. For instance, if our service is temporarily suspended for maintenance we might send users an email. Generally, users may not opt-out of these communications, though they can deactivate their account information. However, these communications are not promotional in nature.

Customer Service

We communicate with users on a regular basis to provide requested services and in regard to issues relating to their account we reply via email or phone in accordance with the users' wishes when a user submits their information through our Contact Us form.

Other Collection and Use of Information


Application and System Logs

Pearson automatically collects log data to help ensure the delivery, availability and security of this site. Log data may include technical information about how a user or visitor connected to this site, such as browser type, type of computer/device, operating system, internet service provider and IP address. We use this information for support purposes and to monitor the health of the site, identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents and appropriately scale computing resources.

Web Analytics

Pearson may use third party web trend analytical services, including Google Analytics, to collect visitor information, such as IP addresses, browser types, referring pages, pages visited and time spent on a particular site. While these analytical services collect and report information on an anonymous basis, they may use cookies to gather web trend information. The information gathered may enable Pearson (but not the third party web trend services) to link information with application and system log data. Pearson uses this information for system administration and to identify problems, improve service, detect unauthorized access and fraudulent activity, prevent and respond to security incidents, appropriately scale computing resources and otherwise support and deliver this site and its services.

Cookies and Related Technologies

This site uses cookies and similar technologies to personalize content, measure traffic patterns, control security, track use and access of information on this site, and provide interest-based messages and advertising. Users can manage and block the use of cookies through their browser. Disabling or blocking certain cookies may limit the functionality of this site.

Do Not Track

This site currently does not respond to Do Not Track signals.

Security


Pearson uses appropriate physical, administrative and technical security measures to protect personal information from unauthorized access, use and disclosure.

Children


This site is not directed to children under the age of 13.

Marketing


Pearson may send or direct marketing communications to users, provided that

  • Pearson will not use personal information collected or processed as a K-12 school service provider for the purpose of directed or targeted advertising.
  • Such marketing is consistent with applicable law and Pearson's legal obligations.
  • Pearson will not knowingly direct or send marketing communications to an individual who has expressed a preference not to receive marketing.
  • Where required by applicable law, express or implied consent to marketing exists and has not been withdrawn.

Pearson may provide personal information to a third party service provider on a restricted basis to provide marketing solely on behalf of Pearson or an affiliate or customer for whom Pearson is a service provider. Marketing preferences may be changed at any time.

Correcting/Updating Personal Information


If a user's personally identifiable information changes (such as your postal address or email address), we provide a way to correct or update that user's personal data provided to us. This can be done on the Account page. If a user no longer desires our service and desires to delete his or her account, please contact us at customer-service@informit.com and we will process the deletion of a user's account.

Choice/Opt-out


Users can always make an informed choice as to whether they should proceed with certain services offered by InformIT. If you choose to remove yourself from our mailing list(s) simply visit the following page and uncheck any communication you no longer want to receive: www.informit.com/u.aspx.

Sale of Personal Information


Pearson does not rent or sell personal information in exchange for any payment of money.

While Pearson does not sell personal information, as defined in Nevada law, Nevada residents may email a request for no sale of their personal information to NevadaDesignatedRequest@pearson.com.

Supplemental Privacy Statement for California Residents


California residents should read our Supplemental privacy statement for California residents in conjunction with this Privacy Notice. The Supplemental privacy statement for California residents explains Pearson's commitment to comply with California law and applies to personal information of California residents collected in connection with this site and the Services.

Sharing and Disclosure


Pearson may disclose personal information, as follows:

  • As required by law.
  • With the consent of the individual (or their parent, if the individual is a minor)
  • In response to a subpoena, court order or legal process, to the extent permitted or required by law
  • To protect the security and safety of individuals, data, assets and systems, consistent with applicable law
  • In connection the sale, joint venture or other transfer of some or all of its company or assets, subject to the provisions of this Privacy Notice
  • To investigate or address actual or suspected fraud or other illegal activities
  • To exercise its legal rights, including enforcement of the Terms of Use for this site or another contract
  • To affiliated Pearson companies and other companies and organizations who perform work for Pearson and are obligated to protect the privacy of personal information consistent with this Privacy Notice
  • To a school, organization, company or government agency, where Pearson collects or processes the personal information in a school setting or on behalf of such organization, company or government agency.

Links


This web site contains links to other sites. Please be aware that we are not responsible for the privacy practices of such other sites. We encourage our users to be aware when they leave our site and to read the privacy statements of each and every web site that collects Personal Information. This privacy statement applies solely to information collected by this web site.

Requests and Contact


Please contact us about this Privacy Notice or if you have any requests or questions relating to the privacy of your personal information.

Changes to this Privacy Notice


We may revise this Privacy Notice through an updated posting. We will identify the effective date of the revision in the posting. Often, updates are made to provide greater clarity or to comply with changes in regulatory requirements. If the updates involve material changes to the collection, protection, use or disclosure of Personal Information, Pearson will provide notice of the change through a conspicuous notice on this site or other appropriate way. Continued use of the site after the effective date of a posted revision evidences acceptance. Please contact us if you have questions or concerns about the Privacy Notice or any objection to any revisions.

Last Update: November 17, 2020