read24 - Server: Student Reporting

July 8, 2020

Roger Ngo

The final "big feature" in which I will be implementing for the read24 server is the student report which is generated when the student, or teacher wishes to check the individual progress.‌

In the original Scholastic Reading Counts! progress, the student can generate a report on-demand. This report looks like those old Crystal Reports that would be generated on business software:

Report

Looking at the report, I see a few important attributes being displayed:

I would like the existing endpoint GET /student/report:studentId to retrieve similar data to be rendered by the client.

The following request:

GET /student/report/:studentId

Will obtain all the QuizToken found by the studentId which have status of Completed, and will build an object representing a report with the following approach:

The report will not include any quizzes with its QuizStatus being Incomplete, or Abandoned.

The resulting payload of a report may be shaped like:

{
    "student": { /* student data */ },
    "data": [
        {
            "passed": true,
            "book": {
                "title": "",
                "author": "",
                "wordCount": 0,
                "lexile": 0
            },
            "quiz": {
                "token": "",
                "dateCreated": 0,
                "total_questions": 10,
                "total_correct": 7
            }
        }
    ]   
}

Notice that since only Completed quizzes are being taken into consideration, the wordCount, and lexile are only counted on the student if they had completed the quiz. It is up to the client to determine whether or not, to process it accordingly if the student had passed the test, or not.

Now, let's dive into implementation.

First, I will define the types that will be needed to shape the response:

interface StudentReportQuizResult {
    token: string;
    dateCreated: number;
    totalQuestions: number;
    totalCorrect: number;
}
interface StudentReportBookItem {
    title: string;
    author: string;
    wordCount: number;
    lexile: number;
}
interface StudentReportItem {
    passed: boolean;
    book: StudentReportBookItem;
    quiz: StudentReportQuizResult;
}
interface StudentReport {
    student: StudentType;
    data: StudentReportItem[];
}

const MINIMUM_PASSING_GRADE = 0.7;

The root type is StudentReport, which contains a StudentType, and a list of StudentReportItem. The StudentReportItem is a representation of the results from the quiz session.

The route implementation is line-for-line what was described earlier when computing properties:

app.get('/student/report/:studentId', (req, res) => {
    const studentId = parseInt(req.params.studentId, 10);

    // Pull data from the database. Only process quiz sessions which have been
    // completed.
    const student = new Student().load(studentId).json() as StudentType;
    const quizTokens = QuizToken
        .listByStudentId(studentId)
        .filter(qt => qt.status === QuizStatus.Completed);

    // Build the report
    const reportItems: StudentReportItem[] = []
    for(const qt of quizTokens) {
        const book = new Book().load(qt.bookId);
        const bookItem: StudentReportBookItem = {
            title: book.title,
            author: book.author,
            wordCount: book.wordCount,
            lexile: book.lexile
        };

        const dateCreated = qt.dateCreated || 0;
        const totalQuestions = QuizQuestion.listByQuizToken(qt.token).length;
        const totalCorrect = StudentAnswer
            .listByQuizToken(qt.token)
                .map(sa => new Choice().load(sa.choiceId))
            .filter(c => c.answer).length;
        const passed = (totalCorrect / totalQuestions) >= MINIMUM_PASSING_GRADE;

        reportItems.push({
            passed,
            book: bookItem,
            quiz: {
                token: qt.token,
                dateCreated,
                totalQuestions,
                totalCorrect
            }
        });
    }

    // Send the final report
    return res.status(200).json({
        student,
        data: reportItems
    });
});

Now I will demonstrate a flow through a series of API calls which will lead to the final use case of a student checking their progress by generating a report.

  1. The student wants to search for a book. The student types in where the red fern grows, and the client sends a GET request:
GET /book/search/title/where%20the%20red%20fern%20grows

The response is returned with the matching book information.

[
    {
        "id": 1,
        "title": "Where the Red Fern Grows",
        "author": "Wilson Rawls",
        "fiction": true,
        "year": "1974",
        "publisher": "Bantam Books",
        "genre": 0,
        "isbn": "978-0-553-12338-8",
        "lexile": 700,
        "wordCount": 75528
    }
]
  1. The student decides on a book to take a quiz. The client makes a POST request.
POST /quiz/book
{
    bookId: 1,
    studentId: 1
}

What is then returned is a quiz token along with the set of questions, choices for the quiz within the session context:

{
    "token": "649d890f-edf8-4553-adfd-733d8b81f644",
    "quiz": [
        {
            "question": {
                "id": 1,
                "bookId": 1,
                "content": "WTRFG - First question"
            },
            "choices": [
                {
                    "id": 1,
                    "questionId": 1,
                    "content": "First choice",
                    "answer": true
                },
                {
                    "id": 2,
                    "questionId": 1,
                    "content": "Second choice",
                    "answer": false
                },
                {
                    "id": 3,
                    "questionId": 1,
                    "content": "Third choice",
                    "answer": false
                },
                {
                    "id": 4,
                    "questionId": 1,
                    "content": "Fourth choice",
                    "answer": false
                }
            ]
        },
        {
            "question": {
                "id": 2,
                "bookId": 1,
                "content": "WTRFG - Second question"
            },
            "choices": [
                {
                    "id": 5,
                    "questionId": 2,
                    "content": "First choice",
                    "answer": false
                },
                {
                    "id": 6,
                    "questionId": 2,
                    "content": "Second choice",
                    "answer": false
                },
                {
                    "id": 7,
                    "questionId": 2,
                    "content": "Third choice",
                    "answer": false
                },
                {
                    "id": 8,
                    "questionId": 2,
                    "content": "Fourth choice",
                    "answer": true
                }
            ]
        },
        {
            "question": {
                "id": 3,
                "bookId": 1,
                "content": "WTRFG - Third question"
            },
            "choices": [
                {
                    "id": 9,
                    "questionId": 3,
                    "content": "First choice",
                    "answer": false
                },
                {
                    "id": 10,
                    "questionId": 3,
                    "content": "Second choice",
                    "answer": false
                },
                {
                    "id": 11,
                    "questionId": 3,
                    "content": "Third choice",
                    "answer": true
                },
                {
                    "id": 12,
                    "questionId": 3,
                    "content": "Fourth choice",
                    "answer": false
                }
            ]
        },
        {
            "question": {
                "id": 4,
                "bookId": 1,
                "content": "WTRFG - Fourth question"
            },
            "choices": [
                {
                    "id": 13,
                    "questionId": 4,
                    "content": "First choice",
                    "answer": false
                },
                {
                    "id": 14,
                    "questionId": 4,
                    "content": "Second choice",
                    "answer": false
                },
                {
                    "id": 15,
                    "questionId": 4,
                    "content": "Third choice",
                    "answer": true
                },
                {
                    "id": 16,
                    "questionId": 4,
                    "content": "Fourth choice",
                    "answer": false
                }
            ]
        },
        {
            "question": {
                "id": 5,
                "bookId": 1,
                "content": "WTRFG - Fifth question"
            },
            "choices": [
                {
                    "id": 17,
                    "questionId": 5,
                    "content": "First choice",
                    "answer": false
                },
                {
                    "id": 18,
                    "questionId": 5,
                    "content": "Second choice",
                    "answer": false
                },
                {
                    "id": 19,
                    "questionId": 5,
                    "content": "Third choice",
                    "answer": true
                },
                {
                    "id": 20,
                    "questionId": 5,
                    "content": "Fourth choice",
                    "answer": false
                }
            ]
        },
        {
            "question": {
                "id": 6,
                "bookId": 1,
                "content": "WTRFG - Sixth question"
            },
            "choices": [
                {
                    "id": 21,
                    "questionId": 6,
                    "content": "First choice",
                    "answer": false
                },
                {
                    "id": 22,
                    "questionId": 6,
                    "content": "Second choice",
                    "answer": false
                },
                {
                    "id": 23,
                    "questionId": 6,
                    "content": "Third choice",
                    "answer": true
                },
                {
                    "id": 24,
                    "questionId": 6,
                    "content": "Fourth choice",
                    "answer": false
                }
            ]
        },
        {
            "question": {
                "id": 7,
                "bookId": 1,
                "content": "WTRFG - Seventh question"
            },
            "choices": [
                {
                    "id": 25,
                    "questionId": 7,
                    "content": "First choice",
                    "answer": false
                },
                {
                    "id": 26,
                    "questionId": 7,
                    "content": "Second choice",
                    "answer": false
                },
                {
                    "id": 27,
                    "questionId": 7,
                    "content": "Third choice",
                    "answer": true
                },
                {
                    "id": 28,
                    "questionId": 7,
                    "content": "Fourth choice",
                    "answer": false
                }
            ]
        },
        {
            "question": {
                "id": 8,
                "bookId": 1,
                "content": "WTRFG - Eighth question"
            },
            "choices": [
                {
                    "id": 29,
                    "questionId": 8,
                    "content": "First choice",
                    "answer": false
                },
                {
                    "id": 30,
                    "questionId": 8,
                    "content": "Second choice",
                    "answer": false
                },
                {
                    "id": 31,
                    "questionId": 8,
                    "content": "Third choice",
                    "answer": true
                },
                {
                    "id": 32,
                    "questionId": 8,
                    "content": "Fourth choice",
                    "answer": false
                }
            ]
        },
        {
            "question": {
                "id": 9,
                "bookId": 1,
                "content": "WTRFG - Nineth question"
            },
            "choices": [
                {
                    "id": 33,
                    "questionId": 9,
                    "content": "First choice",
                    "answer": false
                },
                {
                    "id": 34,
                    "questionId": 9,
                    "content": "Second choice",
                    "answer": false
                },
                {
                    "id": 35,
                    "questionId": 9,
                    "content": "Third choice",
                    "answer": true
                },
                {
                    "id": 36,
                    "questionId": 9,
                    "content": "Fourth choice",
                    "answer": false
                }
            ]
        },
        {
            "question": {
                "id": 10,
                "bookId": 1,
                "content": "WTRFG - Tenth question"
            },
            "choices": [
                {
                    "id": 37,
                    "questionId": 10,
                    "content": "First choice",
                    "answer": false
                },
                {
                    "id": 38,
                    "questionId": 10,
                    "content": "Second choice",
                    "answer": false
                },
                {
                    "id": 39,
                    "questionId": 10,
                    "content": "Third choice",
                    "answer": true
                },
                {
                    "id": 40,
                    "questionId": 10,
                    "content": "Fourth choice",
                    "answer": false
                }
            ]
        }
    ]
}

Notice the quiz token is 649d890f-edf8-4553-adfd-733d8b81f644 , and will be used for the subsequent calls.

  1. The student takes the quiz by answering the question. Each question makes a POST request which looks like this:
POST /quiz/book/question
{
    quizToken: "649d890f-edf8-4553-adfd-733d8b81f644",
    choiceId: 1
}

Returned, is the serialized StudentAnswer object written into the database:

{
    "quizToken": "649d890f-edf8-4553-adfd-733d8b81f644",
    "studentId": 1,
    "questionId": 1,
    "choiceId": 1
}

The student will typically repeat this 9 more times -- eventually completing the quiz.

  1. When the student completes the quiz, they have the opportunity to rate the book.
POST /quiz/book/rate
{
    "quizToken": "649d890f-edf8-4553-adfd-733d8b81f644",
    "rating": 4
}

  1. Finally the student checks their progress by invoking a GET request.
GET /student/report/1

The following report data is returned, and is up to the client to render accordingly.

{
    "student": {
        "id": 1,
        "classroomId": 1,
        "firstName": "Stu",
        "middleName": "",
        "lastName": "Dent",
        "grade": 5,
        "userId": 1
    },
    "data": [
        {
            "passed": true,
            "book": {
                "title": "Where the Red Fern Grows",
                "author": "Wilson Rawls",
                "wordCount": 75528,
                "lexile": 700
            },
            "quiz": {
                "token": "649d890f-edf8-4553-adfd-733d8b81f644",
                "dateCreated": 1594211157338,
                "totalQuestions": 10,
                "totalCorrect": 8
            }
        }
    ]
}

So now we have most of the basic API implemented. in the next session, I will revisit the data layer again, and attempt to build out a generic connector that abstracts read/write operations to the data store.