Additional Blogs by Members
cancel
Showing results for 
Search instead for 
Did you mean: 
Former Member
0 Kudos
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>


13 Comments