Skip navigation
All Places > Blackboard Developer Community > Blackboard Learn Developers > REST > Blog

REST

6 posts

One of the most difficult aspects to using the REST API is mapping the entitlements in the swagger documentation at developer.blackboard.com to the permissions available in the UI for creating a system role. Using this custom javascript bookmarklet makes this a lot easier. Here is the code:

 

javascript:(function()%7Bif('jQuery'%20in%20window)%7Blet%20showUids%3D(jq)%3D%3E%7Bjq('tbody%23listContainer_databody%20%3E%20tr').each((i%2Ctr)%3D%3E%7B%20var%20val%20%3D%20jq('input%5Btype%3Dcheckbox%5D'%2C%20tr).prop('value')%3B%20jq('th'%2C%20tr).append('%3Cdiv%3E%3Ci%3E'%2Bval%2B'%3C%2Fi%3E%3C%2Fdiv%3E')%3B%20%7D)%7D%3Blet%20ws%3D%5B%5D%3Bws.push(window)%3Blet%20ifr%3DjQuery('iframe').prop('contentWindow')%3Bif(ifr)%7Bws.push(ifr)%3B%7Dws.forEach((w)%3D%3E%7BshowUids(w.jQuery)%3B%7D)%3B%7D%7D)()

 

Simply create a new bookmark in your browser, paste this code in, and save it.

 

So what will this do for you? Essentially, this code searches the current webpage looking for checkbox input tags. It finds the value of that checkbox and displays this on the screen. In the context of the privileges page that you use to set permissions for a user role, this value is equal to the entitlement.

 

Here is a screen shot of that page before running this script:

 

 

And here is a screen shot after running this script:

 

 

As you can see the entitlement is now listed under the role. So now you can simply find the entitlement you are looking for in swagger, and ctrl-f/cmd-f to search for it here. Much easier than before.

 

As a side effect, you can use this on any page in Blackboard Learn and get useful information. As an example, search for courses or user, and now you can use this script to find out the objects pk1 value.

 

Enjoy! I hope you find this as useful as I do!

 

Happy developing!!

Hi Everyone!

 

I am excited to share that we have released our first Open Innovation Initiative focused Developer Virtual Blackboard Application (DVBA) via the Amazon Web Services Marketplace. More on that in a moment – first for folks who missed the news at BbWorld 2017, here’s a refresh:

 

GettyImages-489725378_super.jpgAt BbWorld 2017, we announced the Open Innovation Initiative. This program provides a way for LTI and REST developers to create integrations and extensions for our products before entering into a formal partnership with Blackboard. Developers participating in the Open Innovation Initiative receive the following:

  • Access to documentation and development tools
  • Open access to REST API documentation in the Blackboard Development Portal
  • Support via our Blackboard Community site
  • Support for a dedicated development, testing and demo environment

 

If opening our development platform like never before isn’t enough, how about that last bullet point? “Support for a dedicated development, testing and demo environment” – this is the DVBA noted in the Blackboard REST and LTI Developers Agreement you accept when creating an account on our Developer Portal at https://developer.blackboard.com.

 

We want to make it as easy as possible for you to take advantage of our open platform and we are pleased (well, I am thrilled) to announce that we are now providing access to a developer’s version of Blackboard Learn SaaS via an Amazon Machine Image (AMI) through Amazon’s AWS Marketplace. All you need is an AWS account and you can subscribe to available Blackboard Learn AMIs for development, testing and demoing of your REST and LTI integrations with either Original or Ultra experiences of Learn. By leveraging Amazon services, we can provide you with access to the latest Blackboard Learn SaaS production releases. AMIs will be provided in cadence with our SaaS releases and be available so long as those releases are supported, after which the license will expire. We also will be providing a Learn Original AMI that will be released in cadence with that platform’s release schedule and will be available so long as the release is officially supported.

 

This is a great opportunity for developers interested in building REST or LTI integrations that run on Blackboard Learn – from software vendors, to clients, to faculty and students interested in creating solutions for Blackboard Learn. Per this last item see Scott Hurrey’s post on his recent Hackathon at PSU!

 

Although under the Open Innovation Initiative production-level rates and the number of production deployments is limited, working with the Blackboard Learn AMI will allow you to test the Blackboard Learn SaaS waters. Once you make your integration or extension available, we will offer the opportunity to partner with us in a more strategic and robust way via our Blackboard Member Partnership program.

 

To learn more about the Blackboard Learn for REST and LTI Developers AMI, visit our Developer Community site. To learn more about how you can extend and enhance the learning experience with Blackboard, visit our curated Extensions Catalog today.

 

I hope you enjoy using our AMIs. Please drop me a line or add a comment below, I look forward to hearing from you!

 

Cheers and Happy Coding!

-m

 

Mark O'Neil

Senior Product Manager, Developer Platform and REST APIs

Product Management

Blackboard Inc.

 

Quick Links:

Developer AMI

Developer Platform
Developer Community

Recently several Blackboard folks participated in an event called HackPSU. HackPSU is an annual hackathon that takes place at Penn State University. During this awesome event, we discovered that some of the students were trying to use JQuery to make AJAX calls to the REST APIs, and they were failing. Upon further troubleshooting, we realized that the browser was rejecting the calls because of CORS. If you aren't familiar with CORS, there is a great write up on it by the Mozilla Developers Network.

 

Web-API-CORS.pngIn the heat of battle, we discovered the easiest way around it for development purposes is to simply disable web security in the Chrome browser. Sure, its easy, but even typing that makes me cringe. There are really only two options:

  • The REST API must support it; or
  • The client-side javascript must go through a proxy

 

Blackboard is looking at the best way to handle this on the API side, but at least in the short term, the only way to get around this is via a Proxy. A proxy, quite simply, accepts your applications API requests and forwards them to the appropriate endpoint. By doing so, the call doesn't come directly from the browser, and therefore does not require the OPTIONS request to be handled on the API side and therefore bypasses CORS altogether.

 

To illustrate this, I wrote a simple user account sign-up application in Angular 2, and used angular's built in proxy functionality to bypass CORS protection. Basically, the important bits all take place in three files.

  1. proxy.config.json
  2. package.json
  3. src/app/new-user-form/new-user-form.component.ts

 

proxy.config.json

 

In this file, we are, as you might imagine, configuring the proxy with JSON. There are tons of things you can do with this, but in our application, we are only implementing three. Here is the JSON file contents:

 

1{
2  "/learn/api/public/*": {
3   "target": "<Your Learn Domain>",
4   "secure": false,
5   "logLevel": "debug"
6  }
7}

 

In line 2, we are defining our search pattern. This tells Angular that this configuration should be applied any time an HTTP call is made to an endpoint that starts with /learn/api/public. Line 3 defines the domain to where the endpoint should be forwarded, so https://your.blackboard.com. Line 4 determines whether the endpoint should be secure. It is set to false in order to allow for testing against a development instance with a self-signed certificate. If you are moving your application to a production instance, this should be set to true. Lastly, on line 5, we are setting the logLevel for the proxy, to determine how much information is written to the console. Again, debug is great for development, but probably not so much for a production environment.

 

package.json

 

So now that we have configured the proxy, we have to tell angular that we want to implement it. This is done in the package.json file with a quick addition to the start script. To locate this script, open your package.json. You will see a "scripts" : { } tag and inside, you should see "start" : "ng serve". After ng serve and inside the double quotes, simply add a space and then --proxy-config proxy.config.json. That's all there is to it!

 

src/app/new-user-form/new-user-form.component.ts

 

So know you just have to call your endpoint. Contrary to many other examples, you don't call the endpoint directly. You call the endpoint in your proxy. So rather than calling GET https://your.blackboard.com/learn/api/public/v1/users, you simply call GET /learn/api/public/v1/users and the proxy takes care of the rest. Not only does this bypass CORS, but it makes it super simple to change the server you are calling without having to change each individual endpoint in your code.

 

So i hope this helps. As always, if you have any questions, feel free to ask here or email us at developers@blackboard.com.

 

Happy Coding!!

Hello, I would like to share with you some of my thoughts on creating CLIs that work with Blackboard Learn’s REST APIs. In this post, I will be covering: My initial thoughts on BbRest API’s, Why I decided to get started on working with them for our institution, What I have ‘Learned’, Ports, and the future of these CLIs along side with Blackboard’s road map.

 

My initial thoughts.

When I first saw Mark O'Neil and Scott Hurrey's presentation at DevCon16 in Las Vegas on creating simple CLI’s for Blackboard Learn’s REST APIs, I was hooked. That fact the Bb was actually listening to the needs their clients and the problems they are facing had installed great promise. When they announced that they were implementing REST and that you could start testing/using them right away was a great boost in my current thoughts of Learn at the time. I admit, as an LMS administrator at the institution I work for, I wasn’t impressed of the current work flow in which my colleagues and I have to endure….. DevCon16 was also my first year in presenting as well: I presented on creating a B2 for my institution on creating a way for students to submit and application for off campus testing. After completing that presentation and project, I decided that B2s are not the best solution for forward thinking ideas and needs of today’s end users. Personal opinion: I believe that B2s limit the overall flexibility of Learn. (B2: Building Block).

 

When sitting through the presentation, I realized that Blackboard is changing and that Learn will be changing as well. The overall product design was (is) moving forward and that if we do not start planning along side this road map, we are going to have a hard time catching up. So when @Mark O’Neil walked up and started presenting his Python Demo, I was impressed in what he had done; with only just learning Python and creating a CLI in approximately eight hours.

 

When Mark was finished, Scott walked up...Boom, floored again as he did his presentation. Although the demo was slightly shot due to internet issues (live demos ftw!), the work he did was amazing, and almost made me want to jump back in to JAVA...(sorry just kidding; No JAVA for me ;p ). Now there is nothing wrong with JAVA if you love it, but as for me it’s not my cup of tea (facepalm for the pun, sorry). The overall idea came in when they both said that we could take these examples and make them our own, run with them, you can do it! I was highly impressed as most companies are trying to get you to pay for consulting. The notion of being able to get free code created by the developers themselves was very forwarded thinking and that’s where I said sign me up!

 

Deciding to redo the original concept.

I followed the GitHib repo for Mark’s CLI and wanted to test it, so naturally I cloned it down and started using it. I noticed that there was some issues and made some tickets. I ended up talking to Mark later and we ended up showing some ideas we both were working on and I asked if it would be ok if I took on the project and run with it. Mark was ok with it and said go forth and code!

 

Now being a somewhat experienced Python programmer (at least I like to think I am ), I jumped on it as fast as I could. No, really, I was leaving the next day and started working on it at the airport. I had a four hour flight back home and by the time I arrived I had a new base model.

 

I decided to use Docopt, which is a python module created by Vladimir Keleshev, for creating the help screen of the CLI in which in turn, turns into the CLI. It is a beautiful package and the beauty of it is that it has ports over to other languages. More on that later. After a year on working with my CLI and learning the ins and outs of Learn’s Rest API’s, I have found that there was indeed a need for this sort of thing.

 

How we use this at our institution.

It first started with just doing tests and after a few successful use case tests, I decided to share this CLI with a colleague. We decided to test this further as we were at the time just in the process of changing our SIS/ERP system. We found out that it will have the capability to do integrations and make web calls. With our current legacy system this is just not possible without major re-programming and serious debugging. So we started running through some of the scenarios as if it couldn’t actually make a web call and it was just dumping data files to a directory on a system. I looked over the Bb REST APIs and started gathered some examples of the json models and we used it to mock up some test data. After a few tests we knew that this was a very good go to in the event that we wouldn’t be able to get the web services running on the new SIS/ERP system right away.

 

We also found out, that we do a lot of manual data manipulation that our current system cannot process due to business rules and system limitations. We have found that using this CLI made our current daily tasks easier as we did not have to try to remember the ins and outs of integration files and send them up to Bb. Knowing that we have to make sure to use the right integration as that feature in Bb has its own limitations based flaws. If you don’t know the main issue: Bb SIS Integrations own the record in which creates a learn object (User, Course, Enrollment, etc). This means that you cannot modify records that are owned by one integration from another. Now, in a perfect world this is ok, but this isn’t a perfect world and we can’t always use the same integration with other processes; for the institution I work for anyway. This is because we do not like to skew the data coming from one system that belongs to another. Doing this has helped us tremendously in making sure we keep data integrity intact.

 

Using the REST API method we have found that a lot of the data that we need to change isn’t system dependent: Names, Title Changes, Roles for non SIS records; even some SIS records. Now, we have found that somethings we cannot change still with this method: Some data is still protected via the Integration. So until we have successfully moved over to our new SIS/ERP System, there is still a need for the older SIS integration methods. But it’s a start!

What I ‘Learned’ from doing this.

This particular project has given me great insight on programming methodologies, why programming in one particular language is limits your overall developer growth, and a few issues with Bb’s REST. Yep, like all great products, there will be bugs.

 

One of the main issues I noticed is that there is no result population. Meaning that when you make a call to the membership routes, only the internal/external ids are sent back; no friendly readable structure. You will have to make up your own work flow in delivering the readable end result. There are some issues with certain http methods on certain routes: Course Membership (at the time of this article a patch is/was being implemented). You cannot change a users course role; even if it was created via rest. You can only do limited searching upon one major criteria group: /courses?dataSourceId=<id> vs. /courses?dataSourceId=<id>&termId=<id>. This results in a 400 status error and states you cannot load data based on multiple search terms. This in my opinion is a serious limitation, but I am sure that there is some very good and reasonable reasons on this particular issue. There are some inconsistencies in the HTTP Methods: Bb stated that they would favor POST for create and PATCH for updating. All well and good but please be aware of these routes:

 

PUT /learn/api/public/v1/courses/{courseId}/contents/{contentId}/groups/{groupId}

PUT /learn/api/public/v1/courses/{courseId}/users/{userId}

PUT /learn/api/public/v1/courses/{courseId}/groups/{groupId}/users/{userId}

 

These routes break the work flow and when creating a CLI to auto-detect the correct HTTP method to use. Personal thought: PUT should be for creating, POST should be for updating partial information and PATCH should be used to update a record due to a previous error in the information (or error preventing); kind of like a reset method. Not trying to raise and old debate, but to me, that makes more sense.

 

These are just a few things I have noticed that caused some major rethinking on my end when creating a CLI for Bb. There are many other small issues but they are manageable and/or have been corrected (or being looked into) since the first initial release of the APIs.

 

Deciding to port the CLI over to PHP.

I just recently did a presentation at DevCon17 on two projects that I am working on for my institution: Course Role Management Web Application and my Python CLI. During my visit, I had the pleasure of meeting some fellow developers and during our talks it came to me about looking at this with a different lens. I was able to sit down with Scott Hurrey and Mark O'Neil and had a discussion about Blackboard's just announced Open Source Initiative. I started bringing the thought of an SDK to the table. Of course, this was something they have been working on for quite some time. I expressed my opinions on the matter and after one thing after another, I thought I should put this to a concept.

 

I tasked myself on porting my Python CLI over to PHP and still keeping the same usage. This is where docopt comes into play. Docopt has been ported over to several other languages and the creator has supplied a spec in which to write a port of the package. I currently at this time have ported over about 90% of this CLI to PHP while still using the same usage from the Python version; with only working on it a few hours a night spread over a week. This brings me back to SDK idea. I expressed my interest in Bb’s open source solution and that there needs to be an official spec in writing Bb applications from a outside source. This overall idea isn’t new, but new to this particular world of educational products. If we (The Bb Community) can come up with a official spec to create applications that can be used in language agnostic manner, we can offer a beautiful solution for those will come behind us.

 

The idea of an official spec would include but not limited to: Overall protocol flow, Naming Conventions, Data Conventions, Expectancy, Security, Delivery, etc, etc. Having this higher abstracted overview on how to develop a package for the end users choice is a huge help in future productivity and the community. Imagine having an idea and then to be able to quickly try out and based off of programming language/package that you, the developer, are comfortable using. To be able to do it quickly without all the hassle that others have had the pleasure of finding out. Wouldn’t that be great?

 

In doing this port, I have found that not only that I am able to quickly turn around another CLI but that it uses the same overall flow and spec based off the other CLI. I have also found out by doing this, certain things can be improved upon that can be brought up to the higher abstracted view. For instance, Python and PHP have a very different view on imports and data type constructs. In my PHP code I ended up creating a new instance of my authentication class and then added it into the arguments object that Docopt creates. Why? Well, this allows me to pass into the LearnObject class I have (the magical work horse of the actual CLI) to have an instance of the authentication without having to recreate the connection method to talk to Bb; again not a new ground breaking concept but I think you get the point. In my Python CLI, I just loaded the settings in my main application entry point and then stored the information in an object variable. Python scopes your imports so that if you import another class object after you have set a variable, you have access to it; if you re import it into another module, you have all the current settings without having to set them ;p. Kind of like how ES5 (Javascript) works in the overall scope. PHP bases its imports and scopes like C/C++/Java in where the scope is locked to the class you created and has major implications if those scopes are violated.

 

Plans on other ports?

Do I plan on porting this over to other languages? Yep, Why wouldn’t I? To me, it’s a great learning tool. Now I am not planing on doing all languages as that would be a major commitment and as one person, I cannot commit to that. I do however plan on porting it over to NodeJS. I welcome and challenge the Bb community to join the fun! I also welcome all to challenge this concept in general: Is this going to be beneficial? How will this shape not only our personal development growth but for Bb in terms of community and product?

 

Conclusion. What will come out of these examples?

Well that is up to everyone here! I will continue to support the applications I create and share them as long as there is a need. I hope to see others use these ideas and concepts and make them their own and share back. I hope that I can continue with the great relationships I have formed with some of the Bb team and community and in turn be able to produce something that is only going to help the future of the educational field.

 

 

 

My repos for you to clone, fork, hack, destroy, use:

https://github.com/elmiguel/BBDN-REST-PHP Depends on bbdn-core-php.

https://github.com/elmiguel/bbdn-core-php The PHP port of the Python CLI code. Work in progress.

https://packagist.org/packages/bbdn/core same as the github repo just a package to install via composer

https://github.com/elmiguel/aMEBA A Mongo DB, Express JS, Node JS, Blackboard REST Application.

https://github.com/elmiguel/CM-Role-Management A MEAN Stack Application PoC using Bb REST APIS.

https://github.com/elmiguel/BBDN-REST-DEMO_Python Planning on porting into a pip package eventually.

mb23565

aMEBA Application

Posted by mb23565 May 23, 2017

Hello!

Today I would like to share will you all my take on creating what I would like to call a aMEBA (uh-mee-buh) Application: A Mongo DB, ExpressJS(NodeJS), BBRest API Application.

 

Scientific Definition:

A simple application that is categorized by consuming requests and returning data.

 

So how do we create such an application?

Let me first by stating that although that is focused on creating a simple NodeJS application, the concepts can be applied to any language and framework at the discretion of the developer. Also note ,everything in this article is more of a "getting started" approach to using NodeJS, ExpressJS, MongoDB, and making Bb Rest API calls using the Request Module (a HTTP Client for NodeJS). Ok, now that is out of the way, let's begin. I will be mixing *nix terminal commands to start off the project creation then will switch to and IDE later on. I use Atom, but you can use whichever IDE you prefer.

 

Prerequisites:

 

Optional:

  • Typescript: TypeScript - JavaScript that scales.
    • Super set of JS and has many features that are not in regular JS or ES6
  • Babel: https://babeljs.io/
    • ES6 to JS transpiler that allows you to take advantages in newer JS features that are not easily doable in regular JS.
  • Yarn: Yarn
    • A NPM overlay package manager that keeps the install process quick and efficient as it optimizes your node modules by caching your dependencies.
  • nodemon: nodemon

 

 

Getting Started!

After you have installed NodeJS for your OS, begin to create a project directory for our little aMEBA. For my examples I will be using $HOME/CodeProjects as my project home directory that will hold all my code projects.

 

 

$ mkdir $HOME/CodeProjects/aMEBA-app
$ cd $HOME/CodeProjects/aMEBA-app
$ pwd
[home]/CodeProjects/aMEBA-app
$

 

Let's generate our NPM (or Yarn) init package.json

NPM: For now, just press enter to apply the defaults. We can always edit the package.json file later.

 

$ npm init
This utility will walk you through creating a package.json file.
It only covers the most common items, and tries to guess sensible defaults.


See `npm help json` for definitive documentation on these fields
and exactly what they do.


Use `npm install <pkg> --save` afterwards to install a package and
save it as a dependency in the package.json file.


Press ^C at any time to quit.
name: (aMEBA-app) ameba-app
version: (1.0.0) 
description: 
test command: 
git repository: 
keywords: 
author: 
license: (MIT) 
About to write to /home/elmiguel/CodeProjects/aMEBA-app/package.json:


{
  "name": "ameba-app",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "description": ""
}


Is this ok? (yes) 






 

 

or Yarn alt:

 

$ yarn init
yarn init v0.24.4
question name (aMEBA-app): 
question version (1.0.0): 
question description: 
question entry point (index.js): 
question repository url: 
question author: 
question license (MIT): 
success Saved package.json
Done in 11.56s.

 

 

Let's take a peek at the package.json file (Yarn init version):

 

{
  "name": "aMEBA-app",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT"
}



 

For further detail information about understanding what package.json is and what it can do: https://docs.npmjs.com/files/package.json

 

If you know already, or not, you can add a scripts key to add an object of key:value pairs to execute tasks via the terminal to help you in your application building and development process. Let's create a scripts key now if not already with the following:

 

{
  "name": "aMEBA-app",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "node app.js",
    "start:dev": "nodemon app.js",
    "start:ts-dev": "nodemon --exec ./node_modules/.bin/ts-node -- ./src/app.ts"
  }
}



 

Notice that I have created some custom scripts: start:dev and start:ts-dev, these are here if you have decided to use nodemon (highly recommended!) and also if you are going with the ts-node option. Please note, that if you are going to go with Typescript, then I highly recommend that you place your .ts scripts in a src folder within your projects and setup a tsconfig.json file to compile and output to a distribution folder ("dist" by convention). More information here: tsconfig.json · TypeScript. If you go this route, then make sure you update your scripts to point to the appropriate paths. You can also build your .ts via the npm scripts as such:

 

"scripts": {
  "build:ts": "tsc"
}

 

Then run it as:

 

$ npm run build:ts

 

Naturally you can just do tsc in your terminal but one good thing about placing it into the scripts object is that you can use this to keep your commands cleaner by stacking the npm scripts:

 

{
  "name": "aMEBA-app",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "node app.js",
    "start:dev": "npm run build:ts && nodemon app.js",
    "start:ts-dev": "nodemon --exec ./node_modules/.bin/ts-node -- ./src/app.ts",
    "build:ts": "tsc"
  }
}



 

So now, if you want to add options to the tsc typescript build cli, you can do so and keep the start:dev script clean. Read more on tsc options here: Compiler Options · TypeScript. Optional package npm-run-all: npm-run-all this is a nice package to use if you wish to run a series of custom scripts. You can even run them in parallel! Great for doing SASS builds along side your server side compiling.

 

Installing project dependencies.

Ok, now that we got our feet wet in setting up our application project folder, let's start coding! First let's install some dependencies:

I will be using Yarn:

 

$ yarn add ts-node typescript request nodemon

 

This should only take a couple of seconds. Once that is complete, you can check your package.json file to see dependencies and version releases:

 

$ vim package.json

 

Result:

{
  "name": "aMEBA-app",
  "version": "1.0.0",
  "main": "index.js",
  "license": "MIT",
  "scripts": {
    "start": "node app.js",
    "start:dev": "nodemon app.js",
    "start:ts-dev": "nodemon --exec ./node_modules/.bin/ts-node -- ./src/app.ts"
  },
  "dependencies": {
    "nodemon": "^1.11.0",
    "request": "^2.81.0",
    "ts-node": "^3.0.4",
    "typescript": "^2.3.3"
  }
}

 

If you are using Typescript or Babel, then you will have to make sure you install the required dependencies in order to have your code compile correctly for NodeJS to understand.

Note that I am using Typescript, so I have installed ts-node and typescript as dependencies. I will add the type definitions to take advantage of using typescript for creating my node application.

 

Using yarn to save it to my devDependencies:

 

$ yarn add --dev @types/node @types/request

 

NPM:

 

$ npm install --save-dev @types/node @types/request

 

 

We'll be using Express JS to help with our web application along side with the types:

 

$ yarn add express body-parser method-override
$ yarn add --dev @types/express @types/body-parser @types/method-override 

 

 

 

Results:

 

  "devDependencies": {
    "@types/body-parser": "^1.16.3",
    "@types/node": "^7.0.22",
    "@types/method-override": "^0.0.29",
    "@types/request": "^0.0.43"
  }

 

 

Since I am using Typescript, here is my tsconfig.json settings:

 

{
  "compileOnSave": false,
  "compilerOptions": {
    "target": "es5",
    "noLib": false,
    "lib": ["es2016", "dom"],
    "module": "commonjs",
    "moduleResolution": "node",
    "noImplicitAny": true,
    "removeComments": true,
    "preserveConstEnums": true,
    "sourceMap": false,
    "typeRoots": ["node_modules/@types"],
    "rootDir": "src/server",
    "outDir": "dist"
  },
  "include": [
    "src/**/*"
  ],
  "exclude": [
    "node_modules",
    "**/*.spec.ts"
  ]
}






 

Pay attention to the typeRoots key, this will point to the @types directory that holds the type definitions you installed. This will be auto-referenced during the compiling process.

 

Now once we create our app.ts file in our src directory and run tsc, we will have a file called app.js in our dist directory. Make sure to read up on the tsconfig options! tsconfig.json · TypeScript  Note: setting noLib and lib is redundant: noLib will include all available libraries in your application while lib will only include the libraries in which you want to target: es2016 (es6) features, and dom (js features for regular dom accessors) for example.

 

Git init project creation.

Next let's create a git init folder and add node_modules and our dist directory to it.

 

$ git init
$ touch .gitignore
$ echo "node_modules" >> .gitignore && echo "dist" >> .gitignore

 

This will ignore these two directories to help save on project space in GitHub or GitLab for your online repository. If you use yarn, then you can also add the yarn.lock file to keep the cache clean for later rebuilds.

 

Creating the actual Node JS Application.

Create a src directory to place all our scripts in, if you are not going with Typescript or Babel, then you can setup the project however you wish but in my honest option, try Typescript or Babel

Note: If you are not going to be coding in Typescript or Babel, then you can replace the import statements to just const <var_name> = require('<module_name>'). NodeJS uses the v8 engine which is a limited version of ES6. You can enable more ES6 features by running node with the --harmony flag. More on that here: ECMAScript 2015 (ES6) and beyond |

Node.js

 

 

 mkdir src

 

Now in the src directory, create a file call app.ts (app.js for non Typescripters).

 

$ touch src/app.ts

 

You should now see your file listed in your src directory.

 

Open of the newly created app file and let's enter (or copy paste ):

 

"use strict"
import * as express from 'express'
import * as path from 'path'
import * as bodyParser from 'body-parser'
import * as methodOverride from 'method-override'
import * as request from 'request'

// some variable setup
const env = process.env.NODE_ENV || 'development'
const port = process.env.PORT || 3000


// Express app creation and configuration
const app = express()
// Do not display: Express JS in the x-powered-by header
app.set('x-powered-by', false)


// Allow trust from proxies and SSL support from your load balancer
// Especially if you are running on NGINX and upstreams
app.enable('trust proxy')


// setup body-parser and method-override lets express have access to body posts
// and PUT AND PATCH methods.
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(methodOverride((req: any) => {
  if (req.body && typeof req.body === 'object' && '_method' in req.body) {
    // look in urlencoded POST bodies and delete it
    var method = req.body._method
    delete req.body._method
    return method
  }
}))


// mount our api router
app.use('/api', require('./routes').router)
app.get('/', (req: any, res: any) => res.redirect('/api'))


// error handling
// development will print stacktrace esle an empty object
app.use((err: any, req: any, res: any, next: any) => {
  res.status(err.status || 500).json({
    message: err.message,
    error: env === 'development' ? err : {}
  })
})


//now that the app is setup, start the server
app.listen(port, () => {
  console.log(`Server is listening on port: ${port}`)
})







 

You may notice that this is slightly different from you would normally see when coding in regular JS but in ES6 or Typescript we have the import feature that allows us to import modules like other OOP languages. (Yes I know, JS/ES6/Typescript is more prototypal in structure but still OOP non-the-less). Also, no semicolons! You can use them if you wish, but with ES6 and above these are optional as the compiler will insert them automatically if not present. I have not seen any compiling issues concerning time when using semicolons or not.

 

Creating our router file.

Express lets us create mount points to urls which will easily allow us to delegate certain url paths to specific router functions. Create a file called router.ts (router.js). Please note that I am explicitly assigning the req (request) and res (response) arguments to any. I have also set noImplicitAny in my tsconfig.json to allow the tsc compiler warn of any errors pertaining to variable that is implicit to any instead of a expected type. As this can be annoying at times, but is very helpful later when refactoring later for a more production quality app. For instance, req: express.Request, res: express.Response. Explicit type checking is helpful to future developers so that they will know what type the arguments are expected to be. For this small example, I will just use :any as this will just compile and use whatever type that is passed. This will also help if you decide to use node modules that may not have type definitions. You can always change it later during your refactoring. Also, note that I am not coding entirely in Typescript convention: Classes, interfaces, implements, extends etc. This small example, in my opinion, wouldn't really benefit from it anyways. If you were expand on this by making multiple modules that would utilizing those features, then I would say that would be a more appropriate use case for them.

 

"use strict"
import * as express from 'express'


const router = express.Router()


router.get('/', (req: any, res: any) => {
  res.json({ message: "Hello from aMEBA!" })
})


module.exports = router




 

Running our application.

Now that we have our aMEBA in it basic form, let's see if it works!

 

$ npm run start:ts-dev


> aMEBA-app@1.0.0 start:ts-dev /home/elmiguel/CodeProjects/aMEBA-app
> nodemon --exec ./node_modules/.bin/ts-node -- ./src/app.ts


[nodemon] 1.11.0
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `./node_modules/.bin/ts-node ./src/app.ts`
Server is listening on port: 3000






 

So far so go, lets go to our browser and see what we get:

 

Awesome! it works!

 

So at this point we should have our project setup something similar to this:

 

If you managed to get this far, congratulations! You just made your first NodeJS + ExpressJS Web API.

 

Setting up MongoDB and Creating Mongoose Models and Middleware.

This section we'll cover going over the ins and outs of creating a mongdb connection with NodeJS and using Models to interact with our document storage. We will implement a small middleware function on our router to intercept our calls to our local API and make sure that we have the correct authentication to talk to the BbRest API. This is what I like to call a passthrough API setup. Basically will not be trying to reinvent the BbRest API but merely pre-authenticating our request locally and letting app keep the token session alive. Each time we make call we either use the stored token or request a new one. Locally we never authenticate to our app, the app is doing the transfer and only relaying the information. The purpose of our router is to only allow what requests we deem necessary for the end result of the app. For example, if we never plan on sending any data to Bb, then we only have to relay GET requests. In the example, we'll set the router to except all http methods: router.all()

 

Prerequisites:

 

Once your MongoDB install and running press Ctrl+C to stop the app if you have it still running and let's install some more dependencies:

 

$ yarn add express-session connect-mongo mongoose bluebird
$ yarn add --dev @types/express-session @types/connect-mongo @types/mongoose

 

Config.ts

Create another script file called config.ts (config.js) and paste the following:

 

"use strict"
export let mongooseConfig: any = {
  database: 'mongodb://localhost:27017/aMEBA'
}

 

Back in our app.ts file let modify it to connect to MongoDB and create our new database: aMEBA (you can make this any name you wish but I like to keep it that same name as the app.)

 

Edit app.ts

 

"use strict"
import * as express from 'express'
import * as session from 'express-session'
import * as path from 'path'
import * as bodyParser from 'body-parser'
import * as methodOverride from 'method-override'
import * as request from 'request'
import * as mongoStore from 'connect-mongo'
import { mongooseConfig } from './config'
mongoStore(session)


const mongoose = require('mongoose')
mongoose.Promise = require('bluebird')


mongoose.connection.once('open', () => {
  mongoose.connection.on('error', (err: any) => {
    console.log(err)
  })
})
mongoose.connect(mongooseConfig.database)


//...rest of code below 



 

Now if you run the app again, it should connect and the app will load. Now if you were to check the mongodb instance running, you will see that there is not aMEBA database:

 

$ mongo
MongoDB shell version: 3.2.12
connecting to: test
> show databases;
local                0.000GB


 

This is because connect-mongo will not create the database until there is an actual interaction.

 

Let's fix that shall we!

 

Models

Create a folder in your src directory called models, this will be the place where we will be making all our models in to keep the project clean.

 

token.ts

 

import * as mongoose from 'mongoose'
const Schema = mongoose.Schema


const TokenSchema = new Schema({
  access_token: String,
  token_type: String,
  expires_in: String
})
TokenSchema.set('timestamps', true)
TokenSchema.set('capped', { size: 1024, max: 1 })


TokenSchema.statics.getToken = function(cb: any) {
  return this.findOne({}, (err: any, token: any) => {
    if (err) throw (err)
    cb(token)
  })
}


TokenSchema.methods.isValid = function(): boolean {
  // console.log(this)
  let createdAt = new Date(this.createdAt)
  // console.log('createAt:', createdAt)
  let expires_in = new Date(createdAt + this.expires_in + '1000').toISOString()
  // console.log('calculated expires_in:', expires_in)
  let now = new Date().toISOString()
  // console.log('now:', now)


  return now <= expires_in
}


module.exports = mongoose.model('Token', TokenSchema, 'tokens')





 

Now that we have our token model, we can now be able to store our request auth tokens. Note that I have this collection capped to 1 document (line 11). This is so that we are only storing one token and that we can allow the application to look at this token only to check its validity.

 

 

config.ts

"use strict"
export const mongooseConfig: any = {
  database: 'mongodb://localhost:27017/aMEBA'
}

export const BbConfig = {
  key: '<YOUR_APP_KEY>',
  secret: '<YOUR_APP_SECRET>',
  credentials: 'client_credentials',
  cert_path: './trusted/keytool_crt.pem',
  url: 'https://<YOUR_BB_INSTANCE>/learn/api/public/v1',
  auth: ''
}


BbConfig.auth = new Buffer(BbConfig.key + ":" + BbConfig.secret).toString("base64")




BbRest API Middleware

Let's create a simple middleware file. Create a folder call middleware and also a file called bbrest-api.ts (bbrest-api.js):

 

 

import { BbConfig } from '../config'
const Token = require('../models/token')
const request = require('request')


export const api = (req: any, res: any, next: any) => {
  if (process.env.NODE_DEBUG) {
    console.log('[ bbrest-api:bbApiUrl ]\t', req.app.locals.bbApiUrl)
    console.log('[ bbrest-api:bbconfig ]\t', req.app.locals.bbpayload)
  }
  next()
}


export const setToken = (cb: any) => {
  request(
    {
      method: 'post',
      url: `${BbConfig.url}/oauth2/token`,
      headers: {
        "Authorization": `Basic ${BbConfig.auth}`
      },
      form: {
        grant_type: 'client_credentials'
      },
      json: true
    },
    (err: any, res: any, body: any) => {
      if (err) {
        console.log('Oops!')
        throw (err.message)
      }
      Token.create(body, (err: any, token: any) => {
        if (err) throw (err)
        cb(token)
      })
    }
  )
}






 

 

Let's update our app.ts.

 

app.ts

 

"use strict"
import * as express from 'express'
import * as session from 'express-session'
import * as path from 'path'
import * as bodyParser from 'body-parser'
import * as methodOverride from 'method-override'
import * as request from 'request'
import * as mongoStore from 'connect-mongo'
import { mongooseConfig, BbConfig } from './config'
import { setToken } from './middleware/bbrest-api'


mongoStore(session)


const mongoose = require('mongoose')
mongoose.Promise = require('bluebird')


mongoose.connection.once('open', () => {
  mongoose.connection.on('error', (err: any) => {
    console.log(err)
  })
})
mongoose.connect(mongooseConfig.database)
// require our models here
const Token = require('./models/token')




// some variable setup
const env = process.env.NODE_ENV || 'development'
const port = process.env.PORT || 3000




process.env.NODE_DEBUG == '*' ? true : false


// Express app creation and configuration
const app = express()
// Do not display: Express JS in the x-powered-by header
app.set('x-powered-by', false)


// Allow trust from proxies and SSL support from your load balancer
// Especially if you are running on NGINX and upstreams
app.enable('trust proxy')


// Add our BbConfig to our app
app.locals.bbApiUrl = BbConfig.url
Token.getToken((token: any) => {
  if (token && token.isValid()) {
    app.locals.bbpayload = token
  } else {
    setToken((token: any) => {
      app.locals.bbpayload = token
    })
  }
})
// setup body-parser and method-override lets express have access to body posts
// and PUT AND PATCH methods.
app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(methodOverride((req: any) => {
  if (req.body && typeof req.body === 'object' && '_method' in req.body) {
    // look in urlencoded POST bodies and delete it
    var method = req.body._method
    delete req.body._method
    return method
  }
}))


// mount our api router
app.use('/api', require('./routes'))
app.get('/', (req: any, res: any) => res.redirect('/api'))


// error handling
// development will print stacktrace esle an empty object
app.use((err: any, req: any, res: any, next: any) => {
  res.status(err.status || 500).json({
    message: err.message,
    error: env === 'development' ? err : {}
  })
})


//now that the app is setup, start the server
app.listen(port, () => {
  console.log(`Server is listening on port: ${port}`)
})


















 

Now if you re-run your app:

$ npm run start:ts-dev

 

The application should run without any errors. If you open another terminal do the following, you should see our aMEBA database along with our tokens collection that hold our token:

 

$ mongo
> show databases;
aMEBA                0.000GB
local                0.000GB
> use aMEBA;
switched to db aMEBA
> show collections
tokens
> db.tokens.find();
{ "_id" : ObjectId("592587cfc137e1196f2ad1f1"), "updatedAt" : ISODate("2017-05-24T13:17:03.221Z"), "createdAt" : ISODate("2017-05-24T13:17:03.221Z"), "access_token" : "<TOKEN_HERE>", "token_type" : "bearer", "expires_in" : "1444", "__v" : 0 }
> 





 

YAY! Success!!

 

Now for our router refactor and passthrough API. We will create the passthrough API by capturing the first part the path and dynamically load the correct model to set the correct BbRest API route.

 

Refactoring the router and adding a BbUser Model

router.ts

Update the router to the following:

 

"use strict"
import * as express from 'express'
import * as request from 'request'


const router = express.Router()
const BbUser = require('./models/bbuser')
const MODELS: any = {
  users: BbUser,
  // more models would go here
}


router.get('/', (req: any, res: any) => {
  res.json({ message: "Hello from aMEBA!" })
})


router.all('/users/:userId?', (req: any, res: any) => {
  if (process.env.NODE_DEBUG) console.log(req.params)
  if (process.env.NODE_DEBUG) {
    console.log(req.params.userId)
    console.log('isMany?:', req.params.userId == undefined ? true : false)
  }
  doApi(req, res, req.params.userId == undefined ? true : false)
})




function doApi(req: any, res: any, isMany: boolean) {
  if (process.env.NODE_DEBUG) {
    console.log('[ bbapi:req.path   ]\t', req.path)
    console.log('[ bbapi:req.params ]\t', req.params)
    console.log('[ bbapi:req.query  ]\t', req.query)
    console.log('[ bbapi:req.body   ]\t', req.body)
  }


  let params: any = Object.assign({}, req.params)
  let p: string[] = req.path.split('/').slice(1)


  let model: any
  model = MODELS[p[0]]


  // let isMany:boolean = (typeof params != undefined && params[0] != undefined && params != {}) ? false : true
  if (!isMany) {
    for (let key in req.params) {
      let _key = key
      if (key == 'userId') {
        // check for externalId else userName
        if (/^\d+/.test(req.params[key])) {
          _key = 'externalId'
        } else {
          _key = 'userName'
        }
        delete params.userId
      }
      params[_key] = req.params[key].replace(/^(\w+:)/gi, '')
    }
  }


  if (process.env.NODE_DEBUG) console.log(model.name, p, params)


  let docs = model.find(params).exec()
  docs.then((docs: any) => {
    if (process.env.NODE_DEBUG) console.log(docs)
    console.log('should have documents here.....')
    // if no docs then get docs from bb
    console.log(docs.length)
    // if (!docs && docs == []) {
    if (docs.length == 0) {
      // save retrieved to mongo
      console.log('I should be getting the data from Bb here!')
      getDataAndSave(req, res, model, params)
    } else {
      // return docs to client
      console.log('We have some docs...')
      console.log(docs)
      sendJson(res, null, docs)
    }
  })
}


function getDataAndSave(req: any, res: any, model: any, params: any) {
  if (process.env.NODE_DEBUG) console.log('[bbpi.doApi] [document.then()] retrieving document from Bb')
  let url = req.app.locals.bbApiUrl + req.path
  console.log(req.app.locals.bbpayload.access_token)
  if (process.env.NODE_DEBUG) console.log(url)
  request({
    method: req.method,
    url: url,
    headers: {
      "Authorization": `Bearer ${req.app.locals.bbpayload.access_token}`
    },
    qs: req.query,
    form: req.body,
    json: true
  }, (err: any, resp: any, body: any) => {
    // console.log(resp)
    console.log(body)
    // if (body.status != 404) {
    if (body) {
      console.log('docs loaded')
      model.create(body, (err: any, docs: any) => {
        // model.insertMany((err: any, docs: any) => {
        if (err) throw (err)
        sendJson(res, null, docs)
      })
    } else {
      // doc(s) do(es) not exist in Bb
      // pass the result(s) back to the client
      sendJson(res, err, body)
    }
  })
}


function sendJson(res: any, err: any, data: any) {
  if (err || !data) {
    res.status(404).json({ message: 'No records could be found' });
  } else {
    // console.log(data)
    res.status(200).json(data);
  }
}




module.exports = router














































 

BbUser Model

 

Create a new model in the models directory:

bbuser.ts

import crypto = require('crypto')


const mongoose = require('mongoose')
const fieldsAliasPlugin = require('mongoose-aliasfield')
const Schema = mongoose.Schema


/**
 * User Schema
 */


let BbUserSchema = new Schema({
  id: { type: String, alias: 'userId' },
  uuid: String,
  externalId: String,
  dataSourceId: String,
  userName: { type: String },
  educationLevel: String,
  gender: String,
  created: String,
  lastLogin: String,
  systemRoleIds: [],
  availability: {
    available: String
  },
  name: {
    given: String,
    family: String,
    title: String
  },
  contact: {
    email: String
  }
})


let handleE11000 = function(error: any, doc: any, next: any) {
  if (error.name === 'MongoError' && error.code === 11000) {
    next(new Error('There was a duplicate key error'))
  } else {
    next(error);
  }
}


BbUserSchema.post('save', handleE11000);
BbUserSchema.post('update', handleE11000);
BbUserSchema.post('findOneAndUpdate', handleE11000);
BbUserSchema.post('insertMany', handleE11000);




BbUserSchema.plugin(fieldsAliasPlugin)
BbUserSchema.set('timestamps', true)


const default_select = 'id uuid externalId userName name email'


/**
 * Virtuals
 */


BbUserSchema
  .virtual('password')
  .set((password: string) => {
    this._password = password
    this.salt = this.makeSalt()
    this.hashed_password = this.encryptPassword(password)
  })
  .get(() => {
    return this._password
  })


BbUserSchema.set('toJSON', {
  transform: function(doc: any, ret: any, options: any) {
    delete ret.password; return ret;
  }
})




BbUserSchema.pre('save', (next: any) => {
  //if (!this.isNew) return next()


  //if (!validatePresenceOf(this.password)) {
  //    next(new Error('Invalid password'))
  //} else {
  next()
  //}
})


/**
 * Methods
 */


BbUserSchema.methods = {


  /**
   * Authenticate - check if the passwords are the same
   *
   * @param {String} plainText
   * @return {Boolean}
   * @api public
   */


  authenticate: (plainText: string) => {
    // coming from LDAP, if we get here, we are already authenticated
    // TODO: remove when confirmed
    return true
  },


  /**
   * Make salt
   *
   * @return {String}
   * @api public
   */


  makeSalt: function() {
    return Math.round((new Date().valueOf() * Math.random())) + ''
  },


  /**
   * Encrypt password
   *
   * @param {String} password
   * @return {String}
   * @api public
   */


  encryptPassword: (password: string) => {
    if (!password) return ''
    try {
      //noinspection JSUnresolvedVariable
      return crypto
        .createHmac('sha1', this.salt)
        .update(password)
        .digest('hex')
    } catch (err) {
      return ''
    }
  }
}


/**
 * Statics
 */


BbUserSchema.statics = {


  /**
   * Load
   *
   * @param {Object} options
   * @param {Function} cb
   * @api private
   */


  load: function(options: any, cb: any) {
    let criteria = options.criteria || {}
    let select = options.select || default_select
    let isPrimaryId: boolean
    try {
      console.log('[BbUser.load() [options.criteria]', criteria)
      isPrimaryId = /^_\d+_1/.test(criteria.userId)
    } catch (ex) {
      console.log(ex)
      isPrimaryId = false
    } finally {
      console.log(isPrimaryId)
    }
    let _criteria: any = {}
    if (!isPrimaryId) {
      for (let key in criteria) {
        if (key == 'userId') {
          // check for externalId else userName
          if (/^\d+/.test(criteria.userId)) {
            _criteria.externalId = criteria.userId
          } else {
            _criteria.userName = criteria.userId
          }
        } else {
          _criteria[key] = criteria[key]
        }
      }
    } else {
      _criteria = Object.assign({}, criteria)
      _criteria.id = criteria.userId
      delete _criteria.userId
    }
    console.log('[BbUser.load()] [_criteria]:', _criteria)
    return this.findOne(_criteria)
      .select(select)
      .exec(cb)
  },
  list: function(options: any, cb: any) {
    var criteria = options.criteria || {}
    var select = options.select || default_select
    var sort = options.sort || 1
    var page = options.page || 0
    var limit = options.limit || 30
    return this.find(criteria)
      .select(select)
      .populate('users', select)
      .sort({ _id: sort })
      .limit(limit)
      .skip(limit * page)
      .exec(cb)
  },
  findOrCreate: function(options: any, cb: any) {
    var criteria = options.criteria || {}
    var select = options.select || default_select
    let isPrimaryId: boolean
    try {
      console.log('[BbUser.load() [options.criteria]', options.criteria)
      isPrimaryId = /^_\d+_1/.test(options.criteria.userId)
    } catch (ex) {
      console.log(ex)
      isPrimaryId = false
    } finally {
      console.log(isPrimaryId)
    }
    let _criteria: any = {}
    if (!isPrimaryId) {
      for (let key in options) {
        if (key == 'userId') {
          _criteria.userName = options.criteria.userId
        } else {
          _criteria[key] = options.criteria[key]
        }
      }
    } else {
      _criteria = Object.assign({}, criteria)
    }
    console.log('[BbUser.findOrCreate()] [_criteria]:', _criteria)
    this.findOne(_criteria, (err: any, user: any) => {
      if (err) throw (err)
      if (user) {
        cb(err, user)
      } else {
        let _user = new this(criteria)
        cb(null, _user.save())
      }
    })
  }
}


module.exports = mongoose.model('BbUser', BbUserSchema, 'bbusers')

 

 

Note that most of the code is not needed: encryptPassword and validation, but I have it there in case you wish to implement a local login system. This would allow you to add users by first calling the BbRest API, saving them locally to our mongodb instance, then having the local login system check to see if a user exists and is valid.

 

The take away from here would be the main flow: MongoDB First, if not already exists then get from BbRest API, If found save.

 

Now if you run the app, and go to your app url: http://localhost:3000/api/users/userName:<ENTER_A_USERNAME>

 

What happens here is that we are just passing the end route path to the BbRest API and then printing the results that it gave. All method and actions are actually BbRest API Specs: Explore APIs

 

MAGIC!

 

MongoDB Magic!

Checking our mongodb after the API Call:

 

> show collections;
bbusers
tokens
> db.bbusers.find();
{ "_id" : ObjectId("5925cae83f2ee1264e928e23"), "updatedAt" : ISODate("2017-05-24T18:03:20.544Z"), "createdAt" : ISODate("2017-05-24T18:03:20.544Z"), "id" : "_<USER_PK>_1", "uuid" : "<UUID>", "externalId" : "<USER_BATCH_UID>", "dataSourceId" : "_<DATA_SOURCE_PK>_1", "userName" : "mbechtel", "educationLevel" : "Unknown", "gender" : "Unknown", "created" : "2013-07-02T15:02:36.000Z", "lastLogin" : "2017-05-24T11:42:05.000Z", "contact" : { "email" : "mbechtel@irsc.edu" }, "name" : { "given" : "Michael", "family" : "Bechtel", "title" : "System Admin" }, "availability" : { "available" : "Yes" }, "systemRoleIds" : [ "SystemAdmin" ], "__v" : 0 }
> 



 

 

YAY! It Works!

 

Let's step this up one more notch: More User routes and Courses!

Ok, so we have a simple router that only listens to /user/:userId? This means we can only interact with the /users routes. This can be easily expanded on now with just creating more models and routes for those models. Let's create a course.ts model in src/models and then add some route functionality to interact with the BbRest API.

 

routes.ts

Let's just add a quick route for the users:

router.all('/users/:userId/courses', (req: any, res: any) => {
  doApi(req, res, true)
})

 

This route is really listed under the course membership routes but I personally think that it belongs to the user routes. Ok back to courses.

 

courses.ts

import crypto = require('crypto')


const mongoose = require('mongoose')
const Schema = mongoose.Schema


/**
 * User Schema
 */


let CourseSchema = new Schema(
  {
    id: String,
    uuid: String,
    externalId: String,
    dataSourceId: String,
    courseId: String,
    name: String,
    created: Date,
    organization: Boolean,
    ultraStatus: String,
    allowGuests: Boolean,
    readOnly: Boolean,
    availability: {
      available: String,
      duration: {
        type: { type: String }
      }
    },
    enrollment: {
      type: { type: String }
    },
    locale: {
      force: Boolean
    }
  })
CourseSchema.set('timestamps', true)


const default_select = 'id uuid externalId courseId courseName organization availability'




/**
 * Statics
 */


CourseSchema.statics = {


  /**
   * Load
   *
   * @param {Object} options
   * @param {Function} cb
   * @api private
   */


  load: function(options: any, cb: any) {
    var select = options.select || default_select
    console.log('[Course.load()] [criteria]:', options.criteria)
    return this.findOne(options.criteria)
      .select(select)
      .exec(cb)
  },
  list: function(options: any, cb: any) {
    var criteria = options.criteria || {}
    var select = options.select || default_select
    var sort = options.sort || 1
    var page = options.page || 0
    var limit = options.limit || 30
    return this.find(criteria)
      .select(select)
      .populate('users', select)
      .sort({ _id: sort })
      .limit(limit)
      .skip(limit * page)
      .exec(cb)
  },
  findOrCreate: function(options: any, cb: any) {
    var criteria = options.criteria || {}
    var select = options.select || default_select
    this.findOne(options.criteria, (err: any, user: any) => {
      if (err) throw (err)
      if (user) {
        cb(err, user)
      } else {
        let _user = new this(criteria)
        cb(_user.save())
      }
    })
  }
}


module.exports = mongoose.model('Course', CourseSchema, 'courses')



































 

Pretty standard stuff. A little tip, currently I am exporting this as a module package and exporting it as a mongoose model. What this means is that the mongoose model attaches itself to the default mongoose connection directly. If you are going to have multiple mongo connections that will share these models, then you need to export just the schemas and then register them to the connection variable.

 

Example: Taken from the mongoose documentation from the GitHub repository: GitHub - Automattic/mongoose: MongoDB object modeling designed to work in an asynchronous environment.

This ties the model directly to the connection instance that was assigned to a variable.

let conn = mongoose.createConnection('your connection string'),
  MyModel = conn.model('ModelName', schema),
  m = new MyModel;
m.save(); // works


 

So now you can just do MyModel.db.host, MyModel.db.port, MyModel.db.name to get the database information.

 

Now we register this to our router MODELS variable:

 

router.ts

 

Change:

 

const BbUser = require('./models/bbuser')
const MODELS: any = {
  users: BbUser,
  // more models would go here
}

 

To:

const BbUser = require('./models/bbuser')
const Course = require('./models/course')
const MODELS: any = {
  users: BbUser,
  courses: Course
}


 

We have added the new Course model and registered this to our MODELS variable. Since the router is just a passthrough, what we are doing is capturing the first route path and loading the correct model on the fly! Now we just have to let our express app know what to do with /api/courses.

 

Add these routes under your users routers:

 


router.all('/courses/:courseId?', (req: any, res: any) => {
  doApi(req, res, req.params.courseId == undefined ? true : false)
})


router.all('/courses/:courseId/users/:userId?', (req: any, res: any) => {
  doApi(req, res, req.params.userId == undefined ? true : false)
})









 

Now let's test it out, run the app and look up a course:

 

back in mongo:

 

> show collections;
bbusers
courses
tokens
> db.courses.find();
{ "_id" : ObjectId("5926d358192c007e9c63f2e7"), "updatedAt" : ISODate("2017-05-25T12:51:36.346Z"), "createdAt" : ISODate("2017-05-25T12:51:36.346Z"), "id" : "_2841_1", "uuid" : "ad842f780aa845879438a8cb0dd530dd", "externalId" : "LOR-CC-mbechtel", "dataSourceId" : "_1186_1", "courseId" : "LOR-CC-mbechtel", "name" : "LOR-CC-mbechtel", "created" : ISODate("2014-03-04T14:24:14Z"), "organization" : false, "ultraStatus" : "Classic", "allowGuests" : true, "readOnly" : false, "locale" : { "force" : false }, "enrollment" : { "type" : "InstructorLed" }, "availability" : { "available" : "Yes", "duration" : { "type" : "Continuous" } }, "__v" : 0 }
> 



 

YAY!

 

Conclusion

Well I hope you enjoyed my take on creating a small Node JS Express web application and making BbRest API calls. From here, you should be able to create awesome web apps with NodeJS For Bb!

 

Now that I had you read through this entire article....here is the source code: GitHub - elmiguel/aMEBA: A MongoDB, Express JS, BbRest API Application

Working for Blackboard Support I get to learn something new everyday.  The new Rest API gives much simpler access to course information that is consumed/created externally from Blackboard or wrapped in a B2 and delivered as a course tool.     What you chose to do with the data is the interesting part.

 

This small project came from a desire to learn the new Rest interface in order too better support it. Along with my Wifes input that you should not just stop once you have the data, you need to do something with it.  My wife, Adele works on a Peer Mentor Project for People with Drug and Alcohol challenges and often discuses the way language can have an effect on the groups she teaches.  I mentioned that IBM Watson now has an Emotional Tone Content Analyzer and we though it would be good to run some of her teaching materials into it to see how she scored on the openness and analytical scales.

 

 

Text from Watson Site Tone Analyzer | IBM Watson Developer Cloud

 

The IBM Watson™ Tone Analyzer Service uses linguistic analysis to detect three types of tones from written text: emotions, social tendencies, and writing style. Emotions identified include things like anger, fear, joy, sadness, and disgust. Identified social tendencies include things from the Big Five personality traits used by some psychologists. These include openness, conscientiousness, extraversion, agreeableness, and emotional range. Identified writing styles include confident, analytical, and tentative.

 

This was a useful exercise so I though we would try this with Blackboard content and use the new Rest API to get the content out.

 

NB: I am not a developer so please do not consider any of this as best practice.

 

 

 

 

 

Step 1 Using the new rest API:

 

The new documentation is very easy to use and can be found here:  https://developer.blackboard.com/portal/displayApi

 

Rest Process:

 

  • Register as a developer in https://developer.blackboard.com/
  • Register the Rest Application on https://developer.blackboard.com/  You will get the application ID, Rest Key and Secrete
  • In your Blackboard environment create a Rest integration using the application key
  • Create the application using the REST API Key and Secrete in Eclipse or other IDE.
  • Test the application.

 

pastedImage_5.png

 

 

 

Register the Application here: https://developer.blackboard.com/portal/applications

 

rest Reg.png

Setup the API Integration on the Blackboard server:  System admin > REST API Integration's > Create Integration

 

image2016-7-20 14-31-20.png

The Rest API in Q2 2016 can now be used by an external application so lets start  with a simple java project to connect to the Rest API.

 

Using the examples from  https://github.com/blackboard/BBDN-REST-Demo-Java-Webapp I created 3 classes to manage the token, coursepk1 and content items.

 

getToken: This contain a method that takes the user, password and URL and provides the OAuth Token.

 

getcoursePK1: This contains a method that takes the URL, Token and Course BatchUID and return the course PK1

 

getContent: This has 2 methods

 

  • ParentContentItems: This method takes the URL, Token and Course Pk1 and returns an Object containing the Parent Content details.  This uses jackson-core-asl to convert the json to POJO.  This lists the top level content areas in a course.
  • ChildContenItemsJSON: This method takes the URL, Token and Parent Content Pk1 and returns an Object containing the Parent Content details.  This uses jackson-core-asl to convert the json to POJO.  This lists folder, assignment, items etc that are held in the parent content object.

 

TestMethod: This is to test the above methods.  Set the user, Pass, URL.  Run this to get the top level content and the get the text from the child content items into one string.  This Content of each content item has had the HTML stripped out of it using the Jsoup HTML parser https://jsoup.org/

 

Example course for testing.

 

Capture.PNG

Full project is attached.  GetCourseContent.zip

 

public class TestMethods {
     
    //Setup the user and passwords for the Rest Integration.
    public final static String USER = "Rest User";
    public final static String PASS = "Pass Auto Generated on Rest Registration";
    public final static String VIP = "Https://testserver.blackboard.com/";
     

    public static void main(String[] args) {
        // TODO Auto-generated method stub
         
        //Test the Get Token Method.
        String AccessToken = getToken.GetToken(USER, PASS, VIP);
        System.out.println("Token from getToken class: "+AccessToken);

         
        //Get Course Pk1 using the getCoursePK1 method
        String coursePK1 = getCoursePK1.coursePK1(VIP,  AccessToken, "newcourse");
        System.out.println("course Pk1: "+coursePK1);
         
        //Setup a String to use to populate the Body of the content items.
        StringBuilder stringBuilder = new StringBuilder();
         
                 
        //Get JSON content objects in a list of the parent content.
        List<Result> results = getContent.ParentContenItems(VIP, AccessToken, coursePK1);
        for(int i =0; i < results.size();i++){
             
            //Print the Parent content Pk1
            System.out.println("Parent ID: "+results.get(i).getId());
             
            //Get Child Content into results object
            List<Result> childresults = getContent.ChildContenItemsJSON(VIP, AccessToken, coursePK1,results.get(i).getId());
            for(int ic =0; ic < childresults.size();ic++){
                 
                //Only user content items that contain text in the body
                if (childresults.get(ic).getBody() != null && !childresults.get(ic).getBody().isEmpty()){
                 
                //Print the Child PK1 and body
                System.out.println("Child Item: "+childresults.get(ic).getId()+"  Body: "+Jsoup.parse(childresults.get(ic).getBody()).text());

                //Add the text to the String stringBuilder
                stringBuilder.append(Jsoup.parse(childresults.get(ic).getBody()).text()+"  ");
                }
                 
            }
             
             
             
            }
        //Print out the full text of all item in one text line
        System.out.println("Full text from course: "+stringBuilder);
         
        }

}

 

Output of this test method for an example course.

 

Https://mhtest3.blackboard.com/learn/api/public/v1/oauth2/token<grant_type=client_credentials,{Authorization=[Basic M2FkODI5NDQtYTY0MC00OWUwLWEyOTAtOGY5NzAwOTUxZGZhOnR4SkdIeG5JMzB5YVlLcktCcE1qNkRNOGd5TFRtV01K], Content-Type=[application/x-www-form-urlencoded]}>
Https://mhtest3.blackboard.com/learn/api/public/v1/courses/externalId:eng101
Https://mhtest3.blackboard.com/learn/api/public/v1/courses/_5366_1/contents
Https://mhtest3.blackboard.com/learn/api/public/v1/courses/_5366_1/contents/_24537_1/children
Https://mhtest3.blackboard.com/learn/api/public/v1/courses/_5366_1/contents/_24538_1/children
William Shakespeare (/??e?ksp??r/;[1] 26 April 1564 (baptised) – 23 April 1616)[nb 1] was an English poet, playwright, and actor, widely regarded as the greatest writer in the English language and the world's pre-eminent dramatist.[2] He is often called England's national poet, and the "Bard of Avon".[3][nb 2] His extant works, including collaborations, consist of approximately 38 plays,[nb 3] 154 sonnets, two long narrative poems, and a few other verses, some of uncertain authorship. His plays have been translated into every major living language and are performed more often than those of any other playwright.[4] Shakespeare was born and brought up in Stratford-upon-Avon, Warwickshire. At the age of 18, he married Anne Hathaway, with whom he had three children: Susanna, and twins Hamnet and Judith. Sometime between 1585 and 1592, he began a successful career in London as an actor, writer, and part-owner of a playing company called the Lord Chamberlain's Men, later known as the King's Men. He appears to have retired to Stratford around 1613, at age 49, where he died three years later. Few records of Shakespeare's private life survive, which has stimulated considerable speculation about such matters as his physical appearance, sexuality, and religious beliefs, and whether the works attributed to him were written by others.[5] Shakespeare produced most of his known work between 1589 and 1613.[6][nb 4] His early plays were primarily comedies and histories, and these are regarded as some of the best work ever produced in these genres. He then wrote mainly tragedies until about 1608, including Hamlet, Othello, King Lear, and Macbeth, considered some of the finest works in the English language.[2] In his last phase, he wrote tragicomedies, also known as romances, and collaborated with other playwrights. Many of his plays were published in editions of varying quality and accuracy during his lifetime. In 1623, however, John Heminges and Henry Condell, two friends and fellow actors of Shakespeare, published a more definitive text known as the First Folio, a posthumous collected edition of his dramatic works that included all but two of the plays now recognised as Shakespeare's.[7] It was prefaced with a poem by Ben Jonson, in which Shakespeare is hailed, presciently, as "not of an age, but for all time".[7] In the 20th and 21st centuries, his works have been repeatedly adapted and rediscovered by new movements in scholarship and performance. His plays remain highly popular, and are constantly studied, performed, and reinterpreted in diverse cultural and political contexts throughout the w  Watch Video Introduction to William Wordsworth User: n/a - Added: 7/18/12 William Wordsworth was a major English Romantic poet who, with Samuel Taylor Coleridge, helped to launch the Romantic Age in English literature with their joint publication Lyrical Ballads.    

 

 

Step 2 Deploy as a B2 Course Tool and Integrate with Watson.

 

Capture2.PNG

 

B2 Template

I used the amazing starting template  B2 from the All the Ducks team. https://github.com/AllTheDucks/atd-b2-stub

 

This B2 uses the Stripes framework and is an ideal starter B2.  Clone from GitHub  using Git bash for windows.

 

 

$ git clone https://github.com/AllTheDucks/atd-b2-stub.git BBS-AHRestContent

Cloning into 'BBS-AHRestContent'...

remote: Counting objects: 528, done.

remote: Total 528 (delta 0), reused 0 (delta 0), pack-reused 528 eceiving objects:  98% (518/528), 148.01 KiB | 286.00 KReceiving objects:  99% (523/528), 148.01 KiB | 286.00 KiB/s

Receiving objects: 100% (528/528), 167.01 KiB | 286.00 KiB/s, done.

Resolving deltas: 100% (225/225), done.

Checking connectivity... done.

 

ahulme@AHULMEP50 MINGW64 /c/Temp/AHRestContent (master)

$ cd BBS-AHRestContent/

 

ahulme@AHULMEP50 MINGW64 /c/Temp/AHRestContent/BBS-AHRestContent (master)

$ ./gradlew initB2

:initB2

Defaults found for generating building block

> Building 0% > :initB2

What is your Vendor Id (e.g. usq, unsw, qut, swin, etc)? (No spaces)  [BBS] BBS

What is your Vendor Name (e.g. University of Antarctica)? [Blackboard Support]

What is your Vendor Website (e.g. http://www.myu.edu.au/)? (No spaces)  [http://community.blackboard.com/]

What is your B2 Handle (e.g. jshack, autosignon, etc)? (No spaces)  AHRestContent

What is your B2 Name (e.g. Student View)? My RContent

What is the base java package of your project? (e.g. au.edu.uni.myproject) ? (No spaces)  com.ah

Do you want to use the course event listener? (Y/N) N

Do you want to use the Schema.xml? (Y/N) N

Do you want a system tool? (Y/N) Y

Do you want a course tool? (Y/N) Y

Initialising Building Block BBS-AHRestContent

Initialized empty Git repository in C:/Temp/AHRestContent/BBS-AHRestContent/.git/

 

BUILD SUCCESSFUL

 

Total time: 1 mins 13.789 secs

 

 

Setup the Stripes Controller for the Course tool.

 

This uses the 3 class file we created instep 1  with a new method in the getContent and the POJO classes for object have been moved to there own package.

 

getToken: This contain a method that takes the user, password and URL and provides the OAuth Token.

getcoursePK1: This contains a method that takes the URL, Token and Course BatchUID and return the course PK1

getContent: This has 3 methods

 

  • Content: This method takes the URL, Token and Course Pk1 and returns a String containing the text of the content items in the course. This only gets the text form the Parent level and one child level at the moment. .
  • ParentContentItems: This method takes the URL, Token and Course Pk1 and returns an Object containing the Parent Content details.  This uses jackson-core-asl to convert the json to POJO.  This lists the top level content areas in a course.
  • ChildContenItemsJSON: This method takes the URL, Token and Parent Content Pk1 and returns an Object containing the Parent Content details.  This uses jackson-core-asl to convert the json to POJO.  This lists folder, assignment, items etc that are held in the parent content object.

 

 

Setup the Watson service:  Login to bluemix and select the Tone Analyzer.

 

 

 

Stage 1: Getting your service credentials

Before you can work with a Watson Service, you need service credentials in Bluemix. If you already have credentials for this service, you can skip this stage.

To get your service credentials, follow these steps:

  1. Log in to Bluemix at console.ng.bluemix.net.
  2. Create an instance of the service:
    1. In the Bluemix Catalog, select your service.
    2. Under Add Service, type a unique name for the service instance in the Service namefield. For example, type tutorial-<username>. Leave the default values for the other options.
    3. Click Use.
  3. Copy your credentials:
    1. On the left side of the page, click Service Credentials to view your service credentials.
    2. Copy username and password from these service credentials.

 

This is from my account

 

Watson.png

 

 

You can test this from the command line using curl. This example is taken from the IBM documentation.

 

curl -k -u "c5fxccc-xzxcc-43b3-a95f-6edbebaa346a":"aJxFVbhXbY2c" -H "Content-Type: application/json" -d "{\"text\": \"Hi Team, I know the times are difficult! Our sales have been disappointing for the past three quarters for our data analytics product suite. We have a competitive data analytics product suite in the industry. But we need to do our job selling it! \"}" "https://gateway.watsonplatform.net/tone-analyzer/api/v3/tone?version=2016-05-19"

 

C:\Users\ahulme\Downloads\curl_749_1_ssl>curl -k -u "c5f6ce24-1ae0-232323-a95f-6edbebaa346a":"aJxF233323bY2c" -H "Content-Type: application/json" -d "{\"text\": \"Hi Team, I know the times are difficult! Our sales have been disappointing for the past three quarters for our data analytics product suite. We have a competitive data analytics product suite in the industry. But we need to do our job selling it! \"}" "https://gateway.watsonplatform.net/tone-analyzer/api/v3/tone?version=2016-05-19"

 

 

KeyValue
Document_tone{"tone_categories":[{"tones":[{"score":0.455891,"tone_id":"anger","tone_name":"Anger"},{"score":0.156707,"tone_id":"disgust","tone_name":"Disgust"},{"score":0.17315,"tone_id":"fear","tone_name":"Fear"},{"score":0.190073,"tone_id":"joy","tone_name":"Joy"},{"score":0.291627,"tone_id":"sadness","tone_name":"Sadness"}],"category_id":"emotion_tone","category_name":"Emotion Tone"},{"tones":[{"score":0.459,"tone_id":"analytical","tone_name":"Analytical"},{"score":0.0,"tone_id":"confident","tone_name":"Confident"},{"score":0.0,"tone_id":"tentative","tone_name":"Tentative"}],"category_id":"language_tone","category_name":"Language Tone"},{"tones":[{"score":0.03,"tone_id":"openness_big5","tone_name":"Openness"},{"score":0.188,"tone_id":"conscientiousness_big5","tone_name":"Conscientiousness"},{"score":0.405,"tone_id":"extraversion_big5","tone_name":"Extraversion"},{"score":0.879,"tone_id":"agreeableness_big5","tone_name":"Agreeableness"},{"score":0.962,"tone_id":"emotional_range_big5","tone_name":"Emotional Range"}],"category_id":"social_tone","category_name":"Social Tone"}]}
Sentences_tone[{"sentence_id":0,"text":"Hi Team, I know the times are difficult!","input_from":0,"input_to":40,"tone_categories":[{"tones":[{"score":0.340698,"tone_id":"anger","tone_name":"Anger"},{"score":0.132219,"tone_id":"disgust","tone_name":"Disgust"},{"score":0.183509,"tone_id":"fear","tone_name":"Fear"},{"score":0.32062,"tone_id":"joy","tone_name":"Joy"},{"score":0.306103,"tone_id":"sadness","tone_name":"Sadness"}],"category_id":"emotion_tone","category_name":"Emotion Tone"},{"tones":[{"score":0.847,"tone_id":"analytical","tone_name":"Analytical"},{"score":0.0,"tone_id":"confident","tone_name":"Confident"},{"score":0.0,"tone_id":"tentative","tone_name":"Tentative"}],"category_id":"language_tone","category_name":"Language Tone"},{"tones":[{"score":0.035,"tone_id":"openness_big5","tone_name":"Openness"},{"score":0.354,"tone_id":"conscientiousness_big5","tone_name":"Conscientiousness"},{"score":0.787,"tone_id":"extraversion_big5","tone_name":"Extraversion"},{"score":0.585,"tone_id":"agreeableness_big5","tone_name":"Agreeableness"},{"score":0.927,"tone_id":"emotional_range_big5","tone_name":"Emotional Range"}],"category_id":"social_tone","category_name":"Social Tone"}]},{"sentence_id":1,"text":"Our sales have been disappointing for the past three quarters for our data analytics product suite.","input_from":41,"input_to":140,"tone_categories":[{"tones":[{"score":0.437543,"tone_id":"anger","tone_name":"Anger"},{"score":0.206324,"tone_id":"disgust","tone_name":"Disgust"},{"score":0.137719,"tone_id":"fear","tone_name":"Fear"},{"score":0.180326,"tone_id":"joy","tone_name":"Joy"},{"score":0.272556,"tone_id":"sadness","tone_name":"Sadness"}],"category_id":"emotion_tone","category_name":"Emotion Tone"},{"tones":[{"score":0.0,"tone_id":"analytical","tone_name":"Analytical"},{"score":0.0,"tone_id":"confident","tone_name":"Confident"},{"score":0.0,"tone_id":"tentative","tone_name":"Tentative"}],"category_id":"language_tone","category_name":"Language Tone"},{"tones":[{"score":0.164,"tone_id":"openness_big5","tone_name":"Openness"},{"score":0.372,"tone_id":"conscientiousness_big5","tone_name":"Conscientiousness"},{"score":0.399,"tone_id":"extraversion_big5","tone_name":"Extraversion"},{"score":0.759,"tone_id":"agreeableness_big5","tone_name":"Agreeableness"},{"score":0.807,"tone_id":"emotional_range_big5","tone_name":"Emotional Range"}],"category_id":"social_tone","category_name":"Social Tone"}]},{"sentence_id":2,"text":"We have a competitive data analytics product suite in the industry.","input_from":141,"input_to":208,"tone_categories":[{"tones":[{"score":0.308401,"tone_id":"anger","tone_name":"Anger"},{"score":0.298622,"tone_id":"disgust","tone_name":"Disgust"},{"score":0.254084,"tone_id":"fear","tone_name":"Fear"},{"score":0.161348,"tone_id":"joy","tone_name":"Joy"},{"score":0.224839,"tone_id":"sadness","tone_name":"Sadness"}],"category_id":"emotion_tone","category_name":"Emotion Tone"},{"tones":[{"score":0.0,"tone_id":"analytical","tone_name":"Analytical"},{"score":0.0,"tone_id":"confident","tone_name":"Confident"},{"score":0.0,"tone_id":"tentative","tone_name":"Tentative"}],"category_id":"language_tone","category_name":"Language Tone"},{"tones":[{"score":0.628,"tone_id":"openness_big5","tone_name":"Openness"},{"score":0.926,"tone_id":"conscientiousness_big5","tone_name":"Conscientiousness"},{"score":0.031,"tone_id":"extraversion_big5","tone_name":"Extraversion"},{"score":0.526,"tone_id":"agreeableness_big5","tone_name":"Agreeableness"},{"score":0.54,"tone_id":"emotional_range_big5","tone_name":"Emotional Range"}],"category_id":"social_tone","category_name":"Social Tone"}]},{"sentence_id":3,"text":"But we need to do our job selling it!","input_from":209,"input_to":246,"tone_categories":[{"tones":[{"score":0.182246,"tone_id":"anger","tone_name":"Anger"},{"score":0.101415,"tone_id":"disgust","tone_name":"Disgust"},{"score":0.26452,"tone_id":"fear","tone_name":"Fear"},{"score":0.486167,"tone_id":"joy","tone_name":"Joy"},{"score":0.217572,"tone_id":"sadness","tone_name":"Sadness"}],"category_id":"emotion_tone","category_name":"Emotion Tone"},{"tones":[{"score":0.0,"tone_id":"analytical","tone_name":"Analytical"},{"score":0.0,"tone_id":"confident","tone_name":"Confident"},{"score":0.0,"tone_id":"tentative","tone_name":"Tentative"}],"category_id":"language_tone","category_name":"Language Tone"},{"tones":[{"score":0.013,"tone_id":"openness_big5","tone_name":"Openness"},{"score":0.018,"tone_id":"conscientiousness_big5","tone_name":"Conscientiousness"},{"score":0.833,"tone_id":"extraversion_big5","tone_name":"Extraversion"},{"score":0.952,"tone_id":"agreeableness_big5","tone_name":"Agreeableness"},{"score":0.975,"tone_id":"emotional_range_big5","tone_name":"Emotional Range"}],"category_id":"social_tone","category_name":"Social Tone"}]}]

 

 

 

Adding the service to a Building Block using the Java SDK for Watson

 

Instructions can be found here: https://www.ibm.com/watson/developercloud/tone-analyzer/api/v3/?java#authentication

 

Step 1. Set the REST and Watson User/Pass in the B2 setting page.

 

Add the following into the course stripes control Action.  CourseToolActoin.java

 

@SpringBean

privateConfigurationService<Configuration> configService; //injected with Spring

    //Setup Rest URL.  Get Hostname and Protocol from the BB Config service

    String Hostname = blackboard.platform.config.ConfigurationServiceFactory.getInstance().getBbProperty("bbconfig.frontend.fullhostname");

    String protocol = blackboard.platform.config.ConfigurationServiceFactory.getInstance().getBbProperty("bbconfig.frontend.protocol");

    String VIP = protocol+"://"+Hostname+"/";

   

    //Get REST AND Watson user And Pass from B2 Config

    String USER = configService.loadConfiguration().getSettingOne();

    String PASS = configService.loadConfiguration().getSettingTwo();

    String WATUSER =configService.loadConfiguration().getWatsonUser();

    String WATPASS =  configService.loadConfiguration().getWatsonPass();

  

 

Edit the Configuration.java to add in the new settings. This file was generated by the B2 template and already contained 2 settings.  We add 2 more for thew Watson user/pass.

 

package com.ah.config;

 

import net.sourceforge.stripes.validation.Validate;

/**
 * Created by wiley on 19/11/14.
 */
public class Configuration {

    private String settingOne;
    private String settingTwo;
    private String watsonUser;
    private String watsonPass;
    
    //Rest User Name
    public String getSettingOne() {
        return settingOne;
    }

    public void setSettingOne(String settingOne) {
        this.settingOne = settingOne;
    }
    
    //Rest Pass
    public String getSettingTwo() {
        return settingTwo;
    }

    public void setSettingTwo(String settingTwo) {
        this.settingTwo = settingTwo;
    }
    
    //Watson User
    public String getWatsonUser() {
        return watsonUser;
    }

    public void setWatsonUser(String watsonUser) {
        this.watsonUser = watsonUser;
    }
    
    
    //Watson Password
    public String getWatsonPass() {
        return watsonPass;
    }

    public void setWatsonPass(String watsonPass) {
        this.watsonPass = watsonPass;
    }
    
                   
}

 

 

Using the Watson SDK.

 

Add the  following to your gradle build file to get the SDK

 

compile 'com.ibm.watson.developer_cloud:java-sdk:3.0.1'

 

You can use this service by using the following code:

 

 

ToneAnalyzer service = new ToneAnalyzer(ToneAnalyzer.VERSION_DATE_2016_05_19);
        service.setUsernameAndPassword(User, Pass);

        String text = contenttext;

        // Call the service and get the tone
        ToneAnalysis toneAnalysis = service.getTone(text, null).execute();

     

       

  

 

Edit the config.JSP to add the new Form fields.

 

 

Config.png

 

BB Manifest changes. in order for Blackboard to be able to use the Watson service the following permissions are needed.

 

 

<permission type="java.lang.RuntimePermission"name="getenv.VCAP_SERVICES"/>

<permission type="java.net.NetPermission"name="getProxySelector"/>