This document steps through the process of generating a PDF for a given set of items. The high level overview of the process is:
- User makes a request for a PDF
- Item data is loaded from API if it hasn't been already
- Filter items based on search params
- Loading view data for each item
- Taking screenshots if needed
There are two ways to request a PDF from this API:
Has the same parameters as SearchAPIParamsModel, along with a few for customizing the PDF. All of these are URL parameters. Note that optional parameters have a ? and arrays are denoted by [].
// params from SearchAPIParamsModel
itemId?: string;
gradeLevels: GradeLevels;
subjects: string[];
claims?: string[];
interactionTypes?: string[];
performanceOnly?: boolean; // either this or catOnly is required
catOnly?: boolean; // either this or performanceOnly is required
targets?: number[];
calculator?: boolean;
// other params
titlePage?: boolean; // display title page, defaults to true
scoringInfo?: boolean; // display scoring info, defaults to trueThis call is for specifying a specific list of items that should be shown in the PDF, rather than displaying all items matching search terms. Parameters go in URL (except for items array) and are as follows:
items: ItemModel[]; // items that are to be printed, goes in request body
assoc?: boolean; // print items associated with other items in list, defaults to false
scoringInfo?: boolean; // display the scoring info, defaults to trueWhere ItemModel is of the form:
interface ItemModel {
itemKey: number;
bankKey: number;
}We load the item metadata from the Sample Items Website API and cache it on the server in memory for quick access. This is done in the API Repo. We cache the following data:
- All the data in the
AboutItemModelfor each item. This includes theItemCardModel- Scoring information
- Other items associated with performance items
- A few other pieces of data we display with each item in the PDF
- List of subjects
- Filter search model (not used in PDF generation process; only for client to have parameters to filter by)
If the user made the request for the PDF using the GET request above, we filter down the items using the SearchAPIParamsModel. This is done by calling the same method as is used on the client side:
ItemSearch.filterItemCards(itemCards: ItemCardModel[], filter: SearchAPIParamsModel): ItemCardModel[];If the user made the request using the POST request above, we don't do any filtering. Instead, we only use the item IDs that are requested.
We go through for each item and make a request to get the item from Item Viewer Service API so we can inspect it to see if there are any interactive elements. We process the returned HTML by doing the following:
- Remove any links
- Remove the question number that is shown on the screen (we add our own on the PDF)
- Replace image source links with the absolute URL equivalent
- Fix multiple choice problems so that each option has the corresponding letter on the same line and is bolded
- Check if there is a passage. We do this by checking for any elements of class
thePassage. If so, add it to theItemGroupModelfor that item. Otherwise, leave theItemGroupModel.passageundefined. If there is a passage, check it to make sure there are no interactive elements (see below for more). - For each item in the group (there is only more than 1 for performance items), find an element with id
Item_<ItemKey>. We add this to theItemGroupModel.
We noticed that if there are interactive elements within an item (either in the passage or one of the questions), there will be a <span> element with the text Initializing. We use this assumption to figure out whether using the HTML as the view for a question or passage will be an accurate representation of how it would look on a test. If we find <span>Initializing</span>, there is some sort of user interaction using JavaScript that hasn't loaded yet in every case we have seen.
If there are no interactive parts for the question, we can just use the processed HTML as the view. If there are, however, we need to take a screenshot of the item instead. This process is documented below. We use Cheerio (a server-side version of jQuery with a ridiculous name) to parse the items' HTML.
If either the passage or any of the questions in an ItemGroupModel is tagged that we need to use screenshots, we need to render that part and take a screenshot. To make it as close to what it would look like as possible, we just load the item using Chrome and take the picture. We use Puppeteer to control a headless version of Chrome from its Node.js API.
We use Puppeteer to configure the page before taking the screenshot by doing the following:
- Open a new tab and load the item on Item Viewer Service
- Set the page to the page width specified in the .ENV config file (or environment variables)
- Wait until we see an element with the
groupingclass within theIFrame, then wait a little longer for the item to load (200ms) - Calculate the height of the element we're screenshotting + height of the header, then set the page's height to make sure the whole item is in the picture.
- Get the position, height, and width of the element(s) we're going to take a screenshot of. We assume the passage will be marked by class
thePassageand the questions will be in a container element with classtheQuestions. - Take the screenshot(s)
- Close the page