If you're running LMS Classes in Rock, you've probably wanted a way to show someone their own class progress right on their profile or My Account page: which classes they're enrolled in, which ones they've finished, and a quick link back into each one. The Group List Personalized Lava block already does the heavy lifting of finding "all the groups this person belongs to," so it's a natural fit for LMS Classes too, since a Class is really just a Group under the hood.

Once you go looking for each class's completion status, though, it's easy to fall into a classic Lava trap.

The Temptation:

The block hands your Lava template a Groups collection, one entry per class the person belongs to. Your first instinct might be to loop through that collection and, for each class, run a {% raw %}{% learningparticipant %}{% endraw %} entity command to look up that person's completion status for that one class:

{% raw %}{% for group in Groups %} {% learningparticipant where:'LearningClassId == {{ group.Group.Id }} && PersonId == {{ CurrentPerson.Id }}' %} ... {% endlearningparticipant %} {% endfor %}{% endraw %}

That works, but it means one database query per class. Someone enrolled in ten classes just cost you ten round trips to the database, on every single page load of My Account. Entity commands aren't free, and running one inside a loop is a good way to quietly turn a simple sidebar block into the slowest thing on the page.

The Alternative: One Combined Where Clause

Instead of querying once per class, build a single where clause that covers every class the person belongs to, joined with || (or), and run the entity command exactly once. Then match each class back up to its result in memory.

Here's how to set it up:

  1. Configure the block. Add a Group List Personalized Lava block to a sidebar zone (My Account, in this case). Set Include Group Types (or Parent Group) so it only returns the LMS Class group type. This block is generic to any group type, so scoping it is what makes it safe to assume every entry in Groups is really a class.
  2. Build one where clause across every class. Loop through Groups once, appending LearningClassId == {id} for each class and joining them with ||:

    {% raw %}{% lava assign lpWhere = '' for group in Groups if forloop.first == false assign lpWhere = lpWhere | Append:' || ' endif assign lpWhere = lpWhere | Append:'LearningClassId == ' | Append:group.Group.Id endfor %}{% endraw %}

    For someone in three classes, that produces something like LearningClassId == 12 || LearningClassId == 14 || LearningClassId == 19: one clause, not three separate lookups.

  3. Run the entity command once. Pass that combined clause into a single {% raw %}{% learningparticipant %}{% endraw %} command, further filtered to the current person, and select everything you'll need downstream in one pass:

    {% raw %}{% learningparticipant where:'{{ lpWhere }}' expression:'PersonId == {{ CurrentPerson.Id }}' select:' new ( LearningClassId AS ClassId , LearningCompletionStatus AS CompletionStatus , LearningCompletionDateTime AS CompletionDateTime , LearningClass.LearningCourseId AS CourseId , LearningClass.LearningCourse.LearningProgramId AS ProgramId)' iterator:'participants' securityenabled:'false' %} {% endlearningparticipant %}{% endraw %}

    That's the entire database read for completion status. Everything after this works with data already sitting in memory.

  4. Match each class back to its result and build a display-ready dictionary. Loop through Groups a second time (no database calls this time), find the matching participants row for that class, and translate the raw CompletionStatus value into something readable:

    {% raw %}{% lava for group in Groups assign participant = participants | Where:'ClassId',group.Group.Id | OrderBy:'CompletionStatus desc' | First assign completionStatusValue = participant.CompletionStatus if completionStatusValue == 1 assign completionStatus = 'Failed' elseif completionStatusValue == 2 assign completionStatus = 'Passed' else assign completionStatus = 'Enrolled' endif ... endfor %}{% endraw %}

    Along the way, build a learner-facing URL for each class using ToIdHash on the Program, Course, and Class Ids, since a Class is a Group extended by LearningClass, which links up to LearningCourse and LearningProgram:

    {% raw %}{{ 'learn/' | Append:programIdHash | Append:'/courses/' | Append:courseIdHash | Append:'/' | Append:classIdHash }}{% endraw %}

  5. Split into panels and render. Filter the finished array down to Enrolled and Passed, and drop each into its own Bootstrap panel:

    {% raw %}{% if enrolledCount > 0 %} <div class="panel panel-default"> <div class="panel-heading">Enrolled Classes</div> <ul class="list-group list-group-panel"> {% for class in enrolledClasses %} <li class="list-group-item"> <a href="{{ publicApplicationRoot | Append:class.ClassUrl }}">{{ class.ClassName }}</a> </li> {% endfor %} </ul> </div> {% endif %}{% endraw %}

MyAccountClassListSidebar.png

[Screenshot: rendered Enrolled Classes / Completed Classes panels on the My Account sidebar.]

Why This Matters

This pattern isn't specific to LMS. Any time you're about to put an entity command inside a {% raw %}{% for %}{% endraw %} loop, stop and flip it around: build one combined where clause first, run the entity command once outside the loop, then loop again over data you already have in memory. One query beats N queries every time, and your sidebar block stays a sidebar block instead of becoming the reason My Account feels slow.

Two Things to Confirm Before You Ship This

  1. Failed classes. As written, a class with a Failed completion status won't show up in either panel, only Enrolled and Passed render. Decide whether failed classes need their own panel or a spot in Completed, and add it before this goes live.
  2. Rock version. This recipe depends on behavior available in Rock v18.2 and later. On an earlier version, apply the manual SQL update described in SparkDevNetwork/Rock issue #6601 first, or the learningparticipant entity command won't behave as shown above.

Lava Template

{% assign groupCount = Groups | Size %}

{% if groupCount > 0 %}
    /- 
    Use a for loop to build a where clause for the LearningParticipant entity command to get all of the Current Person's LearningParticipant records in one database read.
    -/
    {% lava
    assign lpWhere = ''
    for group in Groups
        if forloop.first == false
            assign lpWhere = lpWhere | Append:' || '
        endif
        assign lpWhere = lpWhere | Append:'LearningClassId == ' | Append:group.Group.Id
    endfor
    %}

    {% if lpWhere and lpWhere != empty %}
        {% learningparticipant
            where:'{{ lpWhere }}'
            expression:'PersonId == {{ CurrentPerson.Id }}'
            select:'
                new (
                    LearningClassId AS ClassId
                    , LearningCompletionStatus AS CompletionStatus
                    , LearningCompletionDateTime AS CompletionDateTime
                    , LearningClass.LearningCourseId AS CourseId
                    , LearningClass.LearningCourse.LearningProgramId AS ProgramId)'
            iterator:'participants'
            securityenabled:'false'
        %}
        {% endlearningparticipant %}
    {% endif %}

    /- 
    Loop through the LMS Class groups and match up the Current Person's LearningParticipant  to get the completion status and completion date/time. Build an array of dictionaries to hold the class name, completion status, completion date/time, and class URL for each class.
    -/
    {% lava
    for group in Groups
        assign classStatus = null
        assign participant = participants | Where:'ClassId',group.Group.Id | OrderBy:'CompletionStatus desc' | First
        assign classStatus = classStatus | AddToDictionary:'ClassId',group.Group.Id
        assign classStatus = classStatus | AddToDictionary:'ClassName',group.Group.Name
        assign completionStatusValue = participant.CompletionStatus
        if completionStatusValue == 1
            assign completionStatus = 'Failed'
        elseif completionStatusValue == 2
            assign completionStatus = 'Passed'
        else
            assign completionStatus = 'Enrolled'
        endif
        assign classStatus = classStatus | AddToDictionary:'CompletionStatus',completionStatus
        assign classStatus = classStatus | AddToDictionary:'CompletionDateTime',participant.CompletionDateTime
        assign programIdHash = participant.ProgramId | ToIdHash
        assign courseIdHash = participant.CourseId | ToIdHash
        assign classIdHash = group.Group.Id | ToIdHash
        assign classUrl = 'learn/' | Append:programIdHash | Append:'/courses/' | Append:courseIdHash | Append:'/' | Append:classIdHash
        assign classStatus = classStatus | AddToDictionary:'ClassUrl',classUrl
        assign classesProgress = classesProgress | AddToArray:classStatus
    endfor

    assign classesProgress = classesProgress | OrderBy:'CompletionDateTime desc'
    assign enrolledClasses = classesProgress | Where:'CompletionStatus','Enrolled'
    assign enrolledCount = enrolledClasses | Size
    assign completedClasses = classesProgress | Where:'CompletionStatus','Passed'
    assign completedCount = completedClasses | Size
    assign publicApplicationRoot = 'Global' | Attribute:'PublicApplicationRoot'
    %}

    {% if enrolledCount > 0 %}
        <div class="panel panel-default">
            <div class="panel-heading">
                Enrolled Classes
            </div>
            <ul class="list-group list-group-panel">
                {% for class in enrolledClasses %}
                    <li class="list-group-item">
                        <a href="{{ publicApplicationRoot | Append:class.ClassUrl }}">
                            {{ class.ClassName }}
                        </a>
                    </li>
                {% endfor %}
            </ul>
        </div>
    {% endif %}

    {% if completedCount > 0 %}
        <div class="panel panel-default">
            <div class="panel-heading">
                Completed Classes
            </div>
            <ul class="list-group list-group-panel">
                {% for class in completedClasses %}
                    <li class="list-group-item">
                        <a href="{{ publicApplicationRoot | Append:class.ClassUrl }}">
                            {{ class.ClassName }}
                        </a>
                    </li>
                {% endfor %}
            </ul>
        </div>
    {% endif %}

{% endif %}