An API for Open Educational Resources

Ned Zimmerman | @greatislander Lead Developer, Pressbooks https://pressbooks.org | @pressbooksdev

Today's session

  1. Introduction to Pressbooks (and some acronyms)
  2. A deep dive into the Pressbooks REST API
  3. Practical applications, challenges, future goals

  1. Introduction

What is a book?

/**

  • A discrete collection of text (and other media)
  • that is designed by an author (or authors)
  • as an internally complete representation of an idea or set of ideas
  • and/or an emotion or set of emotions
  • and is transmitted to readers in various formats. */

/**

  • A discrete collection of text (and other media)
  • that is designed by an author (or authors)
  • as an internally complete representation of an idea or set of ideas
  • and/or an emotion or set of emotions
  • and is transmitted to readers in various formats. */

/**

  • A discrete collection of text (and other media)
  • that is designed by an author (or authors)
  • as an internally complete representation of an idea or set of ideas
  • and/or an emotion or set of emotions
  • and is transmitted to readers in various formats. */

/**

  • A discrete collection of text (and other media)
  • that is designed by an author (or authors)
  • as an internally complete representation of an idea or set of ideas
  • and/or an emotion or set of emotions
  • and is transmitted to readers in various formats. */

/**

  • A discrete collection of text (and other media)
  • that is designed by an author (or authors)
  • as an internally complete representation of an idea or set of ideas
  • and/or an emotion or set of emotions
  • and is transmitted to readers in various formats. */

/**

  • A discrete collection of text (and other media)
  • that is designed by an author (or authors)
  • as an internally complete representation of an idea or set of ideas
  • and/or an emotion or set of emotions
  • and is transmitted to readers in various formats. */ (source: https://github.com/pressbooks/ pressbooks/blob/dev/inc/class-book.php )

What’s Pressbooks?

An open source plugin for WordPress

An open source plugin for WordPress Multisite

WordPress includes the ability to create a network of sites by using the multisite feature. — WordPress Codex

Sites become books

A (fairly) familiar interface

Five custom post types

— Front Matter (Preface, Introduction, &c.) — Parts — Chapters — Back Matter (Afterword, Appendices, &c.)

and — Book Information

Custom Theme Structure

Books on the Web (Webbooks)

https://press.rebus.community/financialstrategy/

Export Formats

— PDF for print on demand & digital distribution — EPUB 2 & 3 — Kindle (MOBI) — XHTML — HTMLBook (uno ffi cial draft) — OpenDocument (ODT) — WordPress eXtended RSS (WXR) — Common Cartridge

The First Five Years

What are Open Educational Resources (OERs)?

Teaching, learning and research materials in any medium, digital or otherwise, that reside in the public domain or have been released under an open license that permits no-cost access, use, adaptation and redistribution by others with no or limited restrictions. (source: 2012 Paris OER Declaration, UNESCO )

The 5R Activities (As defined by David Wiley.)

  1. Retain - the right to make, own, and control copies of the content (e.g., download, duplicate, store, and manage)

  1. Reuse - the right to use the content in a wide range of ways (e.g., in a class, in a study group, on a website, in a video)

  1. Revise - the right to adapt, adjust, modify, or alter the content itself (e.g., translate the content into another language)

  1. Remix - the right to combine the original or revised content with other material to create something new (e.g., incorporate the content into a mashup)

  1. Redistribute - the right to share copies of the original content, your revisions, or your remixes with others (e.g., give a copy of the content to a friend)

! This material is based on original writing by David Wiley, which was published freely under a Creative Commons Attribution 4.0 license at http://opencontent.org/definition/ .

A publisher’s job is to provide good APIs for their books. — Hugh McGuire

What is an Application Programming Interface (API)?

Representational State Transfer (REST)

  1. Deep Dive: v1 and v2 REST APIs

v1 REST API

Props to Brad Payne @ BCcampus !

v1 Endpoints: A Collection of Books — GET /api/v1/books — Gets information about a collection of books in a Pressbooks network. (Docs: https://opentextbc.ca/api/v1/docs/ )

v1 Endpoints: A Book — GET /api/v1/books/{book_id} — Gets information about a specific book. If parameters are passed, it returns information about chapters from that book. (Docs: https://opentextbc.ca/api/v1/docs/ )

v1 Book Endpoint https://press.rebus.community/api/v1/books/16

The WordPress REST API

v2 REST API

Thanks to Ryerson University and eCampus Ontario!

Key Goals

  1. Leverage the work of the WP REST API team

  1. Add a book metadata endpoint, conforming to an interoperable standard

Schema.org !

Pressbooks v1 REST API {

"pb_title" : "Moby Dick" ,

"pb_authors" : "Herman Melville" }

schema.org/Book {

"@context" : "http://schema.org" ,

"@type" : "Book" ,

"name" : "Moby Dick" ,

"author" : [ {

"@type" : "Person" ,

"name" : "Herman Melville" } ] }

  1. Support full-content Create, Read, Update, & Destroy (CRUD) operations for all custom post types

❎ Create* ✅ Read ❎ Update* ❎ Destroy* *Requires additional configuration.

Planning our Endpoints

Books: GET /wp-json/pressbooks/v2/books(/<id>)

Table of Contents: GET /wp-json/pressbooks/v2/toc Metadata: GET /wp-json/pressbooks/v2/metadata Content: — GET /wp-json/pressbooks/v2/front-matter(/<id>) — GET /wp-json/pressbooks/v2/parts(/<id>) — GET /wp-json/pressbooks/v2/chapters(/<id>) — GET /wp-json/pressbooks/v2/back-matter(/<id>)

Technical Approach

Human Made Coding Standards

File Layout

inc/api !"" endpoints

  

$"" controller

  

!"" class-books.php

  

!"" class-metadata.php

  

!"" class-posts.php

  

!"" class-revisions.php

  

!"" class-search.php

  

!"" class-sectionmetadata.php

  

!"" class-terms.php

  

$"" class-toc.php $"" namespace.php

inc/api !"" endpoints

  

$"" controller

  

!"" class-books.php

  

!"" class-metadata.php

  

!"" class-posts.php

  

!"" class-revisions.php

  

!"" class-search.php

  

!"" class-sectionmetadata.php

  

!"" class-terms.php

  

$"" class-toc.php $"" namespace.php

Controller Classes When you start go down the path of inheritance, it is important to understand that if the parent classes ever have to change at any point and your subclasses are dependent on them, you will have a major headache. — REST API Handbook

v2 Posts Endpoint

GET /wp-json/pressbooks/v2/front-matter(/<id>) GET /wp-json/pressbooks/v2/parts(/<id>) GET /wp-json/pressbooks/v2/chapters(/<id>) GET /wp-json/pressbooks/v2/back-matter(/<id>)

namespace

Pressbooks
Api
Endpoints
Controller ; class Posts extends \WP_REST_Posts_Controller {

/** * @param string $post_type Post type. */

public

function __construct ( $post_type )

{

parent ::__construct( $post_type );

$this ->namespace = 'pressbooks/v2' ;

$this ->overrideUsingFilterAndActions(); }

namespace

Pressbooks
Api
Endpoints
Controller ; class Posts extends \WP_REST_Posts_Controller {

/** * @param string $post_type Post type. */

public

function __construct ( $post_type )

{

parent ::__construct( $post_type );

$this ->namespace = 'pressbooks/v2' ;

$this ->overrideUsingFilterAndActions(); }

/**

  • Use object inheritance as little as possible to future-proof against WP API changes
  • With the exception of the abstract \WP_REST_Controller class, the WordPress API documentation
  • strongly suggests not overriding controllers. Instead we are encouraged to create an entirely separate
  • controller class for each end point.
  • Hooks, actions, and register_rest_field are fair game.

@see https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/#overview-the-future */

protected

function overrideUsingFilterAndActions ()

{

// The post type must have custom-fields support otherwise the meta fields will not appear in the REST API. add_post_type_support( $this ->post_type, 'custom-fields' ); add_filter( "rest_{$this->post_type}query" , [ $this , 'overrideQueryArgs' ] ); add_filter( "rest_prepare{$this->post_type}" , [ $this , 'overrideResponse' ], 10 , 3 ); add_filter( "rest_{$this->post_type}_trashable" , [ $this , 'overrideTrashable' ], 10 , 2 ); }

/**

  • Use object inheritance as little as possible to future-proof against WP API changes
  • With the exception of the abstract \WP_REST_Controller class, the WordPress API documentation
  • strongly suggests not overriding controllers. Instead we are encouraged to create an entirely separate
  • controller class for each end point.
  • Hooks, actions, and register_rest_field are fair game.

@see https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/#overview-the-future */

protected

function overrideUsingFilterAndActions ()

{

// The post type must have custom-fields support otherwise the meta fields will not appear in the REST API. add_post_type_support( $this ->post_type, 'custom-fields' ); add_filter( "rest_{$this->post_type}query" , [ $this , 'overrideQueryArgs' ] ); add_filter( "rest_prepare{$this->post_type}" , [ $this , 'overrideResponse' ], 10 , 3 ); add_filter( "rest_{$this->post_type}_trashable" , [ $this , 'overrideTrashable' ], 10 , 2 ); }

/**

  • Override the order the posts are displayed

@param array $args * * @return array */

public

function overrideQueryArgs ( $args )

{

// TODO: $args come from \Pressbooks\Book::getBookStructure, we should consolidate this somewhere? $args[ 'post_status' ] = 'any' ; $args[ 'orderby' ] = 'menu_order' ; $args[ 'order' ] = 'ASC' ;

return $args; }

/**

  • Override the order the posts are displayed

@param array $args * * @return array */

public

function overrideQueryArgs ( $args )

{

// TODO: $args come from \Pressbooks\Book::getBookStructure, we should consolidate this somewhere? $args[ 'post_status' ] = 'any' ; $args[ 'orderby' ] = 'menu_order' ; $args[ 'order' ] = 'ASC' ;

return $args; }

/**

  • Use object inheritance as little as possible to future-proof against WP API changes
  • With the exception of the abstract \WP_REST_Controller class, the WordPress API documentation
  • strongly suggests not overriding controllers. Instead we are encouraged to create an entirely separate
  • controller class for each end point.
  • Hooks, actions, and register_rest_field are fair game.

@see https://developer.wordpress.org/rest-api/extending-the-rest-api/controller-classes/#overview-the-future */

protected

function overrideUsingFilterAndActions ()

{

// The post type must have custom-fields support otherwise the meta fields will not appear in the REST API. add_post_type_support( $this ->post_type, 'custom-fields' ); add_filter( "rest_{$this->post_type}query" , [ $this , 'overrideQueryArgs' ] ); add_filter( "rest_prepare{$this->post_type}" , [ $this , 'overrideResponse' ], 10 , 3 ); add_filter( "rest_{$this->post_type}_trashable" , [ $this , 'overrideTrashable' ], 10 , 2 ); }

/**

  • Override the response object

@param \WP_REST_Response $response * @param \WP_Post $post * @param \WP_REST_Request $request * * @return mixed */

public

function overrideResponse ( $response, $post, $request )

{

if ( $post->post_type === 'chapter' ) {

// Add rest link to associated part $response->add_link(

'part' , trailingslashit( rest_url( sprintf( '%s/%s' , $this ->namespace, 'parts' ) ) ) . $post->post_parent ); }

if ( in_array( $post->post_type, [ 'front-matter' , 'chapter' , 'back-matter' ], true ) ) {

// Add rest link to metadata $response->add_link(

'metadata' , trailingslashit( rest_url( sprintf( '%s/%s/%d/metadata' , $this ->namespace, $this ->rest_base, $post->ID ) ) ) ); }

/**

  • Override the response object

@param \WP_REST_Response $response * @param \WP_Post $post * @param \WP_REST_Request $request * * @return mixed */

public

function overrideResponse ( $response, $post, $request )

{

if ( $post->post_type === 'chapter' ) {

// Add rest link to associated part $response->add_link(

'part' , trailingslashit( rest_url( sprintf( '%s/%s' , $this ->namespace, 'parts' ) ) ) . $post->post_parent ); }

if ( in_array( $post->post_type, [ 'front-matter' , 'chapter' , 'back-matter' ], true ) ) {

// Add rest link to metadata $response->add_link(

'metadata' , trailingslashit( rest_url( sprintf( '%s/%s/%d/metadata' , $this ->namespace, $this ->rest_base, $post->ID ) ) ) ); }

v2 Front Matter Endpoint https://press.rebus.community/financialstrategy/ wp-json/pressbooks/v2/front-matter/157

v2 Table of Contents Endpoint

inc/api !"" endpoints

  

$"" controller

  

!"" class-books.php

  

!"" class-metadata.php

  

!"" class-posts.php

  

!"" class-revisions.php

  

!"" class-search.php

  

!"" class-sectionmetadata.php

  

!"" class-terms.php

  

$"" class-toc.php $"" namespace.php

https://press.rebus.community/financialstrategy/ wp-json/pressbooks/v2/toc

v2 Metadata Endpoint

inc/api !"" endpoints

  

$"" controller

  

!"" class-books.php

  

!"" class-metadata.php

  

!"" class-posts.php

  

!"" class-revisions.php

  

!"" class-search.php

  

!"" class-sectionmetadata.php

  

!"" class-terms.php

  

$"" class-toc.php $"" namespace.php

https://press.rebus.community/financialstrategy/ wp-json/pressbooks/v2/metadata

Putting it all together

GET /wp-json/pressbooks/v2/books/{book_id}

[ {

"id" : "2" ,

"link" : "https://pressbooks.test/mobydick/" ,

"metadata" : {...},

"toc" : {

"front-matter" : [...],

"parts" : [ { ...,

"chapters" : [...] } ],

"back-matter" : [...] },

"_links" : {...} } ]

[ {

"id" : "2" ,

"link" : "https://pressbooks.test/mobydick/" ,

"metadata" : {...},

"toc" : {

"front-matter" : [...],

"parts" : [ { ...,

"chapters" : [...] } ],

"back-matter" : [...] },

"_links" : {...} } ]

[ {

"id" : "2" ,

"link" : "https://pressbooks.test/mobydick/" ,

"metadata" : {...},

"toc" : {

"front-matter" : [...],

"parts" : [ { ...,

"chapters" : [...] } ],

"back-matter" : [...] },

"_links" : {...} } ]

[ {

"id" : "2" ,

"link" : "https://pressbooks.test/mobydick/" ,

"metadata" : {...},

"toc" : {

"front-matter" : [...],

"parts" : [ { ...,

"chapters" : [...] } ],

"back-matter" : [...] },

"_links" : {...} } ]

Books Book TOC Metadata Chapters Front Matter Back Matter Parts Metadata Metadata Metadata

  1. Practical applications, challenges, and future goals

5R Activities: Reuse & Remix

Cloning!

5R Activities: Retain

Catalogs!

Challenges in working with the WordPress REST API

  1. No built-in authentication system

  1. Context switching in multisite is EXPENSIVE

Sites Endpoint: coming soon!

  1. Namespaced REST routes + Gutenberg = !

Future Plans

Enhancements to Cloning: — Attachment metadata (e.g. licenses/attribution) — Better fidelity — Support for third-party content (e.g. H5P activities)

Enhancements to Catalogs: — Improved performance — Live searching and filtering from full network collection

Content Discovery / Aggregation

Thank you!

Questions?