{"id":66,"date":"2016-07-19T00:49:59","date_gmt":"2016-07-19T00:49:59","guid":{"rendered":"http:\/\/duff.io\/?p=66"},"modified":"2024-04-05T03:12:28","modified_gmt":"2024-04-05T03:12:28","slug":"photo-directory-example","status":"publish","type":"post","link":"https:\/\/duff.io\/?p=66","title":{"rendered":"Photo Directory Example"},"content":{"rendered":"<p><img loading=\"lazy\" decoding=\"async\" class=\"alignright wp-image-73 size-thumbnail\" src=\"http:\/\/duff.io\/wp-content\/uploads\/2016\/07\/Screenshot-2016-07-18-19.55.10-150x150.png\" width=\"150\" height=\"150\" \/>Let&#8217;s look at a more complicated example of using the Blackbaud K12 API with the &#8220;ON&#8221; 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. \u00a0There isn&#8217;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.<\/p>\n<p>At the bottom of this post, I&#8217;ll include the full source to this solution, but first I&#8217;ll walk you through the code below.<\/p>\n<p>First, a little HTML\/CSS to try to make the HTML table deal with page breaks, etc. a little better:<\/p>\n<pre class=\"brush: html\">&lt;html&gt;\n &lt;head&gt;\n &lt;style type=\"text\/css\"&gt;\n table { page-break-inside:auto }\n tr { page-break-inside:avoid; page-break-after:auto }\n thead { display:table-header-group }\n tfoot { display:table-footer-group }\n &lt;\/style&gt;\n &lt;\/head&gt;\n<\/pre>\n<p>Next, I need to set the execution timeout to something a little longer. \u00a0This script is going to make a lot of API calls, resize a ton of photos, and generally take a long time to run. \u00a0I&#8217;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.<\/p>\n<pre class=\"brush: php\">error_reporting( error_reporting() &amp; ~E_NOTICE );\nini_set('max_execution_time', 300); # set to five minutes (300 seconds)\n$time_start = microtime(true); # start timing how long this script takes\ninclude_once ('.\/httpful.phar'); # use Httpful http:\/\/phphttpclient.com\/\n<\/pre>\n<p>Next, we get an authentication token:<\/p>\n<pre class=\"brush: php\">$schoolWebsite = \"https:\/\/latinschool.myschoolapp.com\"; # website used to login to the \"ON\" products\n$apiUser = \"my_username\";\n$apiPassword = \"my_password\";\n\n\/\/ Get authentication token for the Blackbaud K12 API\n$uri = \"$schoolWebsite\/api\/authentication\/login\/?username=\". $apiUser . \"&amp;password=\" . $apiPassword . \"&amp;format=json\";\n$response = \\Httpful\\Request::get($uri)-&gt;expectsJson() -&gt;send();\n$token = $response-&gt;body-&gt;{\"Token\"};\n<\/pre>\n<p>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&#8217;t have a profile photo.<\/p>\n<pre class=\"brush: php\">#Some parameters for the photo directory\n$numColums = 4; # How many columns do we want?\n$tdHeight = 210; # Height of the table cell that holds the entry\n$imgHeight = 170; # Image height\n$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 \n<\/pre>\n<p>Next, we&#8217;ll do a little error checking to see if our login effort worked and then start up our table:<\/p>\n<pre class=\"brush: php\">if (strpos($token, 'Invalid') !== false) {\n echo \"Invalid Login.&lt;br&gt;\";\n} else {\n echo \"&lt;table border=0&gt;\";\n $currentColumn = 1;\n<\/pre>\n<p>We are going to use the List API to gather the users we want. If you want to use this script, you&#8217;ll have to create your own list, and use it&#8217;s list ID in the API call. The comments have tips on how this list is setup.<\/p>\n<pre class=\"brush: php\"> # Use Blackbaud K12 list API to gather nonteaching staff and teachers.\n # You can get the listid by hovering over edit and look for slid= in the link\n # on the website. Using lists is faster than doing individual API calls, \n # and they can be edited by end users.\n #\n # If you'd like to generate a photo directory of something else, you can\n # change this list.\n #\n # The following objects were selected for this list:\n # + User Base\n # + User School Defined Fields\n # + User Role\n # + User Detail\n #\n # The following fields were selected for \"display\" (Display As)\n # + User Base.User ID (UserID)\n # + User Base.First Name (FirstName)\n # + User Base.Last Name (LastName)\n # + User Base.E-Mail (email)\n # + User Base.Host ID (HostID)\n # + User School Defined Fields. Defined 2 (latinid)\n #\n # The list has the following filter:\n # User Role.Role any of Non-Teaching Staff,Teacher\n #\n # The list is ordered by: (Change, to change sort order for photo directory)\n #\n # User Base.Last Name Ascending\n # Then By\n # User Base.First Name Ascending\n #\n \n $uri = $schoolWebsite . \"\/api\/list\/46815\/?t=\" . $token . \"&amp;format=json\";\n $response = \\Httpful\\Request::get($uri)-&gt; send();\n $employees = $response-&gt;body;\n<\/pre>\n<p>Now, we will loop through the list results and grab information about each person in the list:<\/p>\n<pre class=\"brush: php\"> $i = 0;\n \n foreach ($employees as $employee) {\n $i++;\n $fname = $employee-&gt;{\"FirstName\"};\n $lname = $employee-&gt;{\"LastName\"};\n $whsid = $employee-&gt;{\"UserID\"};\n $hostid = $employee-&gt;{\"HostID\"};\n<\/pre>\n<p>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.<\/p>\n<pre class=\"brush: php\"> # get details on this employee via the Blackbaud K12 API\n # \/user\/extended uses the system ID or \"UserID\" in the call\n # to get detailed information on an individual user. It can\n # give us access to data we cannot otherwise see, such as the \n # URL to their profile photo.\n $uri = $schoolWebsite . \"\/api\/user\/extended\/\" . $whsid . \"\/?t=\" . $token . \"&amp;format=json\";\n $response = \\Httpful\\Request::get($uri)-&gt; send();\n \n # Let's set some variables with the data from user\/extended:\n $employeeDetail = $response-&gt;body; \n $uname = $employeeDetail-&gt;{\"UserName\"};\n $employeeLatinID = $employeeDetail-&gt;{\"CustomField2\"}; # My school stores our ID number in this custom field\n $employeePhotobookCode = $employeeDetail-&gt;{\"CustomField10\"}; # Code to use to omit people from the directory\n $employeeProfilePhoto = $employeeDetail-&gt;{\"ProfilePhoto\"};\n $employeePhotoURL = $employeeProfilePhoto-&gt;{\"LargeFilenameUrl\"};\n if($employeePhotoURL !== \"\") {\n $employeePhotoURL = $schoolWebsite . $employeePhotoURL; # The photo URL needs the school website added to it\n }\n \n # Job Titles are harder. They are contained within an array called OccupationList.\n # A single person can have multiple occupations. We are going to go through the\n # array and look at each occupation. We don't have a great well to tell which is \n # the right one to use. In my example, I'm going to look for a business name \n # matching my school name and use the title found there. Obviously, if there are\n # more then one occupation listed with the same employer name, it will overwrite\n # the data, but hopefully, we don't have the same person listed more than once.\n # Finally, as a fail safe, it will set it to the last title found, if we haven't\n # set the job title yet.\n \n $employeeOccupationList = $employeeDetail-&gt;{\"OccupationList\"};\n $employeeTitle = \"\";\n foreach ($employeeOccupationList as $employeeOccupation) {\n $employeeBusinessName = $employeeOccupation-&gt;{\"BusinessName\"};\n $employeeOccupationTitle = $employeeOccupation-&gt;{\"JobTitle\"};\n \n if (($employeeBusinessName = \"Latin School of Chicago\") || ($employeeBusinessName = \"The Latin School of Chicago\")) {\n $employeeTitle = $employeeOccupationTitle;\n } else {\n if ($employeeTitle == \"\") { \n \/\/ If we haven't set something yet for the user, let's try setting this title, \n \/\/ even though the business name isn't set right\n $employeeTitle = $employeeOccupationTitle;\n }\n }\n }\n \n # Let's grab their work address. It's stored in an array, but we never\n # have more than one address. This will loop through it and grab the\n # last (hopefully only) value. If you have multiple values here, you'll\n # have to come up with a better way of dealing with this.\n $employeeOffice = \"\";\n $employeeAddressList = $employeeDetail-&gt;{\"AddressList\"};\n foreach ($employeeAddressList as $employeeAddress) {\n $employeeAddressType = $employeeAddress-&gt;{\"address_type\"};\n $employeeAddressLn1 = $employeeAddress-&gt;{\"AddressLine1\"};\n \n if($employeeAddressType == \"Business\/College\") {\n $employeeOffice = $employeeAddressLn1;\n }\n }\n \n # Now let's grab their work phone number. Same deal as address.\n $employeeOfficePhone = \"\";\n $employeeOfficePhoneList = $employeeDetail-&gt;{\"PhoneList\"};\n foreach ($employeeOfficePhoneList as $employeeOfficePhoneItem) {\n $employeeOfficePhoneType = $employeeOfficePhoneItem-&gt;{\"Type\"};\n $employeeOfficePhoneNumber = $employeeOfficePhoneItem-&gt;{\"PhoneNumber\"};\n \n if($employeeOfficePhoneType == \"Business\/College\") {\n $employeeOfficePhone = $employeeOfficePhoneNumber;\n $employeeOfficePhone = str_replace(' ', '', $employeeOfficePhone);\n $employeeOfficePhone = str_replace(')', '.', $employeeOfficePhone);\n $employeeOfficePhone = str_replace('(', '', $employeeOfficePhone); \n $employeeOfficePhone = str_replace('-', '.', $employeeOfficePhone); \n }\n }\n<\/pre>\n<p>You&#8217;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.<\/p>\n<p>We&#8217;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.<\/p>\n<pre class=\"brush: php\"> if($employeePhotobookCode == \"\") { \n # We use CustomField10 as a place to exclude folks from the photo directory\n \n if($currentColumn == 1) {\n echo \"&lt;tr&gt;\";\n }\n\n echo \"&lt;td valign=top align=center height=$tdHeight width=25%&gt;\";\n # This is the table cell with all of the user data\n<\/pre>\n<p>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.<\/p>\n<pre class=\"brush: php\"> # First the photo: \n if($employeePhotoURL !== \"\") {\n echo \"&lt;a href=\\\"$uri\\\"&gt;&lt;img src=\\\"img.php?url=$employeePhotoURL\\\" height=\\\"$imgHeight\\\"&gt;&lt;\/a&gt;&lt;br&gt;\\n\";\n # img.php is a script that crops and resizes the images to a standard size\n } else {\n echo \"&lt;a href=\\\"$uri\\\"&gt;&lt;img src=\\\"img.php?url=$genericPhotoURL\\\"&gt;&lt;\/a&gt;&lt;br&gt;\\n\";\n }\n echo \"&lt;font size=-1&gt;$fname $lname&lt;br&gt;&lt;\/font&gt;\\n\";\n echo \"&lt;font size=-2&gt;$employeeTitle&lt;br&gt;&lt;\/font&gt;\\n\";\n echo \"&lt;font size=-2&gt;$employeeOffice &amp;nbsp; &amp;nbsp; $employeeOfficePhone&lt;br&gt;&lt;\/font&gt;\\n\";\n<\/pre>\n<p>A little cleanup of our HTML table and the end of our script:<\/p>\n<pre class=\"brush: php\"> if($currentColumn == $numColums) {\n echo \"&lt;\/td&gt;&lt;\/tr&gt;\";\n $currentColumn = 1; \n } else {\n echo \"&lt;\/td&gt;\";\n $currentColumn++;\n }\n\n }\n \n if($i&gt;2500) { break;} # Stop if we have way too many results. Can decrease number to debug \n } \n \n if ($currentColumn == $numColums) {\n echo \"&lt;\/table&gt;\";\n } else {\n echo \"&lt;\/tr&gt;&lt;\/table&gt;\";\n\n }\n}\n\necho \"&lt;br&gt;&lt;br&gt;Employees Found: $i&lt;br&gt;\";\n$time_end = microtime(true);\n$time = $time_end - $time_start;\necho \"Run time: \" . round($time,2) . \" s\";\n?&gt;\n\n&lt;\/html&gt;<\/pre>\n<p>&nbsp;<\/p>\n<p>Here is the script in it&#8217;s entirety.<\/p>\n<pre class=\"brush: php\">&lt;html&gt;\n &lt;head&gt;\n &lt;style type=\"text\/css\"&gt;\n table { page-break-inside:auto }\n tr { page-break-inside:avoid; page-break-after:auto }\n thead { display:table-header-group }\n tfoot { display:table-footer-group }\n &lt;\/style&gt;\n &lt;\/head&gt;\n\n&lt;?php\n#------------------------------------------------------------------------------ \n# Copyright 2016 Shandor Simon (shandor at gmail dot com) \n# https:\/\/45-79-134-51.ip.linodeusercontent.com \n# \n# This work is licenced under the Creative Commons \n# Attribution-NonCommercial-ShareAlike 2.5 License. \n# To view a copy of this licence, visit \n# http:\/\/creativecommons.org\/licenses\/by-nc-sa\/2.5\/ \n# or send a letter to Creative Commons, 559 Nathan Abbott Way, Stanford, \n# California 94305, USA. \n#\n# This code is provided as-is with no warranty expressed or implied.\n# It is possible to change or delete data using this API. You should\n# make use of it at your own discretion\/risk. You are solely responsible \n# for its use.\n#\n#------------------------------------------------------------------------------ \n# Version 1.0.1 - 2016-07-01\n#------------------------------------------------------------------------------ \n# Blackbaud K12 \"ON\" products API example in PHP to generate a photo directory\n# that is intended to be printed\n\nerror_reporting( error_reporting() &amp; ~E_NOTICE );\nini_set('max_execution_time', 300); # set to five minutes (300 seconds)\n$time_start = microtime(true); # start timing how long this script takes\ninclude_once ('.\/httpful.phar'); # use Httpful http:\/\/phphttpclient.com\/\n\n$schoolWebsite = \"https:\/\/latinschool.myschoolapp.com\"; # website used to login to the \"ON\" products\n$apiUser = \"my_username\";\n$apiPassword = \"my_password\";\n$debug = true;\n$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 \n\n#Some parameters for the photo directory\n$numColums = 4; # How many columns do we want?\n$tdHeight = 210; # Height of the table cell that holds the entry\n$imgHeight = 170; # Image height\n\n\/\/ Get authentication token for the Blackbaud K12 API\n$uri = \"$schoolWebsite\/api\/authentication\/login\/?username=\". $apiUser . \"&amp;password=\" . $apiPassword . \"&amp;format=json\";\n$response = \\Httpful\\Request::get($uri)-&gt;expectsJson() -&gt;send();\n$token = $response-&gt;body-&gt;{\"Token\"};\n\nif (strpos($token, 'Invalid') !== false) {\n echo \"Invalid Login.&lt;br&gt;\";\n} else {\n echo \"&lt;table border=0&gt;\";\n $currentColumn = 1;\n \n # Use Blackbaud K12 list API to gather nonteaching staff and teachers.\n # You can get the listid by hovering over edit and look for slid= in the link\n # on the website. Using lists is faster than doing individual API calls, \n # and they can be edited by end users.\n #\n # If you'd like to generate a photo directory of something else, you can\n # change this list.\n #\n # The following objects were selected for this list:\n # + User Base\n # + User School Defined Fields\n # + User Role\n # + User Detail\n #\n # The following fields were selected for \"display\" (Display As)\n # + User Base.User ID (UserID)\n # + User Base.First Name (FirstName)\n # + User Base.Last Name (LastName)\n # + User Base.E-Mail (email)\n # + User Base.Host ID (HostID)\n # + User School Defined Fields. Defined 2 (latinid)\n #\n # The list has the following filter:\n # User Role.Role any of Non-Teaching Staff,Teacher\n #\n # The list is ordered by: (Change, to change sort order for photo directory)\n #\n # User Base.Last Name Ascending\n # Then By\n # User Base.First Name Ascending\n #\n \n $uri = $schoolWebsite . \"\/api\/list\/46815\/?t=\" . $token . \"&amp;format=json\";\n $response = \\Httpful\\Request::get($uri)-&gt; send();\n $employees = $response-&gt;body;\n \n $i = 0;\n \n foreach ($employees as $employee) {\n $i++;\n $fname = $employee-&gt;{\"FirstName\"};\n $lname = $employee-&gt;{\"LastName\"};\n $whsid = $employee-&gt;{\"UserID\"};\n $hostid = $employee-&gt;{\"HostID\"};\n \n # get details on this employee via the Blackbaud K12 API\n # \/user\/extended uses the system ID or \"UserID\" in the call\n # to get detailed information on an individual user. It can\n # give us access to data we cannot otherwise see, such as the \n # URL to their profile photo.\n $uri = $schoolWebsite . \"\/api\/user\/extended\/\" . $whsid . \"\/?t=\" . $token . \"&amp;format=json\";\n $response = \\Httpful\\Request::get($uri)-&gt; send();\n \n # Let's set some variables with the data from user\/extended:\n $employeeDetail = $response-&gt;body; \n $uname = $employeeDetail-&gt;{\"UserName\"};\n $employeeLatinID = $employeeDetail-&gt;{\"CustomField2\"}; # My school stores our ID number in this custom field\n $employeePhotobookCode = $employeeDetail-&gt;{\"CustomField10\"}; # Code to use to omit people from the directory\n $employeeProfilePhoto = $employeeDetail-&gt;{\"ProfilePhoto\"};\n $employeePhotoURL = $employeeProfilePhoto-&gt;{\"LargeFilenameUrl\"};\n if($employeePhotoURL !== \"\") {\n $employeePhotoURL = $schoolWebsite . $employeePhotoURL; # The photo URL needs the school website added to it\n }\n \n # Job Titles are harder. They are contained within an array called OccupationList.\n # A single person can have multiple occupations. We are going to go through the\n # array and look at each occupation. We don't have a great well to tell which is \n # the right one to use. In my example, I'm going to look for a business name \n # matching my school name and use the title found there. Obviously, if there are\n # more then one occupation listed with the same employer name, it will overwrite\n # the data, but hopefully, we don't have the same person listed more than once.\n # Finally, as a fail safe, it will set it to the last title found, if we haven't\n # set the job title yet.\n \n $employeeOccupationList = $employeeDetail-&gt;{\"OccupationList\"};\n $employeeTitle = \"\";\n foreach ($employeeOccupationList as $employeeOccupation) {\n $employeeBusinessName = $employeeOccupation-&gt;{\"BusinessName\"};\n $employeeOccupationTitle = $employeeOccupation-&gt;{\"JobTitle\"};\n \n if (($employeeBusinessName = \"Latin School of Chicago\") || ($employeeBusinessName = \"The Latin School of Chicago\")) {\n $employeeTitle = $employeeOccupationTitle;\n } else {\n if ($employeeTitle == \"\") { \n \/\/ If we haven't set something yet for the user, let's try setting this title, \n \/\/ even though the business name isn't set right\n $employeeTitle = $employeeOccupationTitle;\n }\n }\n }\n \n # Let's grab their work address. It's stored in an array, but we never\n # have more than one address. This will loop through it and grab the\n # last (hopefully only) value. If you have multiple values here, you'll\n # have to come up with a better way of dealing with this.\n $employeeOffice = \"\";\n $employeeAddressList = $employeeDetail-&gt;{\"AddressList\"};\n foreach ($employeeAddressList as $employeeAddress) {\n $employeeAddressType = $employeeAddress-&gt;{\"address_type\"};\n $employeeAddressLn1 = $employeeAddress-&gt;{\"AddressLine1\"};\n \n if($employeeAddressType == \"Business\/College\") {\n $employeeOffice = $employeeAddressLn1;\n }\n }\n \n # Now let's grab their work phone number. Same deal as address.\n $employeeOfficePhone = \"\";\n $employeeOfficePhoneList = $employeeDetail-&gt;{\"PhoneList\"};\n foreach ($employeeOfficePhoneList as $employeeOfficePhoneItem) {\n $employeeOfficePhoneType = $employeeOfficePhoneItem-&gt;{\"Type\"};\n $employeeOfficePhoneNumber = $employeeOfficePhoneItem-&gt;{\"PhoneNumber\"};\n \n if($employeeOfficePhoneType == \"Business\/College\") {\n $employeeOfficePhone = $employeeOfficePhoneNumber;\n $employeeOfficePhone = str_replace(' ', '', $employeeOfficePhone);\n $employeeOfficePhone = str_replace(')', '.', $employeeOfficePhone);\n $employeeOfficePhone = str_replace('(', '', $employeeOfficePhone); \n $employeeOfficePhone = str_replace('-', '.', $employeeOfficePhone); \n }\n }\n \n if($employeePhotobookCode == \"\") { \n # We use CustomField10 as a place to exclude folks from the photo directory\n \n if($currentColumn == 1) {\n echo \"&lt;tr&gt;\";\n }\n\n echo \"&lt;td valign=top align=center height=$tdHeight width=25%&gt;\";\n # This is the table cell with all of the user data\n \n # First the photo: \n if($employeePhotoURL !== \"\") {\n echo \"&lt;a href=\\\"$uri\\\"&gt;&lt;img src=\\\"img.php?url=$employeePhotoURL\\\" height=\\\"$imgHeight\\\"&gt;&lt;\/a&gt;&lt;br&gt;\\n\";\n # img.php is a script that crops and resizes the images to a standard size\n } else {\n echo \"&lt;a href=\\\"$uri\\\"&gt;&lt;img src=\\\"img.php?url=$genericPhotoURL\\\"&gt;&lt;\/a&gt;&lt;br&gt;\\n\";\n }\n echo \"&lt;font size=-1&gt;$fname $lname&lt;br&gt;&lt;\/font&gt;\\n\";\n echo \"&lt;font size=-2&gt;$employeeTitle&lt;br&gt;&lt;\/font&gt;\\n\";\n echo \"&lt;font size=-2&gt;$employeeOffice &amp;nbsp; &amp;nbsp; $employeeOfficePhone&lt;br&gt;&lt;\/font&gt;\\n\";\n \n if($currentColumn == $numColums) {\n echo \"&lt;\/td&gt;&lt;\/tr&gt;\";\n $currentColumn = 1; \n } else {\n echo \"&lt;\/td&gt;\";\n $currentColumn++;\n }\n\n }\n \n if($i&gt;2500) { break;} # Stop if we have way too many results. Can decrease number to debug \n } \n \n if ($currentColumn == $numColums) {\n echo \"&lt;\/table&gt;\";\n } else {\n echo \"&lt;\/tr&gt;&lt;\/table&gt;\";\n\n }\n}\n\necho \"&lt;br&gt;&lt;br&gt;Employees Found: $i&lt;br&gt;\";\n$time_end = microtime(true);\n$time = $time_end - $time_start;\necho \"Run time: \" . round($time,2) . \" s\";\n?&gt;\n\n&lt;\/html&gt;<\/pre>\n<p>Here is the img.php script that is used to crop and resize the images:<\/p>\n<pre class=\"brush: php\">asd\n&lt;?php\n\n# https:\/\/github.com\/Nimrod007\/PHP_image_resize\n# http:\/\/www.nimrodstech.com\/php-image-resize\/\n\ninclude_once('php_image_resize.php');\n\n$imgURL = $_GET[\"url\"];\n$resizeResult = smart_resize_image($imgURL,'',180,170,false,'browser',flase,false,100,false);\n\n?&gt;<\/pre>\n","protected":false},"excerpt":{"rendered":"<p>Let&#8217;s look at a more complicated example of using the Blackbaud K12 API with the &#8220;ON&#8221; 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. \u00a0There isn&#8217;t a built-in report in the Blackbaud products for this, nor is [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[3,4],"tags":[],"class_list":["post-66","post","type-post","status-publish","format-standard","hentry","category-blackbaudk12","category-php"],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/duff.io\/index.php?rest_route=\/wp\/v2\/posts\/66","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/duff.io\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/duff.io\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/duff.io\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/duff.io\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=66"}],"version-history":[{"count":1,"href":"https:\/\/duff.io\/index.php?rest_route=\/wp\/v2\/posts\/66\/revisions"}],"predecessor-version":[{"id":293,"href":"https:\/\/duff.io\/index.php?rest_route=\/wp\/v2\/posts\/66\/revisions\/293"}],"wp:attachment":[{"href":"https:\/\/duff.io\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=66"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/duff.io\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=66"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/duff.io\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=66"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}