Photo Directory Example


Let’s look at a more complicated example of using the Blackbaud K12 API with the “ON” products. Recently, I had to develop a photo directory, with pictures of faculty/staff and some information (job title, room number, phone number, e-mail, etc.) about them.  There isn’t a built-in report in the Blackbaud products for this, nor is it easy to export this data along with photos. To solve this, I wrote a simple PHP script, the extracts the data/photos using the API, formats them, and creates a web page that can easily be printed to PDF or sent to a printer.

At the bottom of this post, I’ll include the full source to this solution, but first I’ll walk you through the code below.

First, a little HTML/CSS to try to make the HTML table deal with page breaks, etc. a little better:

<html>
 <head>
 <style type="text/css">
 table { page-break-inside:auto }
 tr { page-break-inside:avoid; page-break-after:auto }
 thead { display:table-header-group }
 tfoot { display:table-footer-group }
 </style>
 </head>

Next, I need to set the execution timeout to something a little longer.  This script is going to make a lot of API calls, resize a ton of photos, and generally take a long time to run.  I’ll also keep track of how long it takes for the script to run and load up the Httpful library I mention in my last post.

error_reporting( error_reporting() & ~E_NOTICE );
ini_set('max_execution_time', 300); # set to five minutes (300 seconds)
$time_start = microtime(true); # start timing how long this script takes
include_once ('./httpful.phar'); # use Httpful http://phphttpclient.com/

Next, we get an authentication token:

$schoolWebsite = "https://latinschool.myschoolapp.com"; # website used to login to the "ON" products
$apiUser = "my_username";
$apiPassword = "my_password";

// Get authentication token for the Blackbaud K12 API
$uri = "$schoolWebsite/api/authentication/login/?username=". $apiUser . "&password=" . $apiPassword . "&format=json";
$response = \Httpful\Request::get($uri)->expectsJson() ->send();
$token = $response->body->{"Token"};

We will set some variables determine how many columns we want, the height of the table cells and image, and an image to use as a generic image when the user doesn’t have a profile photo.

#Some parameters for the photo directory
$numColums = 4; # How many columns do we want?
$tdHeight = 210; # Height of the table cell that holds the entry
$imgHeight = 170; # Image height
$genericPhotoURL = "http://vignette4.wikia.nocookie.net/detectiveconan96/images/7/72/Generic_Male_Profile.jpg/revision/latest?cb=20140709000724"; # This photo will show if we don't have a user profile 

Next, we’ll do a little error checking to see if our login effort worked and then start up our table:

if (strpos($token, 'Invalid') !== false) {
 echo "Invalid Login.<br>";
} else {
 echo "<table border=0>";
 $currentColumn = 1;

We are going to use the List API to gather the users we want. If you want to use this script, you’ll have to create your own list, and use it’s list ID in the API call. The comments have tips on how this list is setup.

 # Use Blackbaud K12 list API to gather nonteaching staff and teachers.
 # You can get the listid by hovering over edit and look for slid= in the link
 # on the website. Using lists is faster than doing individual API calls, 
 # and they can be edited by end users.
 #
 # If you'd like to generate a photo directory of something else, you can
 # change this list.
 #
 # The following objects were selected for this list:
 # + User Base
 # + User School Defined Fields
 # + User Role
 # + User Detail
 #
 # The following fields were selected for "display" (Display As)
 # + User Base.User ID (UserID)
 # + User Base.First Name (FirstName)
 # + User Base.Last Name (LastName)
 # + User Base.E-Mail (email)
 # + User Base.Host ID (HostID)
 # + User School Defined Fields. Defined 2 (latinid)
 #
 # The list has the following filter:
 # User Role.Role any of Non-Teaching Staff,Teacher
 #
 # The list is ordered by: (Change, to change sort order for photo directory)
 #
 # User Base.Last Name Ascending
 # Then By
 # User Base.First Name Ascending
 #
 
 $uri = $schoolWebsite . "/api/list/46815/?t=" . $token . "&format=json";
 $response = \Httpful\Request::get($uri)-> send();
 $employees = $response->body;

Now, we will loop through the list results and grab information about each person in the list:

 $i = 0;
 
 foreach ($employees as $employee) {
 $i++;
 $fname = $employee->{"FirstName"};
 $lname = $employee->{"LastName"};
 $whsid = $employee->{"UserID"};
 $hostid = $employee->{"HostID"};

Next, we will use the /user/extended API call to get more details about each user, including a URL to where their profile photo is stored.

 # get details on this employee via the Blackbaud K12 API
 # /user/extended uses the system ID or "UserID" in the call
 # to get detailed information on an individual user. It can
 # give us access to data we cannot otherwise see, such as the 
 # URL to their profile photo.
 $uri = $schoolWebsite . "/api/user/extended/" . $whsid . "/?t=" . $token . "&format=json";
 $response = \Httpful\Request::get($uri)-> send();
 
 # Let's set some variables with the data from user/extended:
 $employeeDetail = $response->body; 
 $uname = $employeeDetail->{"UserName"};
 $employeeLatinID = $employeeDetail->{"CustomField2"}; # My school stores our ID number in this custom field
 $employeePhotobookCode = $employeeDetail->{"CustomField10"}; # Code to use to omit people from the directory
 $employeeProfilePhoto = $employeeDetail->{"ProfilePhoto"};
 $employeePhotoURL = $employeeProfilePhoto->{"LargeFilenameUrl"};
 if($employeePhotoURL !== "") {
 $employeePhotoURL = $schoolWebsite . $employeePhotoURL; # The photo URL needs the school website added to it
 }
 
 # Job Titles are harder. They are contained within an array called OccupationList.
 # A single person can have multiple occupations. We are going to go through the
 # array and look at each occupation. We don't have a great well to tell which is 
 # the right one to use. In my example, I'm going to look for a business name 
 # matching my school name and use the title found there. Obviously, if there are
 # more then one occupation listed with the same employer name, it will overwrite
 # the data, but hopefully, we don't have the same person listed more than once.
 # Finally, as a fail safe, it will set it to the last title found, if we haven't
 # set the job title yet.
 
 $employeeOccupationList = $employeeDetail->{"OccupationList"};
 $employeeTitle = "";
 foreach ($employeeOccupationList as $employeeOccupation) {
 $employeeBusinessName = $employeeOccupation->{"BusinessName"};
 $employeeOccupationTitle = $employeeOccupation->{"JobTitle"};
 
 if (($employeeBusinessName = "Latin School of Chicago") || ($employeeBusinessName = "The Latin School of Chicago")) {
 $employeeTitle = $employeeOccupationTitle;
 } else {
 if ($employeeTitle == "") { 
 // If we haven't set something yet for the user, let's try setting this title, 
 // even though the business name isn't set right
 $employeeTitle = $employeeOccupationTitle;
 }
 }
 }
 
 # Let's grab their work address. It's stored in an array, but we never
 # have more than one address. This will loop through it and grab the
 # last (hopefully only) value. If you have multiple values here, you'll
 # have to come up with a better way of dealing with this.
 $employeeOffice = "";
 $employeeAddressList = $employeeDetail->{"AddressList"};
 foreach ($employeeAddressList as $employeeAddress) {
 $employeeAddressType = $employeeAddress->{"address_type"};
 $employeeAddressLn1 = $employeeAddress->{"AddressLine1"};
 
 if($employeeAddressType == "Business/College") {
 $employeeOffice = $employeeAddressLn1;
 }
 }
 
 # Now let's grab their work phone number. Same deal as address.
 $employeeOfficePhone = "";
 $employeeOfficePhoneList = $employeeDetail->{"PhoneList"};
 foreach ($employeeOfficePhoneList as $employeeOfficePhoneItem) {
 $employeeOfficePhoneType = $employeeOfficePhoneItem->{"Type"};
 $employeeOfficePhoneNumber = $employeeOfficePhoneItem->{"PhoneNumber"};
 
 if($employeeOfficePhoneType == "Business/College") {
 $employeeOfficePhone = $employeeOfficePhoneNumber;
 $employeeOfficePhone = str_replace(' ', '', $employeeOfficePhone);
 $employeeOfficePhone = str_replace(')', '.', $employeeOfficePhone);
 $employeeOfficePhone = str_replace('(', '', $employeeOfficePhone); 
 $employeeOfficePhone = str_replace('-', '.', $employeeOfficePhone); 
 }
 }

You’ll notice we have to jump through some hoops with phones, addresses and titles, since the API returns a list (array) of responses for each.

We’ve made use of user defined field 10 to exclude certain people from the directory. If there is a value in the field, we skip that person.

 if($employeePhotobookCode == "") { 
 # We use CustomField10 as a place to exclude folks from the photo directory
 
 if($currentColumn == 1) {
 echo "<tr>";
 }

 echo "<td valign=top align=center height=$tdHeight width=25%>";
 # This is the table cell with all of the user data

Here is the code block that produces the content for each directory entry. We run the photo through another script (img.php) which resizes and crops them.

 # First the photo: 
 if($employeePhotoURL !== "") {
 echo "<a href=\"$uri\"><img src=\"img.php?url=$employeePhotoURL\" height=\"$imgHeight\"></a><br>\n";
 # img.php is a script that crops and resizes the images to a standard size
 } else {
 echo "<a href=\"$uri\"><img src=\"img.php?url=$genericPhotoURL\"></a><br>\n";
 }
 echo "<font size=-1>$fname $lname<br></font>\n";
 echo "<font size=-2>$employeeTitle<br></font>\n";
 echo "<font size=-2>$employeeOffice &nbsp; &nbsp; $employeeOfficePhone<br></font>\n";

A little cleanup of our HTML table and the end of our script:

 if($currentColumn == $numColums) {
 echo "</td></tr>";
 $currentColumn = 1; 
 } else {
 echo "</td>";
 $currentColumn++;
 }

 }
 
 if($i>2500) { break;} # Stop if we have way too many results. Can decrease number to debug 
 } 
 
 if ($currentColumn == $numColums) {
 echo "</table>";
 } else {
 echo "</tr></table>";

 }
}

echo "<br><br>Employees Found: $i<br>";
$time_end = microtime(true);
$time = $time_end - $time_start;
echo "Run time: " . round($time,2) . " s";
?>

</html>

 

Here is the script in it’s entirety.

<html>
 <head>
 <style type="text/css">
 table { page-break-inside:auto }
 tr { page-break-inside:avoid; page-break-after:auto }
 thead { display:table-header-group }
 tfoot { display:table-footer-group }
 </style>
 </head>

<?php
#------------------------------------------------------------------------------ 
# Copyright 2016 Shandor Simon (shandor at gmail dot com) 
# https://45-79-134-51.ip.linodeusercontent.com 
# 
# This work is licenced under the Creative Commons 
# Attribution-NonCommercial-ShareAlike 2.5 License. 
# To view a copy of this licence, visit 
# http://creativecommons.org/licenses/by-nc-sa/2.5/ 
# or send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, 
# California 94305, USA. 
#
# This code is provided as-is with no warranty expressed or implied.
# It is possible to change or delete data using this API. You should
# make use of it at your own discretion/risk. You are solely responsible 
# for its use.
#
#------------------------------------------------------------------------------ 
# Version 1.0.1 - 2016-07-01
#------------------------------------------------------------------------------ 
# Blackbaud K12 "ON" products API example in PHP to generate a photo directory
# that is intended to be printed

error_reporting( error_reporting() & ~E_NOTICE );
ini_set('max_execution_time', 300); # set to five minutes (300 seconds)
$time_start = microtime(true); # start timing how long this script takes
include_once ('./httpful.phar'); # use Httpful http://phphttpclient.com/

$schoolWebsite = "https://latinschool.myschoolapp.com"; # website used to login to the "ON" products
$apiUser = "my_username";
$apiPassword = "my_password";
$debug = true;
$genericPhotoURL = "http://vignette4.wikia.nocookie.net/detectiveconan96/images/7/72/Generic_Male_Profile.jpg/revision/latest?cb=20140709000724"; # This photo will show if we don't have a user profile 

#Some parameters for the photo directory
$numColums = 4; # How many columns do we want?
$tdHeight = 210; # Height of the table cell that holds the entry
$imgHeight = 170; # Image height

// Get authentication token for the Blackbaud K12 API
$uri = "$schoolWebsite/api/authentication/login/?username=". $apiUser . "&password=" . $apiPassword . "&format=json";
$response = \Httpful\Request::get($uri)->expectsJson() ->send();
$token = $response->body->{"Token"};

if (strpos($token, 'Invalid') !== false) {
 echo "Invalid Login.<br>";
} else {
 echo "<table border=0>";
 $currentColumn = 1;
 
 # Use Blackbaud K12 list API to gather nonteaching staff and teachers.
 # You can get the listid by hovering over edit and look for slid= in the link
 # on the website. Using lists is faster than doing individual API calls, 
 # and they can be edited by end users.
 #
 # If you'd like to generate a photo directory of something else, you can
 # change this list.
 #
 # The following objects were selected for this list:
 # + User Base
 # + User School Defined Fields
 # + User Role
 # + User Detail
 #
 # The following fields were selected for "display" (Display As)
 # + User Base.User ID (UserID)
 # + User Base.First Name (FirstName)
 # + User Base.Last Name (LastName)
 # + User Base.E-Mail (email)
 # + User Base.Host ID (HostID)
 # + User School Defined Fields. Defined 2 (latinid)
 #
 # The list has the following filter:
 # User Role.Role any of Non-Teaching Staff,Teacher
 #
 # The list is ordered by: (Change, to change sort order for photo directory)
 #
 # User Base.Last Name Ascending
 # Then By
 # User Base.First Name Ascending
 #
 
 $uri = $schoolWebsite . "/api/list/46815/?t=" . $token . "&format=json";
 $response = \Httpful\Request::get($uri)-> send();
 $employees = $response->body;
 
 $i = 0;
 
 foreach ($employees as $employee) {
 $i++;
 $fname = $employee->{"FirstName"};
 $lname = $employee->{"LastName"};
 $whsid = $employee->{"UserID"};
 $hostid = $employee->{"HostID"};
 
 # get details on this employee via the Blackbaud K12 API
 # /user/extended uses the system ID or "UserID" in the call
 # to get detailed information on an individual user. It can
 # give us access to data we cannot otherwise see, such as the 
 # URL to their profile photo.
 $uri = $schoolWebsite . "/api/user/extended/" . $whsid . "/?t=" . $token . "&format=json";
 $response = \Httpful\Request::get($uri)-> send();
 
 # Let's set some variables with the data from user/extended:
 $employeeDetail = $response->body; 
 $uname = $employeeDetail->{"UserName"};
 $employeeLatinID = $employeeDetail->{"CustomField2"}; # My school stores our ID number in this custom field
 $employeePhotobookCode = $employeeDetail->{"CustomField10"}; # Code to use to omit people from the directory
 $employeeProfilePhoto = $employeeDetail->{"ProfilePhoto"};
 $employeePhotoURL = $employeeProfilePhoto->{"LargeFilenameUrl"};
 if($employeePhotoURL !== "") {
 $employeePhotoURL = $schoolWebsite . $employeePhotoURL; # The photo URL needs the school website added to it
 }
 
 # Job Titles are harder. They are contained within an array called OccupationList.
 # A single person can have multiple occupations. We are going to go through the
 # array and look at each occupation. We don't have a great well to tell which is 
 # the right one to use. In my example, I'm going to look for a business name 
 # matching my school name and use the title found there. Obviously, if there are
 # more then one occupation listed with the same employer name, it will overwrite
 # the data, but hopefully, we don't have the same person listed more than once.
 # Finally, as a fail safe, it will set it to the last title found, if we haven't
 # set the job title yet.
 
 $employeeOccupationList = $employeeDetail->{"OccupationList"};
 $employeeTitle = "";
 foreach ($employeeOccupationList as $employeeOccupation) {
 $employeeBusinessName = $employeeOccupation->{"BusinessName"};
 $employeeOccupationTitle = $employeeOccupation->{"JobTitle"};
 
 if (($employeeBusinessName = "Latin School of Chicago") || ($employeeBusinessName = "The Latin School of Chicago")) {
 $employeeTitle = $employeeOccupationTitle;
 } else {
 if ($employeeTitle == "") { 
 // If we haven't set something yet for the user, let's try setting this title, 
 // even though the business name isn't set right
 $employeeTitle = $employeeOccupationTitle;
 }
 }
 }
 
 # Let's grab their work address. It's stored in an array, but we never
 # have more than one address. This will loop through it and grab the
 # last (hopefully only) value. If you have multiple values here, you'll
 # have to come up with a better way of dealing with this.
 $employeeOffice = "";
 $employeeAddressList = $employeeDetail->{"AddressList"};
 foreach ($employeeAddressList as $employeeAddress) {
 $employeeAddressType = $employeeAddress->{"address_type"};
 $employeeAddressLn1 = $employeeAddress->{"AddressLine1"};
 
 if($employeeAddressType == "Business/College") {
 $employeeOffice = $employeeAddressLn1;
 }
 }
 
 # Now let's grab their work phone number. Same deal as address.
 $employeeOfficePhone = "";
 $employeeOfficePhoneList = $employeeDetail->{"PhoneList"};
 foreach ($employeeOfficePhoneList as $employeeOfficePhoneItem) {
 $employeeOfficePhoneType = $employeeOfficePhoneItem->{"Type"};
 $employeeOfficePhoneNumber = $employeeOfficePhoneItem->{"PhoneNumber"};
 
 if($employeeOfficePhoneType == "Business/College") {
 $employeeOfficePhone = $employeeOfficePhoneNumber;
 $employeeOfficePhone = str_replace(' ', '', $employeeOfficePhone);
 $employeeOfficePhone = str_replace(')', '.', $employeeOfficePhone);
 $employeeOfficePhone = str_replace('(', '', $employeeOfficePhone); 
 $employeeOfficePhone = str_replace('-', '.', $employeeOfficePhone); 
 }
 }
 
 if($employeePhotobookCode == "") { 
 # We use CustomField10 as a place to exclude folks from the photo directory
 
 if($currentColumn == 1) {
 echo "<tr>";
 }

 echo "<td valign=top align=center height=$tdHeight width=25%>";
 # This is the table cell with all of the user data
 
 # First the photo: 
 if($employeePhotoURL !== "") {
 echo "<a href=\"$uri\"><img src=\"img.php?url=$employeePhotoURL\" height=\"$imgHeight\"></a><br>\n";
 # img.php is a script that crops and resizes the images to a standard size
 } else {
 echo "<a href=\"$uri\"><img src=\"img.php?url=$genericPhotoURL\"></a><br>\n";
 }
 echo "<font size=-1>$fname $lname<br></font>\n";
 echo "<font size=-2>$employeeTitle<br></font>\n";
 echo "<font size=-2>$employeeOffice &nbsp; &nbsp; $employeeOfficePhone<br></font>\n";
 
 if($currentColumn == $numColums) {
 echo "</td></tr>";
 $currentColumn = 1; 
 } else {
 echo "</td>";
 $currentColumn++;
 }

 }
 
 if($i>2500) { break;} # Stop if we have way too many results. Can decrease number to debug 
 } 
 
 if ($currentColumn == $numColums) {
 echo "</table>";
 } else {
 echo "</tr></table>";

 }
}

echo "<br><br>Employees Found: $i<br>";
$time_end = microtime(true);
$time = $time_end - $time_start;
echo "Run time: " . round($time,2) . " s";
?>

</html>

Here is the img.php script that is used to crop and resize the images:

asd
<?php

# https://github.com/Nimrod007/PHP_image_resize
# http://www.nimrodstech.com/php-image-resize/

include_once('php_image_resize.php');

$imgURL = $_GET["url"];
$resizeResult = smart_resize_image($imgURL,'',180,170,false,'browser',flase,false,100,false);

?>
,

2 responses to “Photo Directory Example”

  1. Uh. Shandor, you can now do this with a google spreadsheet since you can format your user photo url based on mashup of your school’s Blackbaud ID and the userthumb id (usually user id # if you used batch uploader inside of the ON products).

  2. You have a lot more freedom and flexibility using the API as you describe, but I think Google Sheets calling user ids would be easier for folks (like me) who are new / scared of API calls. I realize I should actually contribute a post instead of just mentioning things in comments. I’ll get on it and put something together.

Leave a Reply to Graham Getty Cancel reply

Your email address will not be published. Required fields are marked *