Repository: uccser/cs-unplugged

Issue:
Update error correction and detection lesson 2 (8-10)
This include:
- Content
- Video
- Related programming exercises #391 
- Resources (images still in progress)


Discussion:

Resolving patch:
diff --git a/csunplugged/resources/views/parity_cards.py b/csunplugged/resources/views/parity_cards.py
new file mode 100644
--- /dev/null
+++ b/csunplugged/resources/views/parity_cards.py
@@ -0,0 +1,85 @@
+"""Module for generating Parity Cards resource."""
+
+from PIL import Image, ImageDraw
+from utils.retrieve_query_parameter import retrieve_query_parameter
+
+
+def resource_image(request, resource):
+    """Create a image for Parity Cards resource.
+
+    Args:
+        request: HTTP request object.
+        resource: Object of resource data.
+
+    Returns:
+        A list of Pillow image objects.
+    """
+    CARDS_COLUMNS = 4
+    CARDS_ROWS = 5
+    CARD_SIZE = 500
+    IMAGE_SIZE_X = CARD_SIZE * CARDS_COLUMNS
+    IMAGE_SIZE_Y = CARD_SIZE * CARDS_ROWS
+    LINE_COLOUR = "#000000"
+    LINE_WIDTH = 3
+
+    front_page = Image.new("RGB", (IMAGE_SIZE_X, IMAGE_SIZE_Y), "#fff")
+    draw = ImageDraw.Draw(front_page)
+    for x_coord in range(0, IMAGE_SIZE_X, CARD_SIZE):
+        draw.line([(x_coord, 0), (x_coord, IMAGE_SIZE_Y)], fill=LINE_COLOUR, width=LINE_WIDTH)
+    draw.line([(IMAGE_SIZE_X - 1, 0), (IMAGE_SIZE_X - 1, IMAGE_SIZE_Y)], fill=LINE_COLOUR, width=LINE_WIDTH)
+    for y_coord in range(0, IMAGE_SIZE_Y, CARD_SIZE):
+        draw.line([(0, y_coord), (IMAGE_SIZE_X, y_coord)], fill=LINE_COLOUR, width=LINE_WIDTH)
+    draw.line([(0, IMAGE_SIZE_Y - 1), (IMAGE_SIZE_X, IMAGE_SIZE_Y - 1)], fill=LINE_COLOUR, width=LINE_WIDTH)
+
+    # Retrieve parameters
+    parameter_options = valid_options()
+    back_colour = retrieve_query_parameter(request, "back_colour", parameter_options["back_colour"])
+
+    if back_colour == "black":
+        back_colour_hex = "#000000"
+    elif back_colour == "blue":
+        back_colour_hex = "#3366ff"
+    elif back_colour == "green":
+        back_colour_hex = "#279f2d"
+    elif back_colour == "purple":
+        back_colour_hex = "#6e3896"
+    else:
+        back_colour_hex = "#cc0423"
+
+    back_page = Image.new("RGB", (IMAGE_SIZE_X, IMAGE_SIZE_Y), back_colour_hex)
+
+    return [front_page, back_page]
+
+
+def subtitle(request, resource):
+    """Return the subtitle string of the resource.
+
+    Used after the resource name in the filename, and
+    also on the resource image.
+
+    Args:
+        request: HTTP request object
+        resource: Object of resource data.
+
+    Returns:
+        text for subtitle (string)
+    """
+    text = "{} back - {}".format(
+        retrieve_query_parameter(request, "back_colour"),
+        retrieve_query_parameter(request, "paper_size")
+    )
+    return text
+
+
+def valid_options():
+    """Provide dictionary of all valid parameters.
+
+    This excludes the header text parameter.
+
+    Returns:
+        All valid options (dict).
+    """
+    return {
+        "back_colour": ["black", "blue", "green", "purple", "red"],
+        "paper_size": ["a4", "letter"],
+    }


Test patch:
diff --git a/csunplugged/tests/resources/urls/test_parity_cards.py b/csunplugged/tests/resources/urls/test_parity_cards.py
new file mode 100644
--- /dev/null
+++ b/csunplugged/tests/resources/urls/test_parity_cards.py
@@ -0,0 +1,19 @@
+from tests.BaseTestWithDB import BaseTestWithDB
+from django.urls import reverse
+
+
+class ParityCardsResourceURLTest(BaseTestWithDB):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.language = "en"
+        self.RESOURCE_SLUG = "parity-cards"
+        self.RESOURCE_URL_KWARGS = {"resource_slug": self.RESOURCE_SLUG}
+
+    def test_valid_parity_cards_resource_url(self):
+        url = reverse("resources:resource", kwargs=self.RESOURCE_URL_KWARGS)
+        self.assertEqual(url, "/en/resources/parity-cards/")
+
+    def test_valid_parity_cards_resource_generate_url(self):
+        url = reverse("resources:generate", kwargs=self.RESOURCE_URL_KWARGS)
+        self.assertEqual(url, "/en/resources/parity-cards/generate")
diff --git a/csunplugged/tests/resources/views/test_parity_cards.py b/csunplugged/tests/resources/views/test_parity_cards.py
new file mode 100644
--- /dev/null
+++ b/csunplugged/tests/resources/views/test_parity_cards.py
@@ -0,0 +1,120 @@
+from django.urls import reverse
+from tests.BaseTestWithDB import BaseTestWithDB
+from tests.resources.ResourcesTestDataGenerator import ResourcesTestDataGenerator
+from utils.import_resource_module import import_resource_module
+from utils.create_query_string import query_string
+from utils.resource_valid_test_configurations import resource_valid_test_configurations
+
+
+class BinaryCardsSmallResourceViewTest(BaseTestWithDB):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.test_data = ResourcesTestDataGenerator()
+        self.language = "en"
+
+    def test_parity_cards_resource_form_view(self):
+        resource = self.test_data.create_resource(
+            "parity-cards",
+            "Parity Cards",
+            "resources/parity-cards.html",
+            "parity_cards.py",
+        )
+        kwargs = {
+            "resource_slug": resource.slug,
+        }
+        url = reverse("resources:resource", kwargs=kwargs)
+        response = self.client.get(url)
+        self.assertEqual(200, response.status_code)
+
+    def test_parity_cards_resource_generation_valid_configurations(self):
+        resource = self.test_data.create_resource(
+            "parity-cards",
+            "Parity Cards",
+            "resources/parity-cards.html",
+            "parity_cards.py",
+        )
+        kwargs = {
+            "resource_slug": resource.slug,
+        }
+        base_url = reverse("resources:generate", kwargs=kwargs)
+        resource_module = import_resource_module(resource)
+        valid_options = resource_module.valid_options()
+        combinations = resource_valid_test_configurations(valid_options)
+        print()
+        for combination in combinations:
+            print("   - Testing combination: {} ... ".format(combination), end="")
+            url = base_url + query_string(combination)
+            response = self.client.get(url)
+            self.assertEqual(200, response.status_code)
+            subtitle = "{} back - {}".format(
+                combination["back_colour"],
+                combination["paper_size"],
+            )
+            self.assertEqual(
+                response.get("Content-Disposition"),
+                'attachment; filename="Resource Parity Cards ({subtitle}).pdf"'.format(subtitle=subtitle)
+            )
+            print("ok")
+
+    def test_parity_cards_resource_generation_missing_back_colour_parameter(self):
+        resource = self.test_data.create_resource(
+            "parity-cards",
+            "Parity Cards",
+            "resources/parity-cards.html",
+            "parity_cards.py",
+        )
+        kwargs = {
+            "resource_slug": resource.slug,
+        }
+        url = reverse("resources:generate", kwargs=kwargs)
+        get_parameters = {
+            "paper_size": "a4",
+            "header_text": "",
+        }
+        url += query_string(get_parameters)
+        response = self.client.get(url)
+        self.assertEqual(404, response.status_code)
+
+    def test_binary_cards_small_resource_generation_missing_paper_size_parameter(self):
+        resource = self.test_data.create_resource(
+            "parity-cards",
+            "Parity Cards",
+            "resources/parity-cards.html",
+            "parity_cards.py",
+        )
+        kwargs = {
+            "resource_slug": resource.slug,
+        }
+        url = reverse("resources:generate", kwargs=kwargs)
+        get_parameters = {
+            "back_colour": "black",
+            "header_text": "",
+        }
+        url += query_string(get_parameters)
+        response = self.client.get(url)
+        self.assertEqual(404, response.status_code)
+
+    def test_binary_cards_small_resource_generation_missing_header_text_parameter(self):
+        resource = self.test_data.create_resource(
+            "parity-cards",
+            "Parity Cards",
+            "resources/parity-cards.html",
+            "parity_cards.py",
+        )
+        kwargs = {
+            "resource_slug": resource.slug,
+        }
+        url = reverse("resources:generate", kwargs=kwargs)
+        get_parameters = {
+            "back_colour": "black",
+            "paper_size": "a4",
+        }
+        url += query_string(get_parameters)
+        response = self.client.get(url)
+        self.assertEqual(200, response.status_code)
+        filename = "Resource Parity Cards (black back - a4).pdf"
+        self.assertEqual(
+            response.get("Content-Disposition"),
+            'attachment; filename="{}"'.format(filename)
+        )
diff --git a/csunplugged/utils/resource_valid_test_configurations.py b/csunplugged/utils/resource_valid_test_configurations.py
new file mode 100644
--- /dev/null
+++ b/csunplugged/utils/resource_valid_test_configurations.py
@@ -0,0 +1,18 @@
+"""Create list of all possible valid resource combinations."""
+
+import itertools
+
+
+def resource_valid_test_configurations(valid_options):
+    """Return list of all possible valid resource combinations.
+
+    Args:
+        valid_options: A dictionary containing all valid resource generation
+                       options (dict).
+
+    Returns:
+        List of lists of valid combinations (list).
+    """
+    valid_options["header_text"] = ["", "Example header"]
+    valid_option_keys = sorted(valid_options)
+    return [dict(zip(valid_option_keys, product)) for product in itertools.product(*(valid_options[valid_option_key] for valid_option_key in valid_option_keys))]  # noqa: E501