read24 - Server: Managing Quizzes

July 25, 2020

Roger Ngo

In my opinion, administrating quizzes will be the most fun part as a user to read24. A teacher may go into the system to load more books, and quizzes for the students in their classroom to take. The teacher may also be able to edit questions, and their choices at a later time.

With the API I have also defined, it should be easy to "bulk" upload book, and quiz information too.

Today, I'll be implementing the routes to:

I'll start off by implementing the POST request to create books, and the questions for quizzes. For both the create, and update scenarios, the request body may look something like this:

/**
 * Request Body
 * {
 *     book: {
 *         title,
 *         fiction,
 *         author,
 *         year,
 *         publisher,
 *         genre,
 *         isbn,
 *         lexile,
 *         wordCount
 *     },
 *     questions: [
 *         {
 *             content,
 *             choices: [
 *                 {
 *                     content,
 *                     answer
 *                 }
 *             ]
 *         }
 *     ]
 * 
 * }
 */

Where book is the book information which mostly resembles BookType, and questions is an array of objects containing the content of the question, and the corresponding choices -- which in turn is an array itself for each of these objects.‌

Then I may implement POST /admin/quiz/import as:

  1. Receive the request body as payload
  2. Create the Book resource with the information from req.body.book.
  3. Once the ID of the Book is obtained after writing to the database, then I can create the individual Question resources along with the Choice objects they are mapped to.

The implementation is quite simple, and looks like this:

app.post('/admin/quiz/import', async (req, res) => {        
    const {
        book,
        questions
    } = req.body;

    const b = await new Book({
        title: book.title,
        fiction: book.fiction === 'true' ? true : false,
        author: book.author,
        year: book.year,
        publisher: book.publisher,
        genre: parseInt(book.genre),
        isbn: book.isbn,
        lexile: parseInt(book.lexile),
        wordCount: parseInt(book.wordCount)
    }).insert();

    for (const q of questions) {
        const content = q.content;
        const bookId = b.id;

        const question = await new Question({
            bookId,
            content
        }).insert();

        for(const c of q.choices) {
            await new Choice({
                questionId: question.id,
                content: c.content,
                answer: c.answer === 'true' ? true : false
            }).insert();
        }
    }

    return res.status(200).json({message: 'Quiz has been created.'});
 });

Then the following request body can be represented as JSON to create the book, and quiz into the database:

{
    "book": {
        "title": "Clifford the Big Red Dog",
        "fiction": "true",
        "year": "1985",
        "author": "Norman Bridwell",
        "publisher": "Scholastic, Inc",
        "genre": 1,
        "isbn": "0-590-44297-X",
        "lexile": 370,
        "wordCount": 236
    },
    "questions": [
        {
            "content": "Who is Emily Elizabeth?",
            "choices": [
                {
                    "content": "She is a girl who has a big red dog.",
                    "answer": "true"
                },
                {
                    "content": "She is a big red dog",
                    "answer": "false"
                },
                {
                    "content": "She is Clifford's mother.",
                    "answer": "false"
                }
            ]
        },
        {
            "content": "Emily Elizabeth has a dog, but her dog is...",
            "choices": [
                {
                    "content": "A big dog.",
                    "answer": "false"
                },
                {
                    "content": "A little dog.",
                    "answer": "false"
                },
                {
                    "content": "The biggest, and reddest dog on the street.",
                    "answer": "true"
                }
            ]
        },
        {
            "content": "What games do Emily, and Clifford play with each other?",
            "choices": [
                {
                    "content": "Hide-and-seek",
                    "answer": "true"
                },
                {
                    "content": "Basketball",
                    "answer": "false"
                },
                {
                    "content": "Checkers",
                    "answer": "false"
                }
            ]
        },
        {
            "content": "What kind of tricks does Clifford know?",
            "choices": [
                {
                    "content": "Speaking",
                    "answer": "false"
                },
                {
                    "content": "Sit up and beg",
                    "answer": "true"
                },
                {
                    "content": "Rolling over",
                    "answer": "false"
                }
            ]
        }
    ]
}

This type of JSON format is handy pre-loading data, and sharing books from classroom to classroom. One teacher may give another teacher a "book" through sending this JSON.

Now, for the PUT request to be treated as an update operation for the book, and its quiz information, it is required that any entity to be updated has included the id field in the JSON object. Objects in the JSON file which do not have the id field will be assumed to be a new insertion to the database regardless of duplicate content. ‌

So, here is how the typical update request body will look like:

{
    "book": {
        "id": 2,
        "title": "Clifford the Big Red Dog",
        "fiction": "true",
        "year": "1985",
        "author": "Norman Bridwell",
        "publisher": "Scholastic, Inc",
        "genre": 2,
        "isbn": "0-590-44297-X",
        "lexile": 370,
        "wordCount": 236
    },
    "questions": [
        {
            "id": 11,
            "content": "Who is Emily Elizabeth?",
            "choices": [
                {
                    "id": 41,
                    "content": "She is a girl who has a big red dog.",
                    "answer": "true"
                },
                {
                    "id": 42,    
                    "content": "She is a big red dog",
                    "answer": "false"
                },
                {
                    "id": 43,
                    "content": "She is Clifford's mother.",
                    "answer": "false"
                }
            ]
        },
        {
            "id": 12,
            "content": "Emily Elizabeth has a dog, but her dog is...",
            "choices": [
                {
                    "id": 44,
                    "content": "A big dog.",
                    "answer": "false"
                },
                {
                    "id": 45,
                    "content": "A little dog.",
                    "answer": "false"
                },
                {
                    "id": 46,
                    "content": "The biggest, and reddest dog on the street.",
                    "answer": "true"
                }
            ]
        },
        {
            "id": 13,
            "content": "What games do Emily, and Clifford play with each other?",
            "choices": [
                {
                    "id": 47,
                    "content": "Hide-and-seek",
                    "answer": "true"
                },
                {
                    "id": 48,
                    "content": "Basketball",
                    "answer": "false"
                },
                {
                    "id": 49,
                    "content": "Checkers",
                    "answer": "false"
                }
            ]
        },
        {
            "id": 14,
            "content": "What kind of tricks does Clifford know?",
            "choices": [
                {
                    "id": 50,
                    "content": "Speaking",
                    "answer": "false"
                },
                {
                    "id": 51,
                    "content": "Sit up and beg",
                    "answer": "true"
                },
                {
                    "id": 52,
                    "content": "Rolling over",
                    "answer": "false"
                }
            ]
        },
        {
            "content": "Which is a bad habit of Clifford?",
            "choices": [
                {
                    "content": "Sleeping too much",
                    "answer": "false"
                },
                {
                    "content": "Running after cars",
                    "answer": "true"
                },
                {
                    "content": "Being too loud",
                    "answer": "false"
                }
            ]
        }
    ]
}

As you can see, the above payload will attempt to update the book, and all existing questions, but only create a single question with 3 new choices: Which is a bad habit of Clifford?

Writing this code is again, pretty simple. The assumption is that the ID of the book must first be available. Once it is, then the book is loaded, and its properties will be updated.

Following that, all the questions will be loaded, or created and so the same with Choices.

app.put('/admin/quiz/import', async (req, res) => {
     const {
         book,
         questions
     } = req.body;

     if (!book.id)
        return res.status(400).json({message: 'Bad request. No book.id provided.'});

    const b = await new Book().load(parseInt(book.id));
    b.title = book.title;
    b.fiction = book.fiction === 'true' ? true : false;
    b.author = book.author;
    b.year = book.year;
    b.publisher = book.publisher;
    b.genre = parseInt(book.genre);
    b.isbn = book.isbn;
    b.lexile = parseInt(book.lexile);
    b.wordCount = parseInt(book.wordCount);

    await b.update();

    for(const q of questions) {
        const bookId = b.id;
        const content = q.content;

        let question = null;
        if (q.id) {
            const questionId = parseInt(q.id);

            question = await new Question().load(questionId);
            question.content = content;

            await question.update();
        } else {
            question = await new Question({
                bookId,
                content
            }).insert();
        }

        for(const c of q.choices) {
            const content = c.content;
            const answer = c.answer === 'true' ? true : false;
            let choice = null;
            if (c.id) {
                const choiceId = parseInt(c.id);

                choice = await new Choice().load(choiceId);
                choice.content = content;
                choice.answer = answer;

                await choice.update();
            } else {
                choice = await new Choice({
                    questionId: question.id,
                    content: c.content,
                    answer: c.answer === 'true' ? true : false
                }).insert();
            }
        }
    }
    
     return res.status(200).json({});
 });

The final endpoint to be implemented is the GET request to retrieve the book, and quiz information. This one is mostly the reverse of the POST method. The format of the response must be consistent with what is received from the POST, and PUT request.

 app.get('/admin/quiz/book/:bookId', async (req, res) => {
    const bookId = req.params.bookId;

    const b = await new Book().load(parseInt(bookId));

    const book = {...b.json(), id: b.id};
    const questions = [];

    const qs = await Question.listByBookId(b.id);
    for(const q of qs) {
        const curr = {
            id: q.id,
            content: null,
            choices: []
        };

        curr.content = q.content;

        const choices = await Choice.listByQuestionId(q.id);
        for(const c of choices) {
            curr.choices.push({
                id: c.id,
                content: c.content,
                answer: c.answer ? 'true' : 'false'
            });
        }

        questions.push(curr);
    }

    return res.status(200).json({book, questions});
 });

Calling this request will give the following response:

{
    "book": {
        "id": 2,
        "title": "Clifford the Big Red Dog",
        "fiction": 1,
        "author": "Norman Bridwell",
        "year": "1985",
        "publisher": "Scholastic, Inc",
        "genre": 2,
        "isbn": "0-590-44297-X",
        "lexile": 370,
        "wordCount": 236
    },
    "questions": [
        {
            "id": 11,
            "content": "Who is Emily Elizabeth?",
            "choices": [
                {
                    "id": 41,
                    "content": "She is a girl who has a big red dog.",
                    "answer": "true"
                },
                {
                    "id": 42,
                    "content": "She is a big red dog",
                    "answer": "false"
                },
                {
                    "id": 43,
                    "content": "She is Clifford's mother.",
                    "answer": "false"
                }
            ]
        },
        {
            "id": 12,
            "content": "Emily Elizabeth has a dog, but her dog is...",
            "choices": [
                {
                    "id": 44,
                    "content": "A big dog.",
                    "answer": "false"
                },
                {
                    "id": 45,
                    "content": "A little dog.",
                    "answer": "false"
                },
                {
                    "id": 46,
                    "content": "The biggest, and reddest dog on the street.",
                    "answer": "true"
                }
            ]
        },
        {
            "id": 13,
            "content": "What games do Emily, and Clifford play with each other?",
            "choices": [
                {
                    "id": 47,
                    "content": "Hide-and-seek",
                    "answer": "true"
                },
                {
                    "id": 48,
                    "content": "Basketball",
                    "answer": "false"
                },
                {
                    "id": 49,
                    "content": "Checkers",
                    "answer": "false"
                }
            ]
        },
        {
            "id": 14,
            "content": "What kind of tricks does Clifford know?",
            "choices": [
                {
                    "id": 50,
                    "content": "Speaking",
                    "answer": "false"
                },
                {
                    "id": 51,
                    "content": "Sit up and beg",
                    "answer": "true"
                },
                {
                    "id": 52,
                    "content": "Rolling over",
                    "answer": "false"
                }
            ]
        },
        {
            "id": 15,
            "content": "Which is a bad habit of Clifford?",
            "choices": [
                {
                    "id": 53,
                    "content": "Sleeping too much",
                    "answer": "false"
                },
                {
                    "id": 54,
                    "content": "Running after cars",
                    "answer": "true"
                },
                {
                    "id": 55,
                    "content": "Being too loud",
                    "answer": "false"
                }
            ]
        }
    ]
}

Next, I will work on the DELETE implementation. I will save it for next time since the change is going to affect some database-related functionality.