Skip to Content
Technical Articles
Author's profile photo Carson Wong

Record SF LMS External Learning Events via BTP/CPI using OData API

This post is to demonstrate how to record external learning event using BTP/CPI (sorry I am confused with the exact wordings like BTP/CPI/Integration Suite/Cloud Integration/Integration Flow/…) as they keep changing from time to time.

 

Why this blog is out:

Since the LMS import data tool as well as Learning History connector can only record internal learning event, so API development is the only for the external learning event import, else you have to make in LMS frontend….nightmare.

 

Notes before deep-diving:

In the BTP/CPI, the message body should be in XML format instead of JSON format

  • At the beginning I refer KBA#2421887 – LMS – Admin/supervisor/user records external event through OData API (Product Enhancement b1702 – LRN-15619) and make a POC via Postman which the message body is in JSON format
  • However, when I put the same to BTP/CPI, it doesn’t work with error message below
  • Later on I change it in XML format, then it results another error message (but more meaningful), which means I am finally on the right track

 

Limitation on this API (ie. Record Learning Event to Learning History Web Service)

  • Call limiting: 100 calls per min
  • Array limiting: 10 records per each call

Details please find: https://help.sap.com/viewer/5aab9bef78fc4c4fa199c1f7aa142720/2111/en-US/e568f59a0fa144f2b7599970a2a86bcd.html

 

How to enable the LMS Webservices and obtain OAuth Token

  • KBA#2279128 – Enable API Webservices LMS data in LMS instance
  • KBA#2318897 – LMS ODATA Webservices Knowledge Support and Tips

 

How to connect LMS with BTP/CPI

 

Which service should be used for ‘External’ Learning Event recording?

https://help.sap.com/viewer/5aab9bef78fc4c4fa199c1f7aa142720/2111/en-US/90505b0d331049e5afff352911ea64c3.html

You can find there are 2 micro-services for admin/user:

  • learningevent-service/v1
  • learningEvent/v1

From the Learning OData API Reference guide (see above) as well as KBA#2421887 – LMS – Admin/supervisor/user records external event through OData API (Product Enhancement b1702 – LRN-15619), only micro-service learningevent-service/v1 supports external learning event recording.

 

iFlow Design

The original design is to extract learning records from legacy learning system and post to LMS as external learning events (It is simpler version as there is not much resources to have a completed end-to-end flow from sync-ing items, learning assignments, and learning events at this moment)

So the design in my demo iflow:

  1. Triggered by external API call
  2. Query (GET) student/user records from LMS in order to mimic the record extraction from legacy and transform it to corresponding external learning event format (in XML) referring the metadata
  3. Splitting the message so that only 10 records will be processed for each call (ie. LMS API limitation)
  4. Record (POST) external learning event records

 

*Only focus on the yellow-arrow flow, while others are built for testing during development which can be ignored.

 

Step 1: Triggered by external API call

It is pretty simple by creating a HTTPS Sender Adapter with defining the address so that the iflow can be called (eg. by Postman).

In this example I have defined the address /LMSpartlms0035/postExtLearningEvent, and I will use Postman to trigger the iflow via the URL:

https://xxxxxxxxtrial.it-cpitrial03-rt.cfapps.ap21.hana.ondemand.com/http/LMSpartlms0035/postExtLearningEvent

(you are free to use other triggering approaches, like using Timer event to schedule run)

 

Step 2: Query (GET) student/user records from LMS in order to mimic the record extraction from legacy and transform it to corresponding external learning event format (in XML) referring the metadata

For this step, you are recommended to refer the scenario 1 in https://blogs.sap.com/2020/09/17/successfactors-integrations-beginners-guide-exploring-learning-management-system-continued/ on how to:

  1. Create OAuth2 credential in CPI
  2. Query data from LMS API (in the above blog it queries scheduledoffering-service while I query searchStudent instead, and only 10 student records will be returned)

 

*My demo will not really use the data resulted from the searchStudent API but only using the returned record count to mimic message splitting requirement.

 

import com.sap.gateway.ip.core.customdev.util.Message;
import java.util.HashMap;
import groovy.xml.*;
import groovy.util.*;

def Message processData(Message message)
{   
    def currentDT = new Date();
    
    def body = message.getBody(java.lang.String) as String;
    def Students = new XmlSlurper().parseText(body);
    
    int i = 0;
    
    def writer = new StringWriter();
    def OutputBody = new MarkupBuilder(writer);
    OutputBody.mkp.xmlDeclaration(version: "1.0", encoding: "utf-8");
    OutputBody.ExternalLearningEvents()
    {
        ExternalLearningEvent{
            //Loop for each Student Element
            for(def Row: Students.Student)
            {
                i = i + 1;
                //Mapping
                externalLearningEvents{
                    element{
                    description(Row.studentID.text())
                    studentID('adminCW')
                    completionDate(currentDT.getTime())
                    completionTimeZoneID('GMT')
                    grade('A')
                    creditHours('0.5')
                    cpeHours('0.5')
                    contactHours('0.5')
                    totalHours('0.5')
                    instructorName('BTP')
                    comments(i.toString())
                    }
                }
            }
        }
    }
    
    message.setBody(writer.toString());
    
    return message;
}


//Sample XML
//<?xml version="1.0" encoding="UTF-8"?>
//<ExternalLearningEvents>
//  <ExternalLearningEvent>
//    <externalLearningEvents>
//      <element>
//        <description>External Learning Event 1</description>
//        <studentID>adminCW</studentID>
//        <completionDate number="true">1641357063000</completionDate>
//        <completionTimeZoneID>US/Eastern</completionTimeZoneID>
//        <grade>Test Grade</grade>
//        <creditHours number="true">7</creditHours>
//        <cpeHours number="true">0.5</cpeHours>
//        <contactHours number="true">0.5</contactHours>
//        <totalHours number="true">0.5</totalHours>
//        <instructorName>Test Instructor</instructorName>
//        <comments>Test Comment From Admin</comments>
//      </element>
//    </externalLearningEvents>
//    <externalLearningEvents>
//      <element>
//        <description>External Learning Event 2</description>
//        <studentID>adminCW</studentID>
//        <completionDate number="true">1641357063000</completionDate>
//        <completionTimeZoneID>US/Eastern</completionTimeZoneID>
//        <grade>Test Grade</grade>
//        <creditHours number="true">7</creditHours>
//        <cpeHours number="true">0.5</cpeHours>
//        <contactHours number="true">0.5</contactHours>
//        <totalHours number="true">0.5</totalHours>
//        <instructorName>Test Instructor</instructorName>
//        <comments>Test Comment From Admin</comments>
//      </element>
//    </externalLearningEvents>
//  </ExternalLearningEvent>
//</ExternalLearningEvents>

Then I use a groovy script to:

  1. Loop the student records, which is in XML format, from previous searchStudent API call
  2. Transform into external learning event record for each student record in XML format (the required format can be checked by the metadata https://xxxxxxxx.scdemo.successfactors.eu/learning/odatav4/public/admin/learningevent-service/v1/$metadata

*Again, you can refer scenario 2 from https://blogs.sap.com/2020/09/17/successfactors-integrations-beginners-guide-exploring-learning-management-system-continued/ on how to build the correct format by referring the metadata file

 

Step 3: Splitting the message so that only 10 records will be processed for each call (ie. LMS API limitation)

In this step, a general splitter is used to split the message for every 10 externalLearningEvents records are reached.

*You can refer https://blogs.sap.com/2020/09/21/sap-cloud-platform-integration-general-splitter/ on the general splitter usage

 

Step 4: Record (POST) external learning event records

In this step, create a SuccessFactors OData v4 adapter to call the learningevent-service API.

 

Testing

Postman is used to call the iflow URL (1), the request message body (2) is ignored in my demo. And the response body (3) is exactly the response body from LMS API.

From the response body (3), you can see external learning events are created with completion date 1641380378089 (which is unix epoch timestamp) in GMT timezone. It is converted into:

GMT: Wednesday, January 5, 2022 10:59:38.089 AM
Your time zoneWednesday, January 5, 2022 6:59:38.089 PM GMT+08:00

(I use https://www.epochconverter.com for the conversion)

 

In SuccessFactors LMS, external learning events completed on 5 Jan 2022 06:59PM Asia/Hong Kong (aka GMT+8) are found.

 

Challenges to Me

  • I am new to BTP/CPI iflow development, it takes time for me to figure out the iflow message concept (eg. message header/body/exchange properties)
  • I am new to Web development, it takes time for me to get familiar with XML/JSON usage, groovy script

More to Go

  • Exception handling (in case the POST call is failed, instead of acknowledging them in BTP/CPI monitor, more human-readable way should be built for end-user)
  • Message mapping (in my demo, a groovy script is used to build POST call message body. But for a real integration scenario, message mapping between the learning records from legacy and learning records in LMS should be made. Learnt there are some many message mapping approaches like WSDL, XSLT, scripting. But which one is better to use? any limitation?)

Last, thanks again.. (and what I have referred during the POC)

 

Last….I need your input

I am new to BTP/CPI iflow development as well as web development. I do believe there should be a more professional/friendly/easy way for the same purpose. Please kindly comment and share your experience.

Assigned Tags

      2 Comments
      You must be Logged on to comment or reply to a post.
      Author's profile photo Juli Strattman
      Juli Strattman

      I am interesting in following this.  The biggest obstacle is not having a course number generate and a course title as otherwise this is not really reportable? Would this still be considered blob data?  Most of my customers do not want non-reportable data in their LMS so they have a manual process to record external learning so that they can report against the course.  I can see if the company does not need to necessarily report on external learning why this would be most helpful.  Thanks for sharing

      Author's profile photo Carson Wong
      Carson Wong
      Blog Post Author

      Somehow customers would like to have a single source of truth for learner or admin to extract the learning history records.

      Recording external learning event into LMS lets learner or admin extract consolidated records via reports or frontend.

      Just recording external learning event is a minimum requirement while the next level will be administering the whole learning process (from learning assignment, approval, start learning, learning history, etc) for learning content from 3rd learning platform.

      LMS does have integration with some well-known global OCN providers (in some countries) while additional integration work is required for local OCN providers and/or platforms.