Skip to Content
Author's profile photo Former Member

Ruby on Rails with AJAX

Since I wrote an SAP on Rails, and not on the skids about integration of SAP and Ruby on Rails , it has been great to see the beginnings of a kernel of interest.  As a result I decided to package up the sap4rails code and distribute it properly on RAA.


Rails and AJAX


One of the interesting things that Ruby on Rails provides is built AJAX functionality by virtue of an API over prototype , and Scriptalicious .  In this blog, I would like to show how neatly this integration is implemented in RubyOnRails, using a simple example of Locking, and Unlocking SAP R/3 user accounts.


Installation Requirements


For this example you will need to do your own install of Ruby On Rails.  Use the instructions for installing Ruby, Ruby On Rails, the RFCSDK, and saprfc in the RadRails – another one for the Box of Tricks blog.

Just be sure to install versions Ruby 1.8.4+ and Rails 1.1.2+.


Once you have got this far, the last thing to install is sap4rails.  You can either install the source package or download the gem, and install this with:

UserAdmin


For this example, most of the standard BAPIs are adequate.  We need to be able to list users, with their details, including their lock state.  We also need to be able to lock and unlock them.

The general functionality of the application is to create two lists of users
on a page – locked and unlocked – and for you to be able to drag a user from
one to the other to change their lock state in SAP.

The following RFCs are used:

    • BAPI_USER_GETLIST – list users, and their address details
    • BAPI_USER_LOCK
    • BAPI_USER_UNLOCK

BAPI_USER_GETLIST is not quite enough. This, I have had to wrap in another function module and also modify the results table to include the lock status information of users.


Create a new function module called Z_BAPI_USER_GETLIST, and make sure that
you activate it for RFC on the attributes tab (in SE37) (code) .


Create a new structure called ZBAPIUSNAME, and include the two structures BAPIUSNAME, and USLOCK like this !http://www.piersharding.com/download/ruby/rails/zbapiusname.thumb.png!.

Make sure that you activate the structure.

The code and interface needs to be completed like this:



FUNCTION Z_BAPI_USER_GETLIST.
*”—-


“Local Interface:
*”  IMPORTING
*”     VALUE(MAX_ROWS) TYPE  BAPIUSMISC-BAPIMAXROW DEFAULT 0
*”     VALUE(WITH_USERNAME) TYPE  BAPIUSMISC-WITH_NAME DEFAULT SPACE
*”  EXPORTING
*”     VALUE(ROWS) TYPE  BAPIUSMISC-BAPIROWS
*”  TABLES
*”      SELECTION_RANGE STRUCTURE  BAPIUSSRGE OPTIONAL
*”      SELECTION_EXP STRUCTURE  BAPIUSSEXP OPTIONAL
*”      USERLISTLOCK STRUCTURE  ZBAPIUSNAME OPTIONAL
*”      RETURN STRUCTURE  BAPIRET2 OPTIONAL
*”—-


*
data:
  LOCKSTATE LIKE  USLOCK,
  userlist like bapiusname occurs 0 with header line.


  refresh userlistlock.


  CALL FUNCTION ‘BAPI_USER_GETLIST’
    EXPORTING
      MAX_ROWS              = 0
      WITH_USERNAME         = with_username
    IMPORTING
      ROWS                  = rows
    TABLES
      SELECTION_RANGE       = selection_range
      SELECTION_EXP         = selection_exp
      USERLIST              = userlist
      RETURN                = return
            .


  loop at userlist.
    move-corresponding: userlist to userlistlock.
    if userlistlock-firstname = space.
       userlistlock-firstname = userlistlock-username.
    endif.


    CALL FUNCTION ‘SUSR_USER_LOCKSTATE_GET’
      EXPORTING
        USER_NAME                 = userlist-username
      IMPORTING
        LOCKSTATE                 = lockstate
      EXCEPTIONS
        USER_NAME_NOT_EXIST       = 1
        OTHERS                    = 2
              .


    move-corresponding: lockstate to userlistlock.
    append userlistlock.


  endloop.


ENDFUNCTION.

Activate the function module and test it.


The Rails part



The full application can be downloaded from here – but what I’d
like to do is quickly describe the meat of what had to be done to get this
type of application working.
</p>

Config – sap.yml



As described in the RadRails – another one for the Box of Tricks blog, you need to adjust the configuration in
config/sap.yml to point to your SAP system:


development:
ashost: 10.1.1.1
sysnr: "00"
client: "010"
user: developer
passwd: developer
lang: EN
trace: "1"
...


Model – sap_user.rb


The SapUser object now inherits from the new SAP4Rails::Base class. This
serves to automatically take care of managing RFC connections based on the
config done above. The two main class methods for use are function_module,
which allows you to declare what RFCs you want to use, and parameter which is
a helper method for declaring attributes of a SapUser (or any other Model
object).

In the interests of simplifying the application, by reducing the amount of
ABAP code to be written, and the number of RFC calls to be made, I have in a
way “cheated”, with the arrangement of methods defined in SapUser. Instead of
having a SapUser#find method, I rely on the use of SapUser#find_all and
SapUser#find_cache to reduce a series of SapUser searches down to one RFC call
only. In reality this is probably not good practice, but it suits for this
example.

Read the code comments below for further details:



require_gem “sap4rails”


class SapUser < SAP4Rails::Base


  1. You must define a list of RFCs to preload

  function_module :Z_BAPI_USER_GETLIST,
                     :BAPI_USER_LOCK,
                     :BAPI_USER_UNLOCK


  1. You must define a list of attribute accessors to preload

  parameter :last, :first, :userid, :locked


  1. do your attribute initialisation for each SapUser instance

     def initialize(last, first, userid, locked)
       @last = last
       @first = first
       @userid = userid
       @locked = locked
       @changed = false
     end


  1. what is the lock state

     def locked?
       return self.locked ? true : false
     end


  1. on #save – flip the lock state of the SapUser, calling the
  2. appropriate RFC to do it

  def save()
    RAILS_DEFAULT_LOGGER.warn(“[SapUser]#save what did we get: ” + self.inspect)
  
    if self.locked?
      SapUser.BAPI_USER_LOCK.reset()
      SapUser.BAPI_USER_LOCK.username.value = self.userid
      SapUser.BAPI_USER_LOCK.call()
    else
      SapUser.BAPI_USER_UNLOCK.reset()
      SapUser.BAPI_USER_UNLOCK.username.value = self.userid
      SapUser.BAPI_USER_UNLOCK.call()
    end


   

  1. just so something happens …

    return true
  end


  1. one RFC call to get them all

  def self.find_all
    RAILS_DEFAULT_LOGGER.warn(“[SapUser]#find_all “)
    SapUser.Z_BAPI_USER_GETLIST.reset()
    SapUser.Z_BAPI_USER_GETLIST.with_username.value = ‘X’
    SapUser.Z_BAPI_USER_GETLIST.call()
    users = []
    SapUser.Z_BAPI_USER_GETLIST.userlistlock.rows().each {|row|
            next if row[‘FIRSTNAME’].strip.length == 0
            state = nil
            if row[‘WRNG_LOGON’] == “L” ||
                     row[‘LOCAL_LOCK’] == “L” ||
                     row[‘GLOB_LOCK’] == “L”
              state = true
               else
              state = false
            end
            users.push(SapUser.new(row[‘LASTNAME’],
                                row[‘FIRSTNAME’],
                             row[‘USERNAME’],
                          state))
          }
    return users
  end


  1. find a user base on the results of a SapUser#find_all

  def self.find_cache(user, cache)
    RAILS_DEFAULT_LOGGER.warn(“[SapUser]#find_cache: #The specified item was not found. “)
    cache.each{|row|
       return row if user.strip == row.userid.strip
    }
  end


  1. get a list of all the locked users

  def self.find_locked
    RAILS_DEFAULT_LOGGER.warn(“[SapUser]#find_locked “)
    locked = []
    find_all().each{|user|
       locked.push(user) if user.locked
    }
    return locked
  end


  1. get a list of all the unlocked users

  def self.find_unlocked
    RAILS_DEFAULT_LOGGER.warn(“[SapUser]#find_unlocked “)
    unlocked = []
    find_all().each{|user|
       unlocked.push(user) unless user.locked
    }
    return unlocked
  end
end


Controller – lock_controller.rb


There are only 3 basic actions to the only controller in this application.
The initial list action, build the starting page presenting the two lists of
users (locked and unlocked).  From there, as a result of the AJAX enabled
calls from the dragndrop feature, two further actions are called – set_locked
and set_unlocked.



class LockController < ApplicationController


  1. gnerate the starting user lists, and hand off to the default list view

  def list
    RAILS_DEFAULT_LOGGER.warn(“[LIST] Parameters: ” + @params.inspect)
    @locked_users = SapUser.find_locked()
    @unlocked_users = SapUser.find_unlocked()
    RAILS_DEFAULT_LOGGER.warn(“[LIST] of Locked: ” + @locked_users.inspect)
    RAILS_DEFAULT_LOGGER.warn(“[LIST] of UNLocked: ” + @unlocked_users.inspect)
  end


  1. check through the list of locked users in the locked sortable_element box
  2. and set their locked state in necessary
  3. on completion – render the partial locked_users

  def set_locked
    RAILS_DEFAULT_LOGGER.warn(“[SET_LOCKED] Parameters: ” + @params.inspect)
    @locked_users = []
    cache = SapUser.find_all()
    if @params[‘locked_box’]
      @params[‘locked_box’].each {|locked|
       next if locked.length == 0
          user = SapUser.find_cache(locked, cache)
       next if user.first.strip.length == 0
       if user && ! user.locked?
           user.locked = !user.locked?
            user.save
          end
          @locked_users.push(user)
      }
    end
    render :partial => ‘locked_users’, :object => locked_users
  end


  1. exact opposite/the same as set_locked

  def set_unlocked
    RAILS_DEFAULT_LOGGER.warn(“[SET_UNLOCKED] Parameters: ” + @params.inspect)
    @unlocked_users = []
    cache = SapUser.find_all()
    if @params[‘unlocked_box’]
      @params[‘unlocked_box’].each {|unlocked|
       next unless unlocked.length > 0
       user = SapUser.find_cache(unlocked, cache)
       next if user.first.strip.length == 0
       if user && user.locked?
         user.locked = !user.locked?
           user.save
         end
       @unlocked_users.push(user)
      }
    end
    render :partial => ‘unlocked_users’, :object => unlocked_users
  end


  1. called by the rendering action of the partial locked_users

  def locked_users
    RAILS_DEFAULT_LOGGER.warn(“[LOCKED_USERS] of Locked: ” + @locked_users.inspect)
    @locked_users
  end


  1. called by the rendering action of the partial unlocked_users

  def unlocked_users
    RAILS_DEFAULT_LOGGER.warn(“[UNLOCKED_USERS] of UNLocked: ” + @unlocked_users.inspect)
    @unlocked_users
  end


end


Views


The the overall page template (layout) defines the shape of the page, and what
JavaScript libraries are pulled in for the effects (AJAX).  All pages inherit from this.
</p>
<p>
<b>layout/application.rhtml</b>
</p>
<pre>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.0 Transitional//EN”
  “http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml”>
  <head>
    <meta http-equiv=”Content-type” content=”text/html; charset=utf-8″/>
    <title>User Administration:  <%= controller.controller_name %></title>
    <meta http-equiv=”imagetoolbar” content=”no”/>
    <%= stylesheet_link_tag “administration.css” %>
    <%= javascript_include_tag “prototype”, “effects”, “dragdrop”,
                       “controls” %>
  </head>

  <body>
  <div id=”container”>
    <div id=”header”>
    <div id=”info”>
      <%= link_to “home”, :controller=> “/lock”, :action => ‘list’ %>
    </div>
     

<%= link_to “UserAdmin – #{controller.controller_name}”, :controller => “/lock” %>


       


   


     

<%= @page_heading %>


      <%= @content_for_layout %>
    </div>
  </div>
  </body>
</html>

</pre>

<p>
<b> lock/list.rhtml</b>
</p>
<p>
The two most important things in list are the two container div tags –
unlocked_box and locked_box.  These in turn, have a corresponding partial
(unlocked_users, and locked_users), that are responsible for generating the
dragable user items.
</p>
<pre>
<% @heading = “User Admin – Lock/UnLock” %>

  <div id=”user-admin”>
    <div id=”unlocked” class=”dropbox”>
     

Unlocked Users


      <div id=”unlocked_box”>
        <%= render :partial => ‘unlocked_users’, :object => @unlocked_users %>
      </div>
    </div>

    <div id=”cnt-locked” class=”dropbox”>
     

Locked Users


      <div id=”locked_box”>
        <%= render :partial => ‘locked_users’, :object => @locked_users %>
      </div>
    </div>
    <br clear=”all”/>

  </div>

</pre>

<p><b> lock/_unlocked_users.rhtml</b></p>
<p>the partial unlocked_users either displays a place holder element if there
are no users, or calls the render of unlocked_user for each user.
It also uses the AJAX function sortable_element which dictates what div
container holds the sortable drag and drop elements, and what actions to take
when an event is fired with them.  This is how we trigger the call to the
set_unlocked or set_locked action of the list  controller for updating the
individual “boxes” of users.
</p>
<pre>

<% if unlocked_users.empty? %>
  <div class=”target”>  You have no Unlocked SAP Users…. </div>
<% else %>
  <%= render :partial => ‘unlocked_user’, :collection => unlocked_users %>
<% end %>

<%= sortable_element “unlocked_box”,
  :update => “unlocked_box”,
  :url => {:action=>’set_unlocked’},
  :tag => ‘div’, :handle => ‘handle’, :containment => %>


<%= sortable_element “locked_box”,
  :update => “locked_box”,
  :url=> {:action=>’set_locked’},
  :tag => ‘div’, :handle => ‘handle’, :containment => %>


</pre>

<p><b>lock/_unlocked_user.rhtml</b></p>
<p>
the partial unlocked_user renders a dragble_element for each SapUser.
</p>
<pre>

<div id=”unlockeduser_<%= unlocked_user.userid %>” class=”dragitem”>
  <h4 class=”handle”><%= unlocked_user.userid %></h4>
  <p><%= unlocked_user.last + “, ” + unlocked_user.first %></p>
</div>
<%= draggable_element “unlockeduser_#{unlocked_user.userid}” %>

</pre>

<p><b>lock/_locked_users.rhtml</b></p>
<p> the same as for the partial unlocked_users</p>
<pre>
<% if locked_users.empty? %>
  <div class=”target”>  You have no Locked Users …  </div>
<% else %>
<%= render :partial => ‘locked_user’, :collection => locked_users %>
<% end %>

<%= sortable_element “unlocked_box”,
  :update => “unlocked_box”,
  :url => {:action=>’set_unlocked’},
  :tag => ‘div’, :handle => ‘handle’, :containment => %>


<%= sortable_element “locked_box”,
  :update => “locked_box”,
  :url=> {:action=>’set_locked’},
  :tag => ‘div’, :handle => ‘handle’, :containment => %>


</pre>

<p><b>lock/_locked_user.rhtml</b></p>
<p> the same as for the partial unlocked_user</p>
<pre>
<div id=”lockeduser_<%= locked_user.userid %>” class=”dragitem”>
  <h4 class=”handle”><%= locked_user.userid %></h4>
  <p><%= locked_user.last + “, ” + locked_user.first %></p>
</div>
<%= draggable_element “lockeduser_#{locked_user.userid}” %>

</pre>

<p>
In config/routes.rb
add:
<pre>
map.connect ”, :controller => “lock”, :action => ‘list’
</pre>

and make sure that you delete public/index.html
</P>
<p>
This makes sure that any requests to the root of the server eg.
http://localhost:3000, are forwarded onto the list action of the lock
controller.
</p>


firing Up


Start the Rails WEBrick server by running the script:

ruby scripts/server

Open your broswer and point to http://localhost:3000.

When you connect to http://localhost:3000 (there will be an inital delay as
sap4rails caches the RFC calls), you should get a screen that looks like this

!http://www.piersharding.com/download/ruby/rails/useradmin.thumb.png!


A Flash movie of this in action can be seen here .
</p>


Assigned Tags

      13 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Gregor Wolf
      Gregor Wolf
      Hi Piers,

      could you provide the source of function module Z_SUSR_USER_LOCKSTATE_GET which is used in Z_BAPI_USER_GETLIST.

      Regards
      Gregor

      Author's profile photo Gregor Wolf
      Gregor Wolf
      Hi Piers,

      found a solution on my own. On my IDES ERP 2004 I've found the standard function module SUSR_USER_LOCKSTATE_GET.

      Regards
      Gregor

      Author's profile photo Former Member
      Former Member
      Blog Post Author
      Hi Gregor - good to hear from you.  I forgot about that as the Z version was in relation to some early work I was doing.  I've changed the example, and the source code file to revert to SUSR_USER_LOCKSTATE_GET.  The Z version was only so I could make the SAP standard RFC enabled - there is  no functionality difference.
      Cheers,
      Piers Harding.
      Author's profile photo Gregor Wolf
      Gregor Wolf

      Hi Piers,<br/><br/>I try to run your example aginst our Web AS 6.20 Unicode system. Here I have a problem with german umlauts öäü and ÖÄÜ. In the view you've set the encoding to utf-8 but the codepage for the RFC is 1100 which is ISO-8859-1 Western Europe. I tried to switch Rails to Unicode by adding:<br/><br/>$KCODE = 'u'<br/>require 'jcode'<br/><br/>to config/environment.rb as suggested in HowToUseUnicodeStrings . But this results in this error:<br/><br/>Showing app/views/lock/_unlocked_user.rhtml where line #5 raised:<br/>malformed UTF-8 character<br/>Extracted source (around line #5):<br/>5: <%= draggable_element "unlockeduser_#{unlocked_user.userid}" %><br/><br/>I've tried to add:<br/><br/>  codepage: "4103"<br/>  unicode:  "1"<br/><br/>to config/sap.yml but the error was the same. As I see from the trace the codepage is still 1100. I'm using saprfc-0.19 on a Debian Sarge system.<br/><br/>Hope you can help me out.<br/><br/>Regards<br/>Gregor

      Author's profile photo Former Member
      Former Member
      Blog Post Author
      Hi Gregor,

      I think I've fixed this.  Not all the possible RFC connection options were being passed through.  Now you can pass the UNICODE related options (and everything else).  Please try sap4rails 0.02, and let me know how you get on.
      (http://www.piersharding.com/download/ruby/rails/)
      Cheers,
      Piers Harding.

      Author's profile photo Gregor Wolf
      Gregor Wolf
      Hi Piers,

      wow that was rearly quick. Unfortenately I wasn't so quick to test because of the weekend and holliday in germany. So I've just tested and had a bad result. When I set:

        codepage: "4103"
        unicode:  "1"

      in config/sap.yml and access the application website after starting script/server this error message is returned:

      RFC Call/Exception: Connection Failed   Error group: 104        Key: C

      in the trace file I see:

      ...........C.A.L.L._.F.U.N.C.T.I.O.N._.N.O.T._.F.O.U.N.D......@
      F.u.n.c.t.i.o.n..m.o.d.u.l.e..".RFCPIN"..n.o.t.f.o.u.n.d....

      I can send you the logs if you need further information.

      Regards
      Gregor

      Author's profile photo Former Member
      Former Member
      Blog Post Author
      Hi Gregor -
      is it possible for you to separate out a test case to run outside of Rails - ie. take the code out and run it in a separate Ruby script so that we can remove Rails from the equassion - I would test this myself but I do not have access to a UNICODE system to do it.  Also - it would probably be a good idea to move this to a Forum discussion under Scripting Languages - I watch all the threads, so I'll know when you post. Please add in the traces too.
      Thanks,
      Piers Harding.
      Author's profile photo Gregor Wolf
      Gregor Wolf
      Hi Piers,

      I've now posted a Topic:
      Ruby and Unicode

      Regards
      Gregor

      Author's profile photo Former Member
      Former Member
      Forgive my lack of ABAP function modules skills but when I try to activate the function module I get the error.
      ----
      Function module Z_BAPI_USER_GETLIST
      The field "USERLISTLOCK" is unknown, but there is a field with the
      similar name "USERLIST".
      ----

      What do I need to do to activate this module?

      Author's profile photo Former Member
      Former Member
      Blog Post Author
      Hi Glenn,
      Have you correctly setup the table parameter for the RFC for USERLISTLOCK? =>
      USERLISTLOCK STRUCTURE  ZBAPIUSNAME OPTIONAL
      Cheers.
      Author's profile photo Former Member
      Former Member
      Dear Gleen,

      Hope you must be doing fine, as this is my first interaction with you through mail I wouldn’t take much of your time.

      I am working as a SAP BASIS SECURITY Consultant and involved in implementing ECC6.0 on AIX 5.3 with Oracle 10g.

      Our concern is to install Two Oracle databases on one AIX Box and for the same I was going through one of the notes (Note: 153835) on service.sap.com in which you have suggested that its possible to install two different tnslsnr process for two databases.

      I would like to ask you couple of things here as its keeping me from going ahead.

      1. Is that note applicable for Oracle 10g or earlier versions. (as both the databases which I am going to install would have same versions of 10g)

      2. Apart from this i have a minor doubt here -> when i would be installing ECC6.0(DEV) during installation steps it shows the listner screen where i would choose the default name LISTENER for my first listner

      now when i would do ECC6.0(QAC) installation, during the installation screen where i would be asked to name listner name, there i have to name this as LISTQAC (here i would like to ask you am i right. ) or do i have to choose the default name LISTENER as given for first listner process...

      3. By The above mentioned 2nd point I mean that I should keep the same name “LISTENER” for listner process for both the oracle installations, but the port numbers should be different.

          For First Oracle installation it would be 1527 &

          For Second Oracle installation it would be 1528

      Kindly correct if I am wrong anywhere,

      Your suggestions would be highly appreciated.


      Regards

      Ayush Johri

      Senior SAP Basis Security Consultant

      Tata Consultancy Services.

      Author's profile photo Former Member
      Former Member
      Hi Piers, your example work well, but when i try to refer function modules with a slash (/) (for example: /EO1/BC00_0011), i get a problem when i call the function. Can you give me an answer for how can i solve this problem? I think the solution depends on the notation in ruby?
      th@nx
      Author's profile photo Former Member
      Former Member
      Blog Post Author
      Hi Jens -

      The short answer is that you need to create your own function module wrapper that has no slashes in the name - the long answer is something that I will look into for later releases.

      Cheers,
      Piers Harding.