Recommendations blocks

What are the recommendations blocks

There are two new blocks available as part of the Recommendations engine in Totara Engage. These blocks are designed to highlight and recommend content from across your Totara site.

The Recently viewed block shows the last few items the active user has opened. This block is intended to be a quick way to get back to content the user recently discovered. The following content types are included:

  • Courses
  • Programs
  • Certifications
  • Resources
  • Surveys
  • Playlists
  • Workspaces

The Recommended for you block shows the content generated by the Recommendations engine. It has four different types to show different types of recommendations:

  • Trending: The top courses, programs, certifications, resources, surveys, playlists or workspaces based on user interaction over the last day
  • Micro-learning: Resources recommended to the active user with a time to read of less than five minutes
  • Courses: Courses recommended to the active user
  • Workspaces: Workspaces recommended to the active user

Both blocks can display results in either a compact list format or as larger tiles/cards.

How recommendation blocks work

The Recently viewed block queries the ml_recommender_interactions table from the ml/recommender plugin for any "view" events for the supported components. The supported components all trigger their own view event which implements the core_ml\event\interaction_event interface. The events are observed by the ml/recommender plugin which records the events as interaction records.

The Recommended for you block (excluding the Trending type) queries the ml_recommender_users table to find recommendations targeted at the active user. This table is populated through the recommender engine.

The Trending recommendation type queries the ml_recommender_trending table instead. This table is populated by a scheduled task (refresh_totara_trending_data_task) which reads the ml_recommender_interactions table and identifies which items are currently popular. These results are not targeted at the active user.

For both blocks, the results are then transformed into one of the supported block_totara_recently_viewed\card classes, to be passed through to the Mustache templates to render. As both blocks render content the same way, Recommended for you depends on Recently viewed to do the actual rendering.

Main classes and templates

The core of the blocks is handled by the recommender system, and then the block picks the results out and creates a collection of card objects, then feeding them into the renderers which use Mustache templates found in the Recently viewed block.

ClassPurpose
block_totara_recently_viewed
Main block class for the Recently viewed block. The get_content method will call the renderer to resolve the cards for the specific IDs. Controls when/how this block can be seen.
block_totara_recommendations
Main block class for the Recommended for you block. The get_content method will call the renderer to resolve the cards for the specific IDs. Controls when/how this block can be seen. Provides the renderer with the specific mode helper that should be used.
block_totara_recently_viewed\renderer
Queries the interaction repository for a collection of recently viewed cards, then transforms them and passes them through to the Mustache template.
block_totara_recommendations\renderer
Queries the provided mode helper for a collection of cards, then transforms them and passes them through to the Mustache template.
block_totara_recently_viewed\card
Each type of item rendered in either block must implement a class with this interface. Provides the content to the templates in a standard format.
block_totara_recently_viewed\card_resolver
Helper that searches for all of the supported card types and returns them as cards.
block_totara_recently_viewed\repository\interaction_repository
Repository class that returns a list of recently viewed items based on the view interaction in the ml_recommender_interactions table.
block_totara_recommendations\block_mode_factory
Factory class to return the specific block mode (helper to provide the recommended content). Also does access checks and will not resolve block types that are not enabled (e.g. Recommended workspaces will not be available if the Workspaces advanced feature is disabled).
block_totara_recommendations\mode\block_mode

Each type of recommended block must implement this interface. See block_totara_recommendations\mode\courses_mode for an example.

block_totara_recommendations\observer\trending_observer
Watches for deleted components. When something (such as a resource) is deleted, the trending recommendations for that resource are also deleted.
block_totara_recommendations\repository\recommendations_repository
Repository class that provides the different lookups in the  recommendations tables.
block_totara_recommendations\task\refresh_totara_trending_data_task
Scheduled task that will repopulate the content in the ml_recommender_trending table for the trending content block.


Each type of card has an individual Mustache template to render it out in both list and tile modes.

TemplatePurpose
block_totara_recently_viewed\templates\main.mustache
Main template entry point for Recently viewed. Will loop through the provided card collection and render the appropriate template.
block_totara_recommendations\templates\main.mustache
Main template entry for Recommended for you. Will loop through the provided card collection and render the appropriate templates from recently viewed.
block_totara_recently_viewed\templates\card_container_course.mustache
Template for rendering a course.
block_totara_recently_viewed\templates\card_container_workspace.mustache
Template for rendering a workspace.
block_totara_recently_viewed\templates\card_engage_article.mustache
Template for rendering a resource.
block_totara_recently_viewed\templates\card_engage_survey.mustache
Template for rendering a survey.
block_totara_recently_viewed\templates\card_totara_playlist.mustache
Template for rendering a playlist.
block_totara_recently_viewed\templates\card_totara_program.mustache
Template for rendering a program or certification.

How to support of the new type of content 

Create the view events for Recently viewed

Create a new view event that implements the core_ml\event\interaction_event interface. Make sure the interaction type is set to "view".

use core\event\base;
use core_ml\event\interaction_event;

class new_component_viewed extends base implements interaction_event {
//...
    /**
     * The event we're recording. Should be "view".
     * @return string
     */
    public function get_interaction_type(): string {
        return 'view';
    }

    /**
     * Unique frankenstyle name of the component.
     * @return string
     */
    public function get_component(): string {
        return 'new_component';
    }

    /**
     * Typically should be 1, can be used to give higher
     * weight to this interaction event.
     * @return int
     */
    public function get_rating(): int {
        return 1;
    }

    /**
     * User id who viewed the item
     * @return int
     */
    public function get_user_id(): int {
        return $this->userid;
    }

    /**
     * The id of the specific item that was viewed
     * @return int
     */
    public function get_item_id(): int {
        return $this->objectid;
    }
// ...
}


Trigger the event where the view should be counted.

$event = new_component_viewed::from_component($instance);
$event->trigger();


Now listen to the event with the recommender observer. If you've implemented the interaction_event interface, you can use the provided observer. Otherwise you'll need to write your own.

// inside your component's db/event.php file
//...
$observers = [
	[
        'eventname' => '\new_component\event\new_component_viewed',
        'callback'  => ['\ml_recommender\observer\interaction_observer\interaction_observer', 'watch_interaction'],
    ],
];


Try viewing the page and checking the ml_recommender_interactions table, the view event should be populated.

Create the interaction events for Recommended for you: Trending

To have a new component show up in the Trending content block you will need to add an interaction event for your component. View is the default, but if your component can be interacted with in different ways  (such as liked) then you can add a new event with the interaction_type set to that event. The more interaction events an item has, the more likely it'll show up in trending.

The process is the same as above for creating a view event.

// Following from above, but creating a 'like' event instead
use core\event\base;
use core_ml\event\interaction_event;

class new_component_liked extends base implements interaction_event {
//...
    /**
     * The event we're recording. Should be "like".
     * @return string
     */
    public function get_interaction_type(): string {
        return 'like';
    }

    /**
     * Unique frankenstyle name of the component.
     * @return string
     */
    public function get_component(): string {
        return 'new_component';
    }

    /**
     * Typically should be 1, can be used to give higher
     * weight to this interaction event.
     * @return int
     */
    public function get_rating(): int {
        return 1;
    }

    /**
     * User id who liked the item
     * @return int
     */
    public function get_user_id(): int {
        return $this->userid;
    }

    /**
     * The id of the specific item that was liked
     * @return int
     */
    public function get_item_id(): int {
        return $this->objectid;
    }
// ...
}


Trigger the event where the like should be counted.

$event = new_component_liked::from_component($instance);
$event->trigger();


Now listen to the event with the recommender observer. If you've implemented the interaction_event interface, you can use the provided observer. Otherwise you'll need to write your own.

// inside your component's db/event.php file
//...
$observers = [
	[
        'eventname' => '\new_component\event\new_component_liked',
        'callback'  => ['\ml_recommender\observer\interaction_observer\interaction_observer', 'watch_interaction'],
    ],
];


Try interacting with the page and checking the ml_recommender_interactions table, the defined event should be populated.

Refreshing the trending content cache

To make the new component available for trending, you'll need to edit the block_totara_recommendations\task\refresh_totara_trending_data_task around line 50 and add the component name in the filter.

//...
$components = [
    'totara_playlist',
    'engage_article',
    'engage_survey',
// Add your custom components here
    'new_component',
];

Now trigger the scheduled task again, and your component should be eligible to show in trending content. 

Create the card providers

Before a new component can be rendered on screen, the card data provider must be created. This is a mapping class that takes data from the original source and transforms it into a manner the blocks can read.

Create a new file in the blocks/totara_recently_viewed/classes/your_component folder, called card.php. If you want to place your class in another location, you'll need to overwrite the card_resolver class and teach it show to find your component.

namespace block_totara_recently_viewed\new_component;

use block_totara_recently_viewed\card as base_card;

class new_component implements base_card {
    /**
     * @var \new_component
     */
    private $my_instance;

    /**
     * @param int $id
     * @return base_card
     */
    public static function from_id(int $id): base_card {
        $card = new static();
        $card->my_instance = new_component::from_id($id);

        return $card;
    }

    /**
     * @return int
     */
    public function get_id(): int {
        return $this->my_instance->id;
    }

    /**
     * @param bool $is_dashboard
     * @return \moodle_url
     */
    public function get_url(bool $is_dashboard): moodle_url {
        // $is_dashboard indicates this block was placed on a dashboard
        return new moodle_url('/new/component/view.php', ['id' => $this->get_id()]);
    }

    /**
     * @return string|null
     */
    public function get_title(): ?string {
        return $this->my_instance->title;
    }

    /**
     * @return string|null
     */
    public function get_subtitle(): ?string {
        return null;
    }

    /**
     * @return int|null
     */
    public function get_user_id(): ?int {
        return $this->my_instance->user_id;
    }

    /**
     * @param bool $tile_view
     * @return moodle_url|null
     */
    public function get_image(bool $tile_view): ?\moodle_url {
        $image = $this->my_instance->get_image();

        return $image ? new \moodle_url($image) : null;
    }

    /**
     * Custom data to provide to the template, only for this card type.
     * @return array
     */
    public function get_extra_data(): array {
        return [
			'custom_property' => $this->my_instance->some_custom_value,
        ];
    }

    /**
     * Only return true if this type of component is part of the Engage Library pages 
     * @return bool
     */
    public function is_library_card(): bool {
        return false;
    }
}

Create the Mustache templates

Each component needs a card_component_name.mustache template in the blocks/totara_recently_viewed/templates directory. Create a new file (best is to copy one of the existing cards and modify it to suit).

Additionally an is_tile condition is provided indicating if the block is being rendered in tiled or list format. Some cards can have the same Mustache template for both, and alter the look using SCSS, while others may need to render different HTML for both styles.

Example template: Courses

The course component is a simple example. It uses the same HTML for both tile and list view, with the difference coming in from the SCSS.

<div class="block-trv-card-wrapper">
    <div class="block-trv-card-border">
        <div class="block-trv-card">
            <div class="block-trv-image-wrapper">
                <img class="block-trv-image" src="{{image}}" alt="" />
            </div>
            <div class="block-trv-content">
                <h3 class="block-trv-title"><a href="{{url}}">{{ title }}</a></h3>
                {{#extra}}
                    {{#show_progress}}
                        {{>block_totara_recently_viewed/element_progress}}
                    {{/show_progress}}
                {{/extra}}
                <div class="block-trv-footer-text">
                    {{#str}} course, block_totara_recently_viewed {{/str}}
                </div>
            </div>
        </div>
    </div>
</div>

Example template: Workspaces

Workspaces are an example of a complicated template. The tile and list views have vastly different looks, but are handled in the same place. The is_tile condition is checked multiple times to render different HTML.

<div class="block-trv-card-wrapper">
    <div class="block-trv-card-border">
        <div class="block-trv-card"{{#is_tile}} style="background-image: url('{{image}}'); background-repeat: no-repeat; background-size: cover; background-position: bottom"{{/is_tile}}>
            {{#is_tile}}
                <div class="block-trv-gradient"></div>
            {{/is_tile}}
            {{^is_tile}}
                <div class="block-trv-image-wrapper">
                    <img class="block-trv-image" src="{{image}}" />
                </div>
            {{/is_tile}}
            <div class="block-trv-content">
                <p class="block-trv-title">{{ title }}</p>
                {{^is_tile}}
                    <p class="block-trv-subtitle">{{ subtitle }}</p>
                {{/is_tile}}
                {{#is_tile}}
                    <div class="block-trv-footer-bar">
                        <div class="block-trv-footer-pretext">
                            {{ extra.members }}
                        </div>
                        <div class="block-trv-footer-divider">|</div>
                        <div class="block-trv-footer-posttext">
                            {{#str}} workspace, block_totara_recently_viewed {{/str}}
                        </div>
                    </div>
                {{/is_tile}}
                {{^is_tile}}
                    <div class="block-trv-footer-text">
                        {{#str}} workspace, block_totara_recently_viewed {{/str}}
                    </div>
                {{/is_tile}}
            </div>
        </div>
    </div>
</div>

Activate the Mustache templates

Once you've created a card_new_component.mustache template, you need to teach it to both blocks.

Edit both the blocks/totara_recently_viewed/templates/main.mustache and blocks/totara_reccommendations/templates/main.mustache files and make the same changes.

<!-- ... -->
    {{#is_totara_program}}
        {{>block_totara_recently_viewed/card_totara_program}}
    {{/is_totara_program}}
<!-- Add a new component here -->
    {{#is_new_component}}
        {{>block_totara_recently_viewed/card_new_component}}
    {{/is_new_component}}
<!-- ... -->

Finally, if needed apply any styles through CSS for your site. 

Best practice tips

Only add interaction events that make sense. If one page/component has five different interaction events, and another has only one, then the first page is more likely to be considered 'trending'.

The dashboard blocks have two styles: list and tile view. When adding a new component, make sure both are specifically supported.