Skip navigation
All Places > Blackboard Developer Community > Blog
md0049252

Blackboard REST for Humans

Posted by md0049252 Sep 17, 2018

Overview:

I wrote a python library that makes consuming Blackboard REST really easy. It's very alpha, but I'm going to work on creating a three part series for the community and this is part 0.  Here's the easiest way to get started, though this does assume you have Python installed already.  Feel free to use virtual environments, I'm skipping them in this post.

 

pip install bbrest
pip install jupyterlab 
jupyter lab

 

bbrest is my library.

jupyterlab is an interactive REPL library that is mainly used for data science, but works well for our purposes.

Running 'jupyter lab' launches an interactive command line at http://localhost:8888/lab

Click the Python 3 icon to launch a Jupyter Notebook and then run the following commands:

from bbrest.bbrest import BbRest
url, key, secret = "your_url", "your_key", "your_secret"
bb = BbRest(url=url, key=key, secret=secret)

 

You now have a bb connection, which will do the following for you:

1. Get your version of Bb and only expose the REST calls you have access to.

2. Keep your session alive, automatically renewing if it expires.

3. Make some APIs way easier to use.

 

Here are some quick examples, for more, visit GitHub - mdeakyne/BbRest: Blackboard REST APIs... for humans?

#Session management
bb.expiration() #will print a human readable expiration time.
bb.calls_remaining() #will print your total calls, and call limit.

#Help with APIS
bb.User<tab> #will list calls with user
bb.GetUser(<tab>) #will list required parameters (userId=)
help(bb.GetUser) #will list documentation from Blackboard Explore APIs

#Convenience
bb.GetUser(userId='test_user') #will default to userName for users
bb.GetCourse(courseId='ENG-202') #will default to courseId for courses

bb.GetUser(userId='externalId:202020') #Can still use other specifications with : 

 

Currently, documentation is a little sparse, but you can get up and running in Python with 3 lines of code!

Here's a quick preview of the upcoming 3 part series:

 

1. REST Scripting in Python with Jupyter Lab. (Full REST setup guide)

2. REST App in Python in Flask.

3. LTI in your Flask, restricting access.

 

Hopefully, this library helps you out.  I'm happy to take feedback.

-Matt

Challenge:  Batch assigning users to Admin roles within Nodes.  When you take advantage of the Community systems Learning Context Hierarchy it can be time consuming to assign many users roles is each node.  This was a challenge for Queen Margret University and  Joe Currie talked to us at TLC in Manchester on Dev Day to see if we had a better way.

 

Solution:  I have created a very simple B2 to help with this task.   This B2 adds a new system Admin tool.

Batch Node Admin Roles 

 

 

 

 

 

 

Copy and paste Pipe separated List of User and Roles with the Node Batch UID. This currently only supports one role per user per Node. Use a carriage return for each new user record. The format is user name|Node Batch UID|role name e.g. fflintstone|68555d7b-6445-43b0-a5e1-dd794d0ce2f1|User Administrator

 

Example:

 

AH1|3875808e-9a59-4ae7-8782-be9961a40b5f|User Administrator

AH2|3875808e-9a59-4ae7-8782-be9961a40b5f|User Administrator

AH3|3875808e-9a59-4ae7-8782-be9961a40b5f|User Administrator

AH4|3875808e-9a59-4ae7-8782-be9961a40b5f|User Administrator

AH5|3875808e-9a59-4ae7-8782-be9961a40b5f|User Administrator

AH6|3875808e-9a59-4ae7-8782-be9961a40b5f|User Administrator

 

Technical Details:

 

The B2 is based from the fabulous temple B23 form the All the Ducks Team.  GitHub - AllTheDucks/atd-b2-stub: Building Block template project

 

The Node manager does all the hard work for you.  Load in the Node ID a user list and a role list.

 

NodeManagerImpl n = new NodeManagerImpl();
n.addNodeAdmins(getnodeinfo.getNodeId(), userlist, rolelist);

 

Limitations:  Only one role can be assigned via this tool at the moment per user per node.  If a user already has a role this will be removed and updated with the new role.

 

Going Forward: I am happy to share the B2 or the code with anyone who wants to test or modify this project.  There is plenty of scope for improvements and I hope this will be made redundant in future as our REST API's get more mature and feature rich.

 

Hello and Welcome!

 

It's been awhile since I did a post, and I just got back recently from DevCon18. I was asked by several peers about if I have ever tried to do an Item Analysis over multiple courses. As a matter of fact, I have! A few years ago I was tasked to see if we can do an IAQ on an English Skills test. My first response was to have the instructors just run the builtin IAQ from with in the course. The response from that was that they wanted it from all English courses from lower level and higher level course (ENC1101 - ENC1102). The test they were using was exactly the same in every course: Name of the test, questions, answers. An exact copy. This was a really good start as I got a chance to manually review the test to know: How many questions? How many answers for each question? What types of questions there are? etc...

 

Once I have mapped that out I got a really good understanding of what to do when trying to collect this data. At first, the request was to know the grades of each student for this report. Ok, done. I mapped out the db tables pulled in the grades. Then, the request got bigger. They wanted to know what the student chose! Well that is a completely different approach. the original thought was that I assumed that they would look at the final scores and calculate that based of the points of each question to get a idea of the test overall and then review it further. Nope!

 

So now here comes the SQL Query and hopefully I will be able to break down the key parts:

 


SELECT 
     CM.COURSE_ID,
     CASE
          WHEN INSTR(CM.COURSE_NAME, 'INTERNET') > 0 THEN 'Internet'
          ELSE 'Face To Face'
     END COURSE_TYPE,
     T.SOURCEDID_ID AS TERM_CODE,
     T.NAME AS SEMESTER,
     U.BATCH_UID AS STUDENT_ID,
     REGEXP_REPLACE(
          REGEXP_SUBSTR(U.STUDENT_ID, '^\([[:alnum:]]+\)')
          ,'\(|\)'
          ,''
     ) COHORT,
     U.LASTNAME,
     U.FIRSTNAME,
     GM.TITLE ASSESSMENT,
     QRD.POSITION QUESTION_POSITION,
     TRIM(QAD.TITLE) QUESTION_TITLE,
          QAD.CHOICE_0,
          QAD.CHOICE_1,
          QAD.CHOICE_2,
          QAD.CHOICE_3,
     CASE QRD.STUDENT_RESPONSE
          WHEN '0' THEN QAD.CHOICE_0
          WHEN '1' THEN QAD.CHOICE_1
          WHEN '2' THEN QAD.CHOICE_2
          WHEN '3' THEN QAD.CHOICE_3
          ELSE QRD.STUDENT_RESPONSE
     END STUDENT_RESPONSE,
     QRD.CORRECT,
     A.SCORE,
     GM.POSSIBLE POINTS_POSSIBLE,
     GM.TOOL_COMPUTED_POINTS MANUAL_GRADE_OVERRIDE,
     CASE A.STATUS
          WHEN 3 THEN 'IN_PROGRESS' 
          WHEN 4 THEN 'SUSPENDED' 
          WHEN 6 THEN 'NEEDS_GRADING'
          WHEN 7 THEN 'COMPLETED' 
          WHEN 8 THEN 'IN_MORE_PROGRESS'
          WHEN 9 THEN 'NEEDS_MORE_GRADING'
     END STATUS
FROM 
     BBLEARN.COURSE_MAIN CM,
     BBLEARN.GRADEBOOK_MAIN GM,
     BBLEARN.GRADEBOOK_GRADE GG,
     BBLEARN.COURSE_USERS CU,
     BBLEARN.USERS U,
     BBLEARN.ATTEMPT A,
     (WITH xmlResultData
          AS (SELECT 
               PARENT_PK1,
               QTI_ASI_DATA_PK1,
               PK1,
               POSITION,
               BBMD_GRADE CORRECT,
               XMLTYPE(DATA, nls_charset_id('AL32UTF8')) data
               FROM BBLEARN.QTI_RESULT_DATA)
     SELECT
          x.PARENT_PK1,
          x.QTI_ASI_DATA_PK1,
          x.PK1,
          x.POSITION,
          x.CORRECT,
          REGEXP_REPLACE(
             NVL(x.data.EXTRACT('/item_result/response/response_value/text()').getStringVal(),
             x.data.EXTRACT('/item_result/response/response_value/formatted_text/text()').getStringVal())
             ,'&lt;|p&gt;|/', '') STUDENT_RESPONSE
     FROM xmlResultData x) QRD,
     (WITH xmlData
      AS (SELECT
               qad.PK1,
               qad.PARENT_PK1,
               qad.DESCRIPTION,
               qad.POSITION,
               XMLTYPE(qad.DATA, nls_charset_id('AL32UTF8')) data
           FROM BBLEARN.QTI_ASI_DATA qad)
     SELECT
          x.PK1,
          x.PARENT_PK1,
          x.POSITION,
          x.DESCRIPTION TITLE,
          TRIM(
               REGEXP_REPLACE(
                  REGEXP_REPLACE(
                      x.data.EXTRACT('/item/presentation/flow/flow/response_lid/render_choice/flow_label[1]/response_label/flow_mat/material/mat_extension/mat_formattedtext/text()').getStringVal(),
                      '&quot;|&apos;|&lt;|p&gt;|/',
                      ''
                  ),
               '[' || CHR(10) || CHR(13) || ']',
               ' '
            )
          ) CHOICE_0,

          TRIM(REGEXP_REPLACE(
               REGEXP_REPLACE(
                   x.data.EXTRACT('/item/presentation/flow/flow/response_lid/render_choice/flow_label[2]/response_label/flow_mat/material/mat_extension/mat_formattedtext/text()').getStringVal()
                   ,'&quot;|&apos;|&lt;|p&gt;|/'
                   , ''
               )
          ,'[' || CHR(10) || CHR(13) || ']',' ')) CHOICE_1,

          TRIM(REGEXP_REPLACE(
               REGEXP_REPLACE(
                   x.data.EXTRACT('/item/presentation/flow/flow/response_lid/render_choice/flow_label[3]/response_label/flow_mat/material/mat_extension/mat_formattedtext/text()').getStringVal()
                   ,'&quot;|&apos;|&lt;|p&gt;|/'
                   , ''
               )
          ,'[' || CHR(10) || CHR(13) || ']',' ')) CHOICE_2,

          TRIM(REGEXP_REPLACE(
               REGEXP_REPLACE(
                   x.data.EXTRACT('/item/presentation/flow/flow/response_lid/render_choice/flow_label[4]/response_label/flow_mat/material/mat_extension/mat_formattedtext/text()').getStringVal()
                   ,'&quot;|&apos;|&lt;|p&gt;|/'
                   , ''
               )
          ,'[' || CHR(10) || CHR(13) || ']',' ')) CHOICE_3
FROM xmlData x) QAD,
     BBLEARN.COURSE_TERM CT,
     BBLEARN.TERM T
WHERE CM.PK1 = GM.CRSMAIN_PK1
     AND GM.PK1 = GG.GRADEBOOK_MAIN_PK1
     AND CU.PK1 = GG.COURSE_USERS_PK1
     AND U.PK1 = CU.USERS_PK1
     AND A.QTI_RESULT_DATA_PK1 + 1 = QRD.PARENT_PK1
     AND QAD.PK1 = QRD.QTI_ASI_DATA_PK1
     AND CM.PK1 = CT.CRSMAIN_PK1
     AND T.PK1 = CT.TERM_PK1
     AND GG.PK1 = A.GRADEBOOK_GRADE_PK1
     AND CU.ROLE = 'S'
     AND REGEXP_LIKE(GM.title, '^M\w+\s+English Skills Test\s+#[1|2|3].*$')
     AND GM.title NOT LIKE '%Final%'
     AND T.SOURCEDID_ID >= 20163
     AND REGEXP_LIKE(CM.COURSE_ID, '^ENC110(1|2)-\d{6}$')
     AND ROWNUM<=100
ORDER BY CM.COURSE_ID, U.BATCH_UID, GM.TITLE, QRD.POSITION ASC;




 

 

 

 

 

WHAT THE?????

Seriously, that took awhile to compose. So, a quick little story. So after I started poking around the database tables I came across the QTI_ASI_DATA table. This is the table that houses the assessments. Yep, old Angel users, this is the same type of table but slightly different. So after following the foreign key trails I ended up at the QTI_RESULT_DATA. I came across this DATA field with a CLOB. I first ignored it. I poke around more but could not find the responses from each question.....So I started reaching about the CLOB data type and turns out if you extract that data, you get an xml snapshot of the test response from a particular attempt! So with some good ole hacker'y I managed to extract the data out!

Breakdown, from the top

Getting the Choices

 


CASE QRD.STUDENT_RESPONSE
     WHEN '0' THEN QAD.CHOICE_0
     WHEN '1' THEN QAD.CHOICE_1
     WHEN '2' THEN QAD.CHOICE_2
     WHEN '3' THEN QAD.CHOICE_3
     ELSE QRD.STUDENT_RESPONSE
END STUDENT_RESPONSE,


 

This above snippet show how to provide the multiple choice 'choices'. This helps knowing the max amount of possible choices in the test. I am pulling from my aliased QRD (QTI_RESULT_DATA) table then mapping it to the QAD aliased table. This allowed me to build the test back into my result data set!

But how to get the data out of the CLOB?

 


     (WITH xmlData
      AS (SELECT
               qad.PK1,
               qad.PARENT_PK1,
               qad.DESCRIPTION,
               qad.POSITION,
               XMLTYPE(qad.DATA, nls_charset_id('AL32UTF8')) data
           FROM BBLEARN.QTI_ASI_DATA qad)
     SELECT
          x.PK1,
          x.PARENT_PK1,
          x.POSITION,
          x.DESCRIPTION TITLE,
          TRIM(
               REGEXP_REPLACE(
                  REGEXP_REPLACE(
                      x.data.EXTRACT('/item/presentation/flow/flow/response_lid/render_choice/flow_label[1]/response_label/flow_mat/material/mat_extension/mat_formattedtext/text()').getStringVal(),
                      '&quot;|&apos;|&lt;|p&gt;|/',
                      ''
                  ),
               '[' || CHR(10) || CHR(13) || ']',
               ' '
            )
          ) CHOICE_0,

          TRIM(REGEXP_REPLACE(
               REGEXP_REPLACE(
                   x.data.EXTRACT('/item/presentation/flow/flow/response_lid/render_choice/flow_label[2]/response_label/flow_mat/material/mat_extension/mat_formattedtext/text()').getStringVal()
                   ,'&quot;|&apos;|&lt;|p&gt;|/'
                   , ''
               )
          ,'[' || CHR(10) || CHR(13) || ']',' ')) CHOICE_1,

          TRIM(REGEXP_REPLACE(
               REGEXP_REPLACE(
                   x.data.EXTRACT('/item/presentation/flow/flow/response_lid/render_choice/flow_label[3]/response_label/flow_mat/material/mat_extension/mat_formattedtext/text()').getStringVal()
                   ,'&quot;|&apos;|&lt;|p&gt;|/'
                   , ''
               )
          ,'[' || CHR(10) || CHR(13) || ']',' ')) CHOICE_2,

          TRIM(REGEXP_REPLACE(
               REGEXP_REPLACE(
                   x.data.EXTRACT('/item/presentation/flow/flow/response_lid/render_choice/flow_label[4]/response_label/flow_mat/material/mat_extension/mat_formattedtext/text()').getStringVal()
                   ,'&quot;|&apos;|&lt;|p&gt;|/'
                   , ''
               )
          ,'[' || CHR(10) || CHR(13) || ']',' ')) CHOICE_3
FROM xmlData x) QAD,




 

So this is done the cast a selected data set as xml, then telling it to extract the data via an xpath. I have some convenience trim and replace functions to help clean up the extract data as it is xml with html entities. This helped when exporting this data set to an csv file to be imported into Excel.

 

The actual part is here:

 

     (WITH xmlData
      AS (SELECT
               qad.PK1,
               qad.PARENT_PK1,
               qad.DESCRIPTION,
               qad.POSITION,
               XMLTYPE(qad.DATA, nls_charset_id('AL32UTF8')) data
           FROM BBLEARN.QTI_ASI_DATA qad)

 

The WITH <alias> AS SELECT statement is what does the casting of the DATA field by the use of XMLTYPE. The major key note here is the charset! This took me awhile to find but this is what helps in extracting the data out.

Now onto the fancy stuff

 


     (WITH xmlResultData
          AS (SELECT 
               PARENT_PK1,
               QTI_ASI_DATA_PK1,
               PK1,
               POSITION,
               BBMD_GRADE CORRECT,
               XMLTYPE(DATA, nls_charset_id('AL32UTF8')) data
               FROM BBLEARN.QTI_RESULT_DATA)
     SELECT
          x.PARENT_PK1,
          x.QTI_ASI_DATA_PK1,
          x.PK1,
          x.POSITION,
          x.CORRECT,
          REGEXP_REPLACE(
             NVL(x.data.EXTRACT('/item_result/response/response_value/text()').getStringVal(),
             x.data.EXTRACT('/item_result/response/response_value/formatted_text/text()').getStringVal())
             ,'&lt;|p&gt;|/', '') STUDENT_RESPONSE
     FROM xmlResultData x) QRD,



 

Same as with QAD, we need to do the same for QRD.

 

x.data.EXTRACT('/item/presentation/flow/flow/response_lid/render_choice/flow_label[1]/response_label/flow_mat/material/mat_extension/mat_formattedtext/text()').getStringValue(),



 

The extract method is what allows you to use the xpath to drill down to the data you what to show. You can get more information here: IMS Question & Test Interoperability v1.2 QTILite Specification | IMS Global Learning Consortium on the QTI IMS1.x Specification.

 

Getting the data across multiple courses.

 



WHERE CM.PK1 = GM.CRSMAIN_PK1
     AND GM.PK1 = GG.GRADEBOOK_MAIN_PK1
     AND CU.PK1 = GG.COURSE_USERS_PK1
     AND U.PK1 = CU.USERS_PK1
     AND A.QTI_RESULT_DATA_PK1 + 1 = QRD.PARENT_PK1
     AND QAD.PK1 = QRD.QTI_ASI_DATA_PK1
     AND CM.PK1 = CT.CRSMAIN_PK1
     AND T.PK1 = CT.TERM_PK1
     AND GG.PK1 = A.GRADEBOOK_GRADE_PK1
     AND CU.ROLE = 'S'
     AND REGEXP_LIKE(GM.title, '^M\w+\s+English Skills Test\s+#[1|2|3].*$')
     AND GM.title NOT LIKE '%Final%'
     AND T.SOURCEDID_ID >= 20163
     AND REGEXP_LIKE(CM.COURSE_ID, '^ENC110(1|2)-\d{6}$')
     AND ROWNUM<=100
ORDER BY CM.COURSE_ID, U.BATCH_UID, GM.TITLE, QRD.POSITION ASC;




 

Now, usually I always tend to do LEFT JOINS, they tend to be more accurate and time efficient. But, the data being pull has some oddities in such that; not all choice would the same amount of choices. I believe this example I lucked out and they all ended up with the same amount. Obviously, you will need to modify the GM.TITLE, TERM (if applicable), and the COURSE_ID to match  your specific need. Note: REMOVE ROWNUM , I left this in this example as when you are running tests, you want to make sure that you start off with minimal results then increase when you are more confident in your results. Once completed, remove the rownum to get all.

 

Conclusion

I would like to thank you for sticking through this blog post and that if you have any questions, feel free to contact me via community!

 

Thanks,

 

Mike

Hello,

Who does not feel frustrated using the "Volume Exclusion" tool and does not have the Learn buttons also deleted? Or by using a course as a base, replicating this content for numerous other courses using the "Copy Course" tool. These in turn, also depending on the deletion of buttons before the replica.

Having this and other scenarios I share with you the post (text in Portuguese).

Screen Shot 2018-03-25 at 5.53.04 PM.pngIn an upcoming hackathon at Grand Valley State University students will be asked to innovate and code for Blackboard Learn. In addition to REST API category, a basic HTML category of the competition will use Community Engagement modules.  A new app was created to encourage development of the modules without the need to grant access to a Blackboard Learn system.

While there are a few ways to run the Blackboard Learn system for free or almost without cost, the new wizard makes the development and debugging easy.

This is the app:

Community System: HTML Module Wizard


I've been working in a proof of concept application to use LDAP to authenticate users in Blackboard Collaborate using the simplest possible approach. Now I have something working and I want to share it with the community.

 

First of all I'm not a PHP programmer, so the code you'll see needs a lot of improvement and is not production ready, but I think is enough in order to understand the concept. I've choosen PHP because of the simple LDAP interaction that it has out of the box.

 

The concept is that you can create a login page that binds the user with the LDAP server, and if the binding is successful then it creates a LTI request with the user data in a unique context and redirect the user to the management interface.

 

Here you can watch a demo and code walkthrough, (sorry, it is in Spanish, I'll upload an English version as soon as I have enough time) : Demo LDAP-Collaborate - eLearning Media

 

The code used in the video is available in Github: GitHub - eLearningMedia/LDAP-Collaborate-login: Simple application to manage Blackboard Collaborate sessions and recordi…

I had a support case recently to check the Custom Parameters being send from Learn via LTI.  My normal go to is http://ltiapps.net as this works great for testing LTI both as a consumer and provider.

 

In my case I wanted to test using SSL and have a go with NodeJS.

 

Borrowing some of the code from the Signup List B2-to-REST Migration it was simple to create my first LTI tool with NodeJS just to return the values passed in the LTI Body. The project is attached here.

 

 

app.js

 

/*jshint sub:true*/
var https = require('http');
var fs = require('fs');
var express = require('express');
var app = express();
var lti = require('ims-lti');
var _ = require('lodash');
var bodyParser  = require('body-parser');
//I got the key I got the secrete #Urban Cookie Collective

var ltiKey = "mykeyagain";
var ltiSecret = "mysagain";

app.engine('pug', require('pug').__express);

app.use(express.bodyParser());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

app.set('view engine', 'pug');

//Setup a POST endpoint to take anything going to /launch_lti
app.post('/launch_lti', function(req, res, next){

  req.body = _.omit(req.body, '__proto__');
      if (req.body['oauth_consumer_key']===ltiKey){
          var provider = new lti.Provider(ltiKey, ltiSecret);
         //Check is the Oauth  is valid using the LTI plugin for NodeJS.
              provider.valid_request(req, function (err, isValid){
                  if (err) {
                  console.log('Error in LTI Launch:' + err);
                  res.status(403).send(err);
                  
                  }
                  else {
                  if (!isValid) {
                    console.log('\nError: Invalid LTI launch.');
                    res.status(500).send({ error: "Invalid LTI launch" });
                     } 
                  else {
                      //User is Auth so pass back when ever we need. in this case we use pug to render the values to screen
                      res.render('start', { title: 'LTI SETTINGS', CourseID: 'CourseID: '+req.body['context_id'], userID: 'UserID: '+req.body['user_id'], UserRole: 'Course Role: '+req.body['roles'], FulllogTitle: 'Full Log: ', Fulllog: JSON.stringify(req.body) });
            }}
       });
    }
  else {
      console.log('LTI KEY NOT MATCHED:');
      res.status(403).send({ error: "LTI KEY NOT MATCHED" });      
  }

});

//Setup the http server, When delyed locally this will run on port 5000, when deployed on Heroku it will assign a port and add SSL
var server = https.createServer(app).listen(process.env.PORT || 5000, function(){
  console.log("https server started");
});

 

 

I then deployed this to heroku so It would add SSL and is available for anyone to test.

 

 

Tool URL: https://porttestsupport.herokuapp.com/launch_lti

Key: mykeyagain

Secrete: mysagain

 

Add this to Learn as a course tool and when clicked it will give you the full Parameter list sent to the LTI tool.  This is the output from a Test Student.

 

I want to share my experience creating a "blank page" content item in the course TOC programatically.

 

My motivation was to add the Collaborate recordings download links from a course to a content in order to keep them available after restoring the course in other instance of Learn after archiving, because the pk1 and the Collaborate plugin tables are not kept.

 

The core of the solution is this code:

 

  CourseToc newToc = new CourseToc();

  newToc.setTargetType(Target.CONTENT_ITEM);
  newToc.setCourseId(course.getId());
  newToc.setLabel("Recordings");
  newToc.setLaunchInNewWindow(false);
  newToc.setIsEnabled(true);
  newToc.setAllowGuests(false);

  Content content = new Content();

  content.setTitle("Recordings");
  content.setCourseId(course.getId());
  content.setIsAvailable(true);
  content.setAllowGuests(false);
  content.setIsFolder(false);
  content.setLaunchInNewWindow(false);
  content.setContentHandler("resource/x-bb-blankpage");
  content.setBody(FormattedText.toFormattedText(contentString));

  ContentDbPersister.Default.getInstance().persist(content);

  newToc.setContentId(content.getId());

  CourseTocDbPersister.Default.getInstance().persist(newToc );

  CourseMapManagerFactory.getInstance().invalidateCache(course.getId());

 

The contentString variable contains the HTML to show in the blank page content.

 

Hope this will help somebody who want to create any kind of content programatically, not just my particular user case.

Hello everyone! I made a comment in a post a while back about using JS Hacks to override some CSS rules that the new 2016 Theme enforces. I have a few people ask me about this and I though this would be a good opportunity to write up a small post about it.

 

 

Why won't my custom theme stick!

If you are using the new 2016 theme, you may have notice that some things are very opinionated and you want to change some things. So you download the theme and start hacking away at the CSS files. You finally re-upload your theme and....the rules are not sticking!!! What gives?!?!

 

How do I overcome this issue?

Well what is happening is that there is a them1.css file that is being injection during the page render time. Since the last rule set wins when it comes to css, your beautiful css rules are now squashed by Blackboard! Bummer!

 

Conquering the CSS Takeover!

Well, there is another way! You can make all the css rules that are being squashed, into another css file. Then you can place either as an attachment via JS Hacks, or what I have done is place this file in the Content Collection and made it publicly available. Now I can copy this URI path and use it in my diabolical plan to take over the world! Blackboard!

 

 

Here is the JS Hack:

 

 

<!--

    your.blackboard.com = your instance of Blackboard Learn

    CC_FOLDER = is just a folder created in the Content Collection

    your_theme1 = the id you wish you give your element

 -->

<script type="text/javascript">

// https://your.blackboard.com/bbcswebdav/institution/CC_FOLDER/theme/theme1.css

    Event.observe(document,"dom:loaded", function() {

        //Your javascript goes here.

        // console.log($$('head')[0])

        $$('head')[0].insert({

            bottom: new Element('link', {

                href: '/bbcswebdav/institution/CC_FOLDER/theme/theme1.css',

                type: 'text/css',

                rel: 'stylesheet',

                id: 'your_theme1'

            })

        });

    });

</script>


Hello, I recently just had to run a report via Open DB and supply some Learn Information to a vendor that will be creating a PoC. I was tasked on getting this information but the information of course would violate FERPA. So I had to come up with a way to mask the user information to still supply an accurate model but protect the user information at the same time. So I decided to use an old python script that converted CSV files to XLSX and make some modifications.


 

I used openpyxl to create the Excel XLSX files, Docopt to make the CLI creation easy, and some built in core modules: csv, json, and random.


 

The main challenge was that I had two reports from Open Db that correlated between Activity and Gradebook Data by the USER_ID. I ended up doing some Python wizardry and use a global (gasp!! what globals in Python, your fired!) Anyways, yes the use of a global in Python is frowned upon, but hey it got the job done! That’s what re-factoring is for...right?


 

Anyways, I thought I would share this application and share my work and hopefully see how useful this can be. I plan on implementing a way to connect directly to Open Db (Already have it done in another project) and then add in some common functionality: Mongo DB Storage, Better Configuration Methods, etc.


 

You are all welcome to use, share, clone, hack, and destroy this code here:

https://github.com/elmiguel/CSV2XLSX


 

You will just need to query and save your own data sets… Luckily for you, I will share my queries:

 

Activity Accumulator:


 

SELECT AA.PK1
,AA.EVENT_TYPE
,U.USER_ID
,CM.BATCH_UID
,AA.GROUP_PK1
,AA.FORUM_PK1
,AA.INTERNAL_HANDLE
,AA.CONTENT_PK1
,AA.DATA
,AA.TIMESTAMP
,AA.STATUS
,AA.MESSAGES
,AA.SESSION_ID
FROM BBLEARN.ACTIVITY_ACCUMULATOR AA
INNER JOIN BBLEARN.USERS U
ON AA.USER_PK1 = U.PK1
INNER JOIN BBLEARN.COURSE_MAIN CM
ON AA.COURSE_PK1 = CM.PK1
WHERE CM.COURSE_ID IN (‘LIST COURSE_IDS HERE’
)
ORDER BY AA.TIMESTAMP DESC;







 

 

 
  Gradebook Data:


 

SELECT
CM.COURSE_ID,
U.USER_ID,
U.FIRSTNAME,
U.LASTNAME,
GM.TITLE,
GM.DUE_DATE,
A.SCORE,
CASE A.STATUS
WHEN 1 THEN 'NOT_ATTEMPTED'  
WHEN 2 THEN 'ABANDONED'
WHEN 3 THEN 'IN_PROGRESS'  
WHEN 4 THEN 'SUSPENDED'
WHEN 5 THEN 'CANCELLED '
WHEN 6 THEN 'NEEDS_GRADING'  
WHEN 7 THEN 'COMPLETED'
WHEN 8 THEN 'IN_MORE_PROGRESS'  
WHEN 9 THEN 'NEEDS_MORE_GRADING'
ELSE 'NO STATUS'
END AS STATUS,
A.ATTEMPT_DATE,
A.FIRST_GRADED_DATE,
A.LAST_GRADED_DATE,
A.DATE_ADDED,
A.DATE_MODIFIED,
A.LATEST_IND
-- A.STUDENT_SUBMISSION
-- A.PK1 ATTEMPT_PK1,
-- A.QTI_RESULT_DATA_PK1,
-- GG.GRADEBOOK_MAIN_PK1,
-- GG.PK1 GG_PK1
FROM GRADEBOOK_MAIN GM  
INNER JOIN GRADEBOOK_GRADE GG
ON GG.GRADEBOOK_MAIN_PK1 = GM.PK1
INNER JOIN ATTEMPT A
ON A.GRADEBOOK_GRADE_PK1 = GG.PK1
INNER JOIN COURSE_USERS CU
ON GG.COURSE_USERS_PK1 = CU.PK1
INNER JOIN COURSE_MAIN CM
ON CU.CRSMAIN_PK1 = CM.PK1
INNER JOIN USERS U
ON CU.USERS_PK1 = U.PK1
WHERE CM.COURSE_ID IN (‘LIST COURSE_IDS HERE’);







mkauffman

ALL FILES No More

Posted by mkauffman Apr 18, 2017

I'm writing this to announce an upcoming requirement for all Building Blocks (B2s). Currently Blackboard allows the following in a B2's bb-manifest.xml file.

<permission type="java.io.FilePermission" name="&amp;lt;&amp;lt;ALL FILES&amp;gt;&amp;gt;" actions="read,write,delete"/>

The above allows the B2 to write to anywhere on the host file system.

 

Because of the security implications,  Blackboard is asking all B2 developers to remove that permission from bb-manifest.xml by Q4 2017. We'll be communicating this out here on our Community site, via our Partner Newsletter, in an Announcement on Behind the Blackboard, etc.

 

Below is a set of permissions that opens up everything in the Blackboard directories. These are almost as 'bad' as <<ALL FILES>> in that they let you overwrite other Blackboard files and content, for example anything in the vi directory, but are far better than allowing changes to any file in the file system. /- indicates every file and directory beneath the specified folder. Once you get your Building Block functioning with the following, then we recommend reducing the set of permissions to only those directories/files that it needs to access, and only those actions that are necessary.

 

<permission type="java.io.FilePermission" name="${java.home}/-" actions="read"/>

<permission type="java.io.FilePermission" name="BB_HOME/-" actions="read,write,delete"/>

<permission type="java.io.FilePermission" name="BB_HOME/apps/tomcat/temp/-"  actions="read,write,delete" />

<permission type="java.io.FilePermission" name="BB_CONTENT/-" actions="read,write,delete"/>

 

Here is an option for logging only that is less promiscuous, if your B2 doesn't need to write elsewhere in the BB directories.

<permission type="java.io.FilePermission" name="BB_HOME/logs/-" actions="read,write,delete"/>

 

Following is a link to sample code that uses logback to write to blackboard/logs/custom and blackboard/logs/plugins/<vendor_id>-<handle>/

 

blackboard/logs/plugins/<vendor_id>-<handle> is the directory that B2s should be writing to from here on out. It's the only B2-specific directory that makes the B2 specific logs available to the Kibana log visualizer in SaaS.  GitHub - mark-b-kauffman/bbdn-bblogbackb2: Demo the use of Logback to create log files.

 

Please plan to address this soon so that your Building Block will be able to be installed in Q4 2017 and later.

Now we have a working Building Block lets add some more information to the course tool as a form.

 

CourseToolConfig.java

 

//Create a LIST filled with Group Objects using the Course ID.

 

  private static List<Group> GroupObjects(String course_id) throws PersistenceException{
        GroupDbLoader groupDbLoader = GroupDbLoader.Default.getInstance();
        List<Group> groups = groupDbLoader.loadAvailableByCourseId((Id.generateId(new DataType(blackboard.data.course.Course.class), course_id)));

        List<Group> G1 = new LinkedList<>();

            for (Group group : groups){
                
                //Get group titles. Add to Linked list g1.
                G1.add(group);
                System.out.println("Group Size"+group.getGroupMemberships().size());
    
                
            }
            
            return G1;
        
    }

 

 

Now we need to add some Getter's and Setter's for the List we will pass to the JSP page.

 

 

private List<Group> groupTitles; 
public List<Group> getGroupTitles() {return groupTitles; }
public void setGroupTitles(List<Group> groupTitles) { this.groupTitles = groupTitles; }

 

 

In our Default Handler   @DefaultHandler   public Resolution displayPage() we can populate this list

 

groupTitles = GroupObjects(course_id);

 

To display this in the JSP we use the actionBean.groupTitles List and then can call out the getTitle and getGroupMemberships().size() from the Group Object in the List

 

<bbNG:form action="coursetool" method="post">

        <bbNG:dataCollection>

            <bbNG:step title="Groups">

         

                        <bbNG:inventoryList description="Group Names"  className="Group" collection="${actionBean.groupTitles}" objectVar="title" emptyMsg="No data to display">

                        <bbNG:listElement  name="Group" label="Group" isRowHeader="true">${title.getTitle()}</bbNG:listElement>

                        <bbNG:listElement  name="Group Enrollments" label="Group Enrollments"  isRowHeader="false">${title.getGroupMemberships().size()}</bbNG:listElement>

                        <bbNG:listCheckboxElement  title="Title" name="${title.getTitle()}" id="ID" value="Y"></bbNG:listCheckboxElement>

                </bbNG:inventoryList>

 

                        <input type="hidden" name="course_id" value="${actionBean.course_id}" />

                     

                     

                 

            <bbNG:stepInstructions text="    ${message}  <BR>

               Course Title =  ${actionBean.course_title} <BR>

               Course Pk1 =  ${actionBean.course_id}  <BR>

               User Name  = ${actionBean.username}

               "/>

            </bbNG:step>

        <bbNG:stepSubmit  cancelUrl="Cancle" />

        </bbNG:dataCollection>

    </bbNG:form>

In part one we build a basic course tool.  In this part we will use the config utility to set dome value for our B2.  These can be used for things like Global user/pass.

 

The B2 stub uses ATD's Configuration Utilities Library to provide configuration services to the building block. This building block handles the saving, loading and caching of a configuration bean.

 

 

The Configuration.java contain the list of Getters and Setters for this page. More can be added of modified.

 

package com.devday.config;

import net.sourceforge.stripes.validation.Validate;

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

    private String settingOne;
    private int settingTwo;

    public String getSettingOne() {
        return settingOne;
    }

    public void setSettingOne(String settingOne) {
        this.settingOne = settingOne;
    }

    public int getSettingTwo() {
        return settingTwo;
    }

    public void setSettingTwo(int settingTwo) {
        this.settingTwo = settingTwo;
    }
}

 

 

Stripes config.action

 

@LoginRequired
@EntitlementRestrictions(entitlements = "bbs.yourb2name.admin.MODIFY", errorPage = "/error.jsp")
@UrlBinding("/config")
public class ConfigAction implements ActionBean {

    private BlackboardActionBeanContext context;

    @ValidateNestedProperties({@Validate(field = "settingOne", required = true),
            @Validate(field = "settingTwo", required = true, minvalue = 0, maxvalue = 100)})
    private Configuration config;

    @SpringBean
    private ConfigurationService<Configuration> configService;

    @Before(stages = LifecycleStage.BindingAndValidation)
    public void loadConfiguration() {
        config = configService.loadConfiguration();
    }


    @DefaultHandler
    @DontValidate
    public Resolution displayConfigPage() {
        return new ForwardResolution("/WEB-INF/jsp/config.jsp");
    }

    public Resolution saveConfiguration() {
        configService.persistConfiguration(config);
        return new RedirectResolution(PlugInUtil.getPlugInManagerURL(), false);
    }

    @Override
    public ActionBeanContext getContext() {
        return context;
    }

    @Override
    public void setContext(ActionBeanContext context) {
        this.context = (BlackboardActionBeanContext)context;
    }

    public Configuration getConfig() {
        return config;
    }

    public void setConfig(Configuration config) {
        this.config = config;
    }
}

 

 

Note the Form Validation, if you make a change to the methods for example making the int a String you will need to update this validation to match the type.

 

@ValidateNestedProperties({@Validate(field = "settingOne", required = true),     @Validate(field = "settingTwo", required = true, minvalue = 0, maxvalue = 100)})

 

Default Values:

 

If you want to pre-populate these configs with default values then edit the defultconfig.xml under src/main/resources

 

<com.devday.config.Configuration>
  <settingOne>A default string value</settingOne>
  <settingTwo>42</settingTwo>
</com.devday.config.Configuration>

 

Config JSP.

 

This uses a simple Form to collect the data and pass via the Stripes Action

 

 

 

<stripes:form beanclass="com.devday.stripes.ConfigAction">

        <stripes:param name="saveConfiguration"/>

 

        <bbNG:dataCollection>

            <bbNG:step title="${toolSettingsStepTitle}" instructions="${toolSettingsStepInstructions}">

                <bbNG:dataElement isRequired="true" label="${settingOneLabel}">

                    <stripes:text name="config.settingOne"></stripes:text>

                    <stripes:errors field="config.settingOne"></stripes:errors>

                </bbNG:dataElement>

                <bbNG:dataElement isRequired="true" label="${settingTwoLabel}">

                    <stripes:text name="config.settingTwo"></stripes:text>

                    <stripes:errors field="config.settingTwo"></stripes:errors>

                </bbNG:dataElement>

            </bbNG:step>

            <bbNG:stepSubmit></bbNG:stepSubmit>

        </bbNG:dataCollection>

    </stripes:form>

For those of you joining the DevCon in Milan we will be running a Get you started session for Building Blocks using the fantastic Building Block template from the 'All the Ducks' Team.  This uses Stripes as its framework and Gradle to Build the project.

 

This will be a Multi-part series of Blog posts designed to get your up and running with your first B2.

 

If you would like to join the session then it will be helpful to run though this guide before hand and install Eclipse and Git Bash.

 

We will cover in the session:

 

  1. Getting you started with Eclipse
  2. Language pack Bundles
  3. API calls
  4. Data Handling

 

 

Install Eclipse:

 

Download and install Eclipse from Eclipse - The Eclipse Foundation open source community website.

 

Download Git bash for Windows Git - Downloading Package

 

Getting Started.

 

Launch git bash and CD to your Work space. I am using

 

ahulme@AHULMEP50 MINGW64 ~

$ cd workspace/devday/

 

Clone the template to your local Workspace.  Note the First part of the directory name shroud be you Vendor ID. In my case I am using bbs  (Use lowercase chars) and then the name of the B2.  This example uses bbs-yourb2name

 

 

ahulme@AHULMEP50 MINGW64 ~/workspace/devday

$ git clone https://github.com/AllTheDucks/atd-b2-stub.git bbs-yourb2name

Cloning into 'bbs-yourb2name'...

remote: Counting objects: 712, done.

remote: Total 712 (delta 0), reused 0 (delta 0), pack-reused 712 eceiving objects:  99% (705/712)

Receiving objects: 100% (712/712), 206.33 KiB | 0 bytes/s, done.

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

Checking connectivity... done.

 

Change into the project directory

 

ahulme@AHULMEP50 MINGW64 ~/workspace/devday

$ cd bbs-yourb2name/

 

Run the initB2 task and answer all the questions.  Make sure you set the Vendor ID to match the one used above and the B2 handle as the same name as above.

 

ahulme@AHULMEP50 MINGW64 ~/workspace/devday/bbs-yourb2name (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)  yourb2name

What is your B2 Name (e.g. Student View)? Dev Day2016         *Note I created this in January and forgot we moved on a year, its just to cold in Jan to think....

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

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

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

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

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

Initializing Building Block bbs-yourb2name

Initialized empty Git repository in C:/Users/ahulme/workspace/devday/bbs-yourb2name/.git/

 

BUILD SUCCESSFUL

 

Total time: 1 mins 59.188 secs

 

Import the Project into Eclipse.

 

File > Import > Gradle Project

 

Browse to the Directory your created and Select Build model.  Check the Project and finish.  This has now imported this into your work-space.

 

 

 

This is the project as it looks in Eclipse.    There are 3 stripes action classes in src > com.devday.stripes .  These control the actions for the Course Tool, System Tool and Config tool.  The JSP files for these tools can be found in src > main > webapp > WEB-INF > jsp.  The language bundles are in  src > main > webapp > WEB-INF > bundles

 

 

Build the B2.

 

Without making any changes we can Build this B2 and Deploy it to Blackboard.  This will create a course tool, a system tool, Some Database Schema objects and a Config tool for the B2.

 

Right click gradlew > run as > Gradle STS Build

Set the task to build > apply and then run

 

Console Output:

 

[sts] -----------------------------------------------------

[sts] Starting Gradle build for the following tasks:

[sts]      build

[sts] -----------------------------------------------------

:compileJavawarning: [options] bootstrap class path not set in conjunction with -source 1.7

1 warning

 

:processResources

:classes

:war

:assemble

:compileTestJava UP-TO-DATE

:processTestResources UP-TO-DATE

:testClasses UP-TO-DATE

:test UP-TO-DATE

:check UP-TO-DATE

:build

 

BUILD SUCCESSFUL

 

Total time: 1.584 secs

[sts] -----------------------------------------------------

[sts] Build finished succesfully!

[sts] Time taken: 0 min, 1 sec

[sts] -----------------------------------------------------

 

Deploy To Blackboard.

 

System admin GUI > Building Blocks > installed tools > Upload Building block

 

If you see this error in Red you will need to enable B2's to add custom objects. Building Blocks > Global Settings

 

Error: Could not install plugin.name. There are associated database changes and the global setting has been set to prevent any Building Block from creating custom database objects.

 

 

 

The war file can be found in the build\libs\ directory of the project space.

 

There will be a new tool now in the System admin panel highlighted below.

 

 

Click on this new link and you will see the tool.  As we have not configured this tool to do anything as yet a simple message will show to confirm it is working.   Note the URL used for this tool is https://Yoururl.com/webapps/bbs-yourb2name-BBLEARN/systemtool

 

 

The course tool is set by default to be unavailable courses so you can either set this globally or set on a per course bases for testing.  In a course  Customization > Tool Availability  > find the tool called Dev Day2016 and enable it.  You can then find this tool under tools and select it.

 

 

 

 

The B2 also has a config page for setting Global Configurations.  This can be access though the settings page and has buy default a String and an Integer value. We can change these and add more if required.

 

 

 

 

 

Course Tool Action.

 

This controls the Behavior when the course tool link is selected.  The URL https://finn.pd.local/webapps/bbs-yourb2name-BBLEARN/systemtool is mapped to this CourseToolActoin.java and the default action is to rerun the JSP /WEB-INF/jsp/course-tool.jsp

 

package com.devday.stripes;

import com.alltheducks.bb.stripes.BlackboardActionBeanContext;
import com.alltheducks.bb.stripes.EntitlementRestrictions;
import com.alltheducks.bb.stripes.LoginRequired;
import net.sourceforge.stripes.action.*;
import net.sourceforge.stripes.integration.spring.SpringBean;

@LoginRequired
@EntitlementRestrictions(entitlements = "bbs.yourb2name.course.VIEW", errorPage = "/error.jsp")
@UrlBinding("/coursetool")
public class CourseToolAction implements ActionBean {

    private BlackboardActionBeanContext context;

    @DefaultHandler
    public Resolution displayPage() {
        return new ForwardResolution("/WEB-INF/jsp/course-tool.jsp");
    }

    @Override
    public ActionBeanContext getContext() {
        return context;
    }

    @Override
    public void setContext(ActionBeanContext context) {
        this.context = (BlackboardActionBeanContext)context;
    }
}

 

 

A simple course Tool Project:

 

lets use our basic course tool to display some information about the course.  The FULL URL called for the tool link is https://finn.pd.local/webapps/bbs-yourb2name-BBLEARN/coursetool?course_id=_36_1  and this contain the course Pk1.  We can use this to get the course Pk1 and use the context to get the Course Title

 

Add the following to the CourseToolActoin.java on line 32 to setup our Getters and Setters.

 

private String course_title;

public String getCourse_title() {return course_title; }

public void setCourse_title(String course_title) { this.course_title = course_title; }

   

private String course_id;

public String getCourse_id() {return course_id; }

public void setCourse_id(String course_id) { this.course_id = course_id; }

 

The course Pk1 will be automatically mapped to the action bean course_id as this is provided in the URL.  We can set the Course Title using the context Stripes Action Bean.

 

package com.devday.stripes;

import com.alltheducks.bb.stripes.BlackboardActionBeanContext;
import com.alltheducks.bb.stripes.EntitlementRestrictions;
import com.alltheducks.bb.stripes.LoginRequired;
import net.sourceforge.stripes.action.*;
import net.sourceforge.stripes.integration.spring.SpringBean;

@LoginRequired
@EntitlementRestrictions(entitlements = "bbs.yourb2name.course.VIEW", errorPage = "/error.jsp")
@UrlBinding("/coursetool")
public class CourseToolAction implements ActionBean {

    private BlackboardActionBeanContext context;

    @DefaultHandler
    public Resolution displayPage() {
        
        //Set the actionbean.course_id to the  Course Title from the context
        course_title = context.getCourse().getTitle();
        
        
        return new ForwardResolution("/WEB-INF/jsp/course-tool.jsp");
    }

    @Override
    public ActionBeanContext getContext() {
        return context;
    }

    @Override
    public void setContext(ActionBeanContext context) {
        this.context = (BlackboardActionBeanContext)context;
    }
    
    
    private String course_title; 
       public String getCourse_title() {return course_title; }
    public void setCourse_title(String course_title) { this.course_title = course_title; }
    
    private String course_id; 
       public String getCourse_id() {return course_id; }
    public void setCourse_id(String course_id) { this.course_id = course_id; }
    
}


 

in the course_tools.jsp add in the actionBeans to display the Course Title and Pk1

 

 

mkauffman

Upgrade Your DVM

Posted by mkauffman Dec 28, 2016

Scott Hurrey mentioned this the other day during our Technical Office hours... You can upgrade a 3000.x DVM using the same installer package that you use for a self-hosted system. This blog post will show you how easy that is, after a brief plug for our office hours. If you've not attended, office hours a great way to get answers to your questions and to network with other developers. The schedule is at the bottom of the page here: Blackboard Learn Developers

 

Here's what you do to upgrade your DVM. You will do all of your work as the vagrant user. Do not use root or bbuser.

  1. Stop Learn
  2. Download the installer for the version you want to upgrade to from Behind the Blackboard. Note this only works for Q2 2016 and later.
    1. For this example, I upgraded my DVM from 3000.1.1 to 3000.1.3 (Q2 2016 CU1 to Q2 2016 CU3)
  3. As the vagrant user, create the recommended installation directories.
    1. mkdir /usr/local/bbinstaller
    2. mkdir /usr/local/bbinstaller/3000_1_3/
  4. Create an installer.properties file. I've placed the full text for one that works for a DVM below.
    1. Place the installer.properties file in the /usr/local/bbinstaller directory.
  5. Make a copy of the license file. You MUST do this because the install will mess everything up if the installer.properties is pointing to a license file inside the blackboard directory that is being upgraded.
    1. cp /usr/local/blackboard/config/license/blackboard-license.xml /usr/local/blackboard-license-copy.xml
    2. chown vagrant /usr/local/blackboard-license-copy.xml
  6. Move the downloaded .zip installer file to the /usr/local/bbinstaller/3000_1_3/ directory
    1. The trick here is that on the DVM the /vagrant directory is the same directory as the host directory where you ran the vagrant up and vagrant ssh command. You place the .zip installer file into that directory, then on the DVM guest you can move it from into the installation directory.
  7. cd /usr/local/bbinstaller/3000_1_3/
  8. unzip learn-installer-3000.1.3-rel.70+214db31.zip
  9. Run the upgrade
    1. ./installer.sh -c /usr/local/bbinstaller/installer.properties
  10. Run PushConfig, which will also start the server.
      1. cd /usr/local/blackboard/tools/admin
      2. ./PushConfigUpdates.sh

 

That's it! Below are the contents of the installer.properties file that works for upgrading a DVM:

 

# DVM installer.properties for upgrade. First attempt was from Q2 2016 CU1 to CU3.

# Kauffman 2016.12.28

bbconfig.basedir=/usr/local/blackboard

bbconfig.java.home=/usr/lib/jvm/java-8-oracle

 

# cp /usr/local/blackboard/config/license/blackboard-license.xml /usr/local/blackboard-license-copy.xml

# chown bbuser /usr/local/blackboard-license-copy.xml

bbconfig.file.license=/usr/local/blackboard-license-copy.xml

 

##         these properties should not be modified manually        ##

antargs.default.vi.db.name=BBLEARN

antargs.default.vi.stats.db.name=BBLEARN_stats

 

# just like the Windows upgrade, the Linux upgrade fails if you don't have these 3 dummy antarg lines

antargs.default.users.administrator.password=passworddoesnotmatter

antargs.default.users.integration.password=passworddoesnotmatter

antargs.default.users.rootadmin.password=passworddoesnotmatter

 

############## properties listed here can be modified ###############

antargs.default.vi.db.password=postgres

antargs.default.vi.stats.db.password=postgres

antargs.default.vi.report.user.password=password