diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 550ff59cec24951220335bc0cd1aee4e972f7945..2718f5f24e828c1d3b57a8ca8f66f986bf2a068b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -18,6 +18,7 @@ build:
   stage: build
   image: docker.io/python:3.9.7-alpine
   before_script:
+    - apk add --no-cache git
     - pip install pipenv==${PIPENV_VERSION}
     - unset PIPENV_VERSION
     - pipenv sync
diff --git a/Pipfile b/Pipfile
index 430cac11c0c08d1feedc773c85b28cd73925cacb..7f9073d8a92734e856dea4927b5a2acf91693bc0 100644
--- a/Pipfile
+++ b/Pipfile
@@ -5,6 +5,8 @@ name = "pypi"
 
 [packages]
 mkdocs = "~=1.2.3"
+mkdocs-material = "~=7.3.6"
+mkdocs-git-revision-date-localized-plugin = "~=0.10.2"
 
 [dev-packages]
 
diff --git a/Pipfile.lock b/Pipfile.lock
index 2c986f693734baf07614c19e894c82348521be29..894f2f0393b7792f225a212eb890720c4724152f 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -1,7 +1,7 @@
 {
     "_meta": {
         "hash": {
-            "sha256": "73260d73b196eb25c7a8f2eaecfb6af799f8f98ca2a2d5f95c1286f46e2cfba9"
+            "sha256": "eb241c9baf0e485ff9d3998f6c63add07915fc484b2205252e93222c8edc2422"
         },
         "pipfile-spec": 6,
         "requires": {
@@ -16,6 +16,14 @@
         ]
     },
     "default": {
+        "babel": {
+            "hashes": [
+                "sha256:ab49e12b91d937cd11f0b67cb259a57ab4ad2b59ac7a3b41d6c06c0ac5b0def9",
+                "sha256:bc0c176f9f6a994582230df350aa6e05ba2ebe4b3ac317eab29d9be5d2768da0"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.9.1"
+        },
         "click": {
             "hashes": [
                 "sha256:353f466495adaeb40b6b5f592f9f91cb22372351c84caeb068132442a4518ef3",
@@ -39,21 +47,37 @@
             ],
             "version": "==2.0.2"
         },
+        "gitdb": {
+            "hashes": [
+                "sha256:8033ad4e853066ba6ca92050b9df2f89301b8fc8bf7e9324d412a63f8bf1a8fd",
+                "sha256:bac2fd45c0a1c9cf619e63a90d62bdc63892ef92387424b855792a6cabe789aa"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==4.0.9"
+        },
+        "gitpython": {
+            "hashes": [
+                "sha256:dc0a7f2f697657acc8d7f89033e8b1ea94dd90356b2983bca89dc8d2ab3cc647",
+                "sha256:df83fdf5e684fef7c6ee2c02fc68a5ceb7e7e759d08b694088d0cacb4eba59e5"
+            ],
+            "markers": "python_version >= '3.7'",
+            "version": "==3.1.24"
+        },
         "importlib-metadata": {
             "hashes": [
-                "sha256:b618b6d2d5ffa2f16add5697cf57a46c76a56229b0ed1c438322e4e95645bd15",
-                "sha256:f284b3e11256ad1e5d03ab86bb2ccd6f5339688ff17a4d797a0fe7df326f23b1"
+                "sha256:53ccfd5c134223e497627b9815d5030edf77d2ed573922f7a0b8f8bb81a1c100",
+                "sha256:75bdec14c397f528724c1bfd9709d660b33a4d2e77387a3358f20b848bb5e5fb"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==4.8.1"
+            "version": "==4.8.2"
         },
         "jinja2": {
             "hashes": [
-                "sha256:827a0e32839ab1600d4eb1c4c33ec5a8edfbc5cb42dafa13b81f182f97784b45",
-                "sha256:8569982d3f0889eed11dd620c706d39b60c36d6d25843961f33f77fb6bc6b20c"
+                "sha256:077ce6014f7b40d03b47d1f1ca4b0fc8328a692bd284016f806ed0eaca390ad8",
+                "sha256:611bb273cd68f3b993fabdc4064fc858c5b47a973cb5aa7999ec1ba405c87cd7"
             ],
             "markers": "python_version >= '3.6'",
-            "version": "==3.0.2"
+            "version": "==3.0.3"
         },
         "markdown": {
             "hashes": [
@@ -154,6 +178,30 @@
             "index": "pypi",
             "version": "==1.2.3"
         },
+        "mkdocs-git-revision-date-localized-plugin": {
+            "hashes": [
+                "sha256:16ffc10407d5e84f0e5248496191d065d611095a12a48cb5070167b4808ae06a",
+                "sha256:817dd2c897ede4f801673898d454191b280cf0b21650de82b2144e4774b4cd2a"
+            ],
+            "index": "pypi",
+            "version": "==0.10.2"
+        },
+        "mkdocs-material": {
+            "hashes": [
+                "sha256:1b1dbd8ef2508b358d93af55a5c5db3f141c95667fad802301ec621c40c7c217",
+                "sha256:1b6b3e9e09f922c2d7f1160fe15c8f43d4adc0d6fb81aa6ff0cbc7ef5b78ec75"
+            ],
+            "index": "pypi",
+            "version": "==7.3.6"
+        },
+        "mkdocs-material-extensions": {
+            "hashes": [
+                "sha256:a82b70e533ce060b2a5d9eb2bc2e1be201cf61f901f93704b4acf6e3d5983a44",
+                "sha256:bfd24dfdef7b41c312ede42648f9eb83476ea168ec163b613f9abd12bbfddba2"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==1.0.3"
+        },
         "packaging": {
             "hashes": [
                 "sha256:096d689d78ca690e4cd8a89568ba06d07ca097e3306a4381635073ca91479966",
@@ -162,12 +210,28 @@
             "markers": "python_version >= '3.6'",
             "version": "==21.2"
         },
+        "pygments": {
+            "hashes": [
+                "sha256:b8e67fe6af78f492b3c4b3e2970c0624cbf08beb1e493b2c99b9fa1b67a20380",
+                "sha256:f398865f7eb6874156579fdf36bc840a03cab64d1cde9e93d68f46a425ec52c6"
+            ],
+            "markers": "python_version >= '3.5'",
+            "version": "==2.10.0"
+        },
+        "pymdown-extensions": {
+            "hashes": [
+                "sha256:01e4bec7f4b16beaba0087a74496401cf11afd69e3a11fe95cb593e5c698ef40",
+                "sha256:430cc2fbb30cef2df70edac0b4f62614a6a4d2b06462e32da4ca96098b7c1dfb"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==9.0"
+        },
         "pyparsing": {
             "hashes": [
                 "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
                 "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
             ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2'",
+            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==2.4.7"
         },
         "python-dateutil": {
@@ -175,9 +239,16 @@
                 "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86",
                 "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==2.8.2"
         },
+        "pytz": {
+            "hashes": [
+                "sha256:3672058bc3453457b622aab7a1c3bfd5ab0bdae451512f6cf25f64ed37f5b87c",
+                "sha256:acad2d8b20a1af07d4e4c9d2e9285c5ed9104354062f275f3fcd88dcef4f1326"
+            ],
+            "version": "==2021.3"
+        },
         "pyyaml": {
             "hashes": [
                 "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293",
@@ -230,9 +301,26 @@
                 "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
                 "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
             ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==1.16.0"
         },
+        "smmap": {
+            "hashes": [
+                "sha256:2aba19d6a040e78d8b09de5c57e96207b09ed71d8e55ce0959eeee6c8e190d94",
+                "sha256:c840e62059cd3be204b0c9c9f74be2c09d5648eddd4580d9314c3ecde0b30936"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==5.0.0"
+        },
+        "typing-extensions": {
+            "hashes": [
+                "sha256:49f75d16ff11f1cd258e1b988ccff82a3ca5570217d7ad8c5f48205dd99a677e",
+                "sha256:d8226d10bc02a29bcc81df19a26e56a9647f8b0a6d4a83924139f4a8b01f17b7",
+                "sha256:f1d25edafde516b146ecd0613dabcc61409817af4766fbbcfb8d1ad4ec441a34"
+            ],
+            "markers": "python_version < '3.10'",
+            "version": "==3.10.0.2"
+        },
         "watchdog": {
             "hashes": [
                 "sha256:25fb5240b195d17de949588628fdf93032ebf163524ef08933db0ea1f99bd685",
diff --git a/README.md b/README.md
index cc3f135b074d82e39d2b4dc3a8b7a4191120886a..85c03e9e285f804ffced5db43aae747b44fdd3f4 100644
--- a/README.md
+++ b/README.md
@@ -6,11 +6,14 @@
 
 - Python >= 3.9
 - [`pipenv`](https://pipenv.kennethreitz.org/en/latest/)
+- [MkDocs](https://www.mkdocs.org/) and [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/)
+  - Included in the [list of dependencies](./Pipfile) that will be installed by
+    `pipenv`
 
 ## Getting Started
 
 Assuming `pipenv` present in the shell, create a new virtual environment and
-download all of the required dependencies:
+install all of the required dependencies into the new virtual environment:
 
 ```shell
 pipenv sync
@@ -38,6 +41,12 @@ mkdocs build
 
 By default, the built website is written into `site` directory.
 
+## Adding New Content
+
+Write the new content into a new text file written using Markdown format and
+store them in [`docs/`](./docs/) directory. Then, update the navigation section
+(i.e., `nav`) in [`mkdocs.yml`](./mkdocs.yml).
+
 ## License
 
 Copyright (c) 2021 Faculty of Computer Science Universitas Indonesia.
diff --git a/docs/2020/index.md b/docs/2020/index.md
new file mode 100644
index 0000000000000000000000000000000000000000..8f1faa324059bafe556154ce6ffeea11786f8373
--- /dev/null
+++ b/docs/2020/index.md
@@ -0,0 +1,12 @@
+# Course Information (2020)
+
+Course instructors:
+
+- [Dr. Ade Azurat](https://rse.cs.ui.ac.id/?open=staff/ade)
+  > Responsible for the latter half of the course.
+- [Daya Adianto, M.Kom.](https://me.adian.to)
+  > Responsible for the first half of the course.
+
+Teaching assistants:
+
+- TBD
diff --git a/docs/2020/midexam1.md b/docs/2020/midexam1.md
new file mode 100644
index 0000000000000000000000000000000000000000..1c402802c5710d1b980c6b3a149b82fb1a0f20f9
--- /dev/null
+++ b/docs/2020/midexam1.md
@@ -0,0 +1,264 @@
+# Midterm Exam - Ansible to Docker DevOps
+
+Some projects in [Pusilkom UI](https://pusilkom.com) have automated provisioning
+and deployment implemented using [Ansible](https://www.ansible.com). The
+application and environment configuration are parameterised using Ansible
+variables that written in [Jinja](https://jinja.palletsprojects.com/en/2.11.x/)
+syntax and stored in YAML files. Since some of the variables might contain
+sensitive information such as database password or API key, Ansible Vault is
+also used to separate sensitive variables to a new file and encrypt it for
+storage in the version control system (Git). The variables will only be
+decrypted during provisioning and deployment process.
+
+In recent times, there are requests to prepare a Docker-based deployment using
+Docker Compose. One way to configure an application or a component running in
+a container is by providing the configuration through environment variables.
+To make it easier for DevOps Engineer or System Administrator to provide the
+environment variables, usually they write the variables into a text-based file
+called `.env` that subsequently will be read by Docker Compose.
+
+The problem is how to keep existing Ansible-related artefact in place and make
+them still usable for Docker-based deployment. Since most of the configuration
+are written in YAML files, there must be a way to reuse the configuration
+variables in YAML files and transform them into a `.env` file. In addition,
+the values for some of the variables might also get passed through environment
+variables when run in CI environment.
+
+The following example is a program that reads YAML files, merge them into single
+structure, and print the content as string that can be written into a `.env`
+file:
+
+```python
+#!/usr/bin/env python
+# File: create_dotenv.py
+import os
+import re
+import sys
+
+try:
+    from yaml import safe_load
+    from yaml.scanner import ScannerError
+except ImportError:
+    print("Cannot find PyYAML module in the environment.", file=sys.stderr)
+    print("Please install it first using `pip` or other means", file=sys.stderr)
+    sys.exit(1)
+
+def is_valid_yaml_files(files: list) -> bool:
+    '''
+    Checks whether the given list of file paths contain all valid YAML files.
+
+    Suppose that we have two valid YML files. If the paths to both files are
+    passed to the function, then the function should return True.
+
+    >>> import tempfile
+    >>> with tempfile.NamedTemporaryFile(mode="w", newline="\\n", suffix=".yml") as file1:
+    ...     with tempfile.NamedTemporaryFile(mode="w", newline="\\n", suffix=".yml") as file2:
+    ...         file1.writelines(["---", "a: 1", "b: 2"])
+    ...         file2.writelines(["d: 3", "e: 4"])
+    ...         is_valid_yaml_files([file1.name, file2.name])
+    True
+
+    Otherwise, the function should return False.
+
+    >>> import tempfile
+    >>> with tempfile.NamedTemporaryFile(mode="w", newline="\\n", suffix=".yml") as file1:
+    ...     with tempfile.NamedTemporaryFile(mode="w", newline="\\n", suffix=".yml") as file2:
+    ...         file1.writelines(["{a: 1", "b: 2"])
+    ...         file2.writelines(["d: 3}", "e: 4"])
+    ...         is_valid_yaml_files([file1.name, file2.name])
+    False
+
+    TODO: Fix doctest when run under Windows
+    '''
+    if len(files) == 0:
+        return False
+
+    try:
+        for file in files:
+            with open(file, 'r') as yaml_file:
+                safe_load(yaml_file)
+    except OSError as os_error:
+        print("Cannot open the given file", file=sys.stderr)
+        print(os_error, file=sys.stderr)
+        return False
+    except ScannerError as scanner_error:
+        print("Unable to parse the given YAML files correctly", file=sys.stderr)
+        print(scanner_error, file=sys.stderr)
+        return False
+
+    return True
+
+def parse_decrypted_vault(main_yaml: dict, vault_yaml: dict) -> dict:
+    '''
+    Replaces every occurences of Jinja variables with the corresponding values
+    from a decrypted vault file.
+
+    For example, suppose main_yaml and vault_yaml are illustrated as follows:
+
+    >>> main_yaml = {'app_name': 'example', 'db_user': 'bangtoyib', 'db_pass': '{{ vault_db_pass }}'}
+    >>> vault_yaml = {'vault_db_pass': 'cepatpulang'}
+    >>> parse_decrypted_vault(main_yaml, vault_yaml)
+    {'app_name': 'example', 'db_user': 'bangtoyib', 'db_pass': 'cepatpulang'}
+
+    The function should return the dictionary whose keys are the same as the
+    original dictionary and all of the values with Jinja variables have been
+    replaced.
+
+    TODO: Handle YAML structure with > 1 level (nested)
+    '''
+    from copy import deepcopy
+    result_yaml = deepcopy(main_yaml)
+
+    for key, value in main_yaml.items():
+        if isinstance(value, str) and re.match(r'{{ vault_(\w)+ }}', value):
+            result_yaml[key] = vault_yaml[f'vault_{key}']
+
+    return result_yaml
+
+def main() -> None:
+    if is_valid_yaml_files(sys.argv[1:]):
+        with open(sys.argv[1], 'r') as vars_file:
+            with open(sys.argv[2], 'r') as vault_file:
+                yaml = parse_decrypted_vault(
+                    safe_load(vars_file),
+                    safe_load(vault_file)
+                )
+
+                for key, value in yaml.items():
+                    if isinstance(value, str):
+                        contain_variable = re.match(r"{{ (?P<variable>\w+) }}", value)
+
+                        if contain_variable:
+                            # Assume environment variable is written in all uppercase
+                            os_variable_name = contain_variable.group('variable').upper()
+                            print(f'{key.upper()}={os.getenv(os_variable_name, "")}')
+                        else:
+                            print(f'{key.upper()}={value}')
+                    else:
+                        print(f'{key.upper()}={value}')
+    else:
+        print("One or more YAML files are invalid", file=sys.stderr)
+        sys.exit(1)
+
+if __name__ == '__main__': main()
+```
+
+Actual, but redacted running example:
+
+```bash
+$ python create_dotenv.py \
+  ../ansible/inventories/develop/group_vars/all/vars.yml \
+  ../ansible/inventories/develop/group_vars/all/vault.yml
+APP_DIR=/opt/unhan/euis
+APP_USER=unhan
+APP_THEME=unhan
+APP_SOURCE=
+APP_SUPERPASS=bangtoyib!
+DATABASE_HOST=192.168.0.162
+DATABASE_PORT=5432
+DATABASE_NAME=euis_demo2
+DATABASE_USERNAME=euis_demo2
+DATABASE_PASSWORD=cepatpulang!
+EMAIL_HOST=192.168.0.174
+EMAIL_PORT=25
+EMAIL_USERNAME=no-reply@pusilkom.com
+EMAIL_PASSWORD=None
+EMAIL_ENCRYPTION=None
+PHP_VERSION=7.0
+```
+
+The example above reads two YAML files named `vars.yml` and `vault.yml`.
+The program replaces all occurences of Jinja variables in the `vars.yml` with
+values read from `vault.yml`. In addition, if there are still some Jinja
+variables left in the `vars.yml`, the program reads the OS' environment
+variables to obtain the values for the remaining Jinja variables. Finally,
+any remaining Jinja variables will be assigned with empty values then
+printed to the standard output.
+
+## SQA Problem - Input Space Partitioning
+
+> _Estimated time: 5 minutes_
+
+You are asked to **design an input space model for the API by following a functionality-based approach.**
+The information required to develop the model can be derived by reading the code
+snippet.
+
+Your tasks are as follows:
+
+1. Determine the characteristics and the partition for a function chosen by the
+   proctor.
+   > Possible functions:
+   >
+   > - `is_valid_yaml_files()`
+   > - `main()`
+   > - The whole script, i.e. `create_dotenv.py`
+
+2. Based on the input space model that you have created, create the test
+   requirement and the test cases based on certain coverage criteria chosen by
+   the proctor.
+
+   > Possible coverage criteria choices:
+   >
+   > - All Combinations Coverage (ACoC)
+   > - Each Choice Coverage (ECC)
+   > - Pair-Wise Coverage (PWC)
+   > - Base Choice Coverage (BCC)
+   >
+   > _Note: **You do not have to write all test cases due to the time limit.**
+   > However, make sure you can justify your subset of test cases match with
+   > the chosen coverage criteria!_
+
+Write your answer in a sheet of paper or Microsoft Word/Google Docs document.
+You may include illustrations in your answer. Please prepare to present your
+answer remotely via Zoom/Google Hangouts during discussion time.
+
+## SQA Problem - Graph Coverage
+
+> _Estimated time: 5 minutes_
+
+You are asked to design a **control flow graph (CFG), prepare the test
+requirement, and create the test paths.**
+
+Your tasks are as follows:
+
+1. Create the CFG for a function chosen by the proctor.
+   > Possible functions:
+   >
+   > - `is_valid_yaml_files()`
+   > - `main()`
+   > - The whole script, i.e. `create_dotenv.py`
+
+2. Based on the CFG that you have created, create the test requirement and the
+   test paths based on certain coverage criteria chosen by the proctor.
+   > Possible coverage criteria choices:
+   >
+   > - Node Coverage (NC)
+   > - Edge Coverage (EC)
+   > - Edge-Pair Coverage (EPC)
+   >
+   > _Note: **You do not have to write all test paths due to the time limit.**
+   > However, make sure you can justify your subset of test paths match with
+   > the chosen coverage criteria!_
+
+Write your answer in a sheet of paper or Microsoft Word/Google Docs document.
+You may include illustrations in your answer. Please prepare to present your
+answer remotely via Zoom/Google Hangouts during discussion time.
+
+## SQA Problem - Discussion
+
+> _Estimated time: 10 minutes_
+
+You are asked to present your answers to the given problems and also to have
+one-on-one interview with the proctor during the discussion time.
+
+The list of topics that might be discussed is as follows:
+
+- Code coverage (line coverage)
+- Test-Driven Development (TDD)
+- Test isolation
+- Writing test cases in Java (JUnit)/Python (`unittest` and Django)/PHP
+  (PHPUnit)
+- Your experience in conducting SQA activities in academics and/or work
+  environment
+- The ideas of mutation testing
+- And many more that may still related to SQA
diff --git a/docs/2020/midexam2.md b/docs/2020/midexam2.md
new file mode 100644
index 0000000000000000000000000000000000000000..842cd99b54558ad9ae3bb8ed0829052d1c4ca4a2
--- /dev/null
+++ b/docs/2020/midexam2.md
@@ -0,0 +1,100 @@
+# Midterm Exam - Lion Game (Backend)
+
+This project provides an API that allows Lion Game communicate with Google
+Sheets-based backend for storing play session analytics. The source code is
+available [here](https://gitlab.cs.ui.ac.id/bukan-macan-ternak/liongame-backend).
+
+## SQA Problem - Input Space Partitioning
+
+> _Estimated time: 5 minutes_
+
+You are asked to **design an input space model for the API by following a functionality-based approach.**
+The minimal information required to develop the model can be derived by
+reading the API documentation available in [`README.md`](https://gitlab.cs.ui.ac.id/bukan-macan-ternak/liongame-backend/-/blob/master/README.md)
+file.
+
+Your tasks are as follows:
+
+1. Determine the characteristics and the partition for a function chosen by the
+   proctor.
+   > Possible functions:
+   >
+   > - The request handler function for `/api/logs/` endpoint.
+   > - The request handler function for `/api/logs/batch` endpoint.
+   >
+   > The body of both functions can be inspected in [`App.php`](https://gitlab.cs.ui.ac.id/bukan-macan-ternak/liongame-backend/-/blob/master/src/App.php).
+
+2. Based on the input space model that you have created, create the test
+   requirement and the test cases based on certain coverage criteria chosen by
+   the proctor.
+
+   > Possible coverage criteria choices:
+   >
+   > - All Combinations Coverage (ACoC)
+   > - Each Choice Coverage (ECC)
+   > - Pair-Wise Coverage (PWC)
+   > - Base Choice Coverage (BCC)
+   >
+   > _Note: **You do not have to write all test cases** due to the time limit.
+   > However, make sure you can justify your subset of test cases match with
+   > the chosen coverage criteria!_
+
+Write your answer in a sheet of paper or Microsoft Word/Google Docs document.
+You may include illustrations in your answer. Please prepare to present your
+answer remotely via Zoom/Google Hangouts during discussion time.
+
+## SQA Problem - Graph Coverage
+
+> _Estimated time: 5 minutes_
+
+You are asked to design a **control flow graph (CFG), prepare the test
+requirement, and create the test cases.**
+
+Your tasks are as follows:
+
+1. Create the CFG for a function chosen by the proctor.
+   > Possible functions:
+   >
+   > - The request handler function for `/api/logs/` endpoint.
+   > - The request handler function for `/api/logs/batch` endpoint.
+   > - `isValidData()` function.
+   > - `appendValues()` function.
+   >
+   > The definition of request handler functions and `isValidData()` can be
+   > inspected in [`App.php`](https://gitlab.cs.ui.ac.id/bukan-macan-ternak/liongame-backend/-/blob/master/src/App.php).
+   > The `appendValues()` function definition is available in [`SheeetsService.php`](https://gitlab.cs.ui.ac.id/bukan-macan-ternak/liongame-backend/-/blob/master/src/service/SheetsService.php).
+2. Based on the CFG that you have created, create the test requirement and the
+   test paths based on certain coverage criteria chosen by the proctor.
+
+   > Possible coverage criteria choices:
+   >
+   > - Node Coverage (NC)
+   > - Edge Coverage (EC)
+   > - Edge-Pair Coverage (EPC)
+   >
+   > _Note: **You do not have to write all test paths due to the time limit.**
+   > However, make sure you can justify your subset of test paths match with
+   > the chosen coverage criteria!_
+
+Write your answer in a sheet of paper or Microsoft Word/Google Docs document.
+You may include illustrations in your answer. Please prepare to present your
+answer remotely via Zoom/Google Hangouts during discussion time.
+
+## SQA Problem - Discussion
+
+> _Estimated time: 10 minutes_
+
+You are asked to present your answers to the given problems and also to have
+one-on-one interview with the proctor during the discussion time.
+
+The list of topics that might be discussed is as follows:
+
+- Code coverage (line coverage)
+- Test-Driven Development (TDD)
+- Test isolation
+- Writing test cases in Java (JUnit)/Python (`unittest` and Django)/PHP
+  (PHPUnit)
+- Your experience in conducting SQA activities in academics and/or work
+  environment
+- The ideas of mutation testing
+- And many more that may still related to SQA
diff --git a/docs/2021/ex1.md b/docs/2021/exercise1.md
similarity index 84%
rename from docs/2021/ex1.md
rename to docs/2021/exercise1.md
index 826bc1b50bcf49907bf36b1201377d887e6ccf69..766f2c4ff99f71811c47dc263f90ed7a5aff704d 100644
--- a/docs/2021/ex1.md
+++ b/docs/2021/exercise1.md
@@ -3,17 +3,18 @@
 You are asked to set up a CI/CD pipeline of the your group project
 **individually** by forking the existing group project codebase and updating
 the CI/CD configuration. As part of the exercise, you also need to prepare
-your own VM on Google Cloud Platform (GCP) and explore how to use Static
-Application Security Testing (SAST) on self-hosted GitLab (GitLab CSUI).
+your own VM on GCP and explore how to use SAST on  a self-hosted GitLab (i.e.
+GitLab CSUI).
 
 For your information when setting up the GitLab CI/CD configuration that will
 be run on GitLab CSUI, the following is the overview of the CI infrastructure
 in our faculty:
 
 - We run GitLab CSUI using GitLab Enterprise Edition version 13.12.15.
-- The CI server runs 8 instances of GitLab Runner version 13.12.0.
+- The CI server runs 8 instances of [GitLab Runner](https://gitlab.com/gitlab-org/gitlab-runner)
+  version 13.12.0.
   - Each instance is running as a container with limited resources (2 CPU per
-    container) and disabled the privileged mode. Hence, it is not possible to
+    container) and the privileged mode disabled. Hence, it is not possible to
     run a Docker-in-Docker (DIND) type of CI job.
   - Each instance shares the cache between CI jobs using [Minio](https://min.io/).
   - Each instance is also limited to run single CI job at a time.
@@ -39,7 +40,8 @@ with a teaching assistant to demonstrate your work.
 5. [ ] Add the SAST job into the CI/CD pipeline of your own fork and make sure
    it runs.
    > Due to [an ongoing issue on running the latest SAST image](https://gitlab.com/gitlab-org/gitlab/-/issues/344022),
-   > pin the version of SAST analyser image to version 2.28.5.
+   > pin the version of SAST analyser image to version 2.28.5 in the CI/CD
+   > configuration file.
 6. [ ] Arrange an one-on-one meeting with a teaching assistant to demonstrate
    your work. You are expected to be able to:
     - Explain the process of setting up the deployment environment of your group
@@ -54,3 +56,8 @@ with a teaching assistant to demonstrate your work.
 
 - [GitLab CI/CD Reference on GitLab CSUI](https://gitlab.cs.ui.ac.id/help/ci/yaml/README.md)
 - [SAST Documentation on GitLab CSUI](https://gitlab.cs.ui.ac.id/help/user/application_security/sast/index.md)
+
+*[CSUI]: Faculty of Computer Science Universitas Indonesia
+*[GCP]: Google Cloud Platform
+*[SAST]: Static Analysis Security Testing
+*[VM]: Virtual Machine
diff --git a/docs/images/favicon.png b/docs/images/favicon.png
new file mode 100644
index 0000000000000000000000000000000000000000..b0c8719e1546b0910d76d7cbb89f00fff1e2d562
Binary files /dev/null and b/docs/images/favicon.png differ
diff --git a/docs/images/logo.png b/docs/images/logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..60f6f753178d383bb61644f9044ffb7be8d17df7
Binary files /dev/null and b/docs/images/logo.png differ
diff --git a/mkdocs.yml b/mkdocs.yml
index 7f56b24c84338a4fdfea3a1e3c87a6a05e0a66be..47e6be703a81fd8207dcd62ee98abf72fef646e4 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -8,9 +8,52 @@ copyright: Copyright &copy; 2021 Faculty of Computer Science Universitas Indones
 
 repo_url: https://gitlab.cs.ui.ac.id/pmpl/course-site
 repo_name: GitLab @ CSUI
+edit_uri: ""
+
+theme:
+  name: material
+  features:
+    - navigation.tabs
+    - navigation.indexes
+    - navigation.top
+  logo: images/logo.png
+  favicon: images/favicon.png
+  icon:
+    repo: fontawesome/brands/gitlab
+  palette:
+    - media: "(prefers-color-scheme: light)"
+      scheme: default
+      primary: deep orange
+      toggle:
+        icon: material/toggle-switch-off-outline
+        name: Switch to dark mode
+    - media: "(prefers-color-scheme: dark)"
+      scheme: slate
+      primary: deep orange
+      toggle:
+        icon: material/toggle-switch
+        name: Switch to light mode
+
+plugins:
+  - git-revision-date-localized:
+      type: iso_datetime
+      enable_creation_date: true
+
+markdown_extensions:
+  - abbr
+  - pymdownx.highlight:
+      linenums: true
+  - pymdownx.superfences
+  - pymdownx.snippets
+  - pymdownx.tasklist:
+      custom_checkbox: true
 
 nav:
   - Home: index.md
   - Year 2021:
-    - Course Page: 2021/index.md
-    - Exercise 1: 2021/ex1.md
+    - 2021/index.md
+    - Exercise 1: 2021/exercise1.md
+  - Year 2020:
+    - 2020/index.md
+    - Midterm Exam 1: 2020/midexam1.md
+    - Midterm Exam 2: 2020/midexam2.md