Repository: uccser/cs-unplugged

Issue:
Add sorting network cards resource



Discussion:

Resolving patch:
diff --git a/csunplugged/resources/views/sorting_network.py b/csunplugged/resources/views/sorting_network.py
--- a/csunplugged/resources/views/sorting_network.py
+++ b/csunplugged/resources/views/sorting_network.py
@@ -62,7 +62,7 @@ def subtitle(request, resource):
         text for subtitle (string)
     """
     prefilled_values = retrieve_query_parameter(request, "prefilled_values")
-    if prefilled_values != "blank":
+    if prefilled_values == "blank":
         range_text = "blank"
     else:
         SUBTITLE_TEMPLATE = "{} to {}"
diff --git a/csunplugged/resources/views/sorting_network_cards.py b/csunplugged/resources/views/sorting_network_cards.py
new file mode 100644
--- /dev/null
+++ b/csunplugged/resources/views/sorting_network_cards.py
@@ -0,0 +1,134 @@
+"""Module for generating Sorting Network Cards resource."""
+
+from random import sample
+from PIL import Image, ImageDraw, ImageFont
+from utils.retrieve_query_parameter import retrieve_query_parameter
+
+
+def resource_image(request, resource):
+    """Create a image for Sorting Network Cards resource.
+
+    Args:
+        request: HTTP request object.
+        resource: Object of resource data.
+
+    Returns:
+        A list of Pillow image objects.
+    """
+    IMAGE_SIZE_X = 2000
+    IMAGE_SIZE_Y = 3000
+    LINE_COLOUR = "#000000"
+    LINE_WIDTH = 3
+    font_path = "static/fonts/PatrickHand-Regular.ttf"
+
+    # Retrieve parameters
+    parameter_options = valid_options()
+    card_type = retrieve_query_parameter(request, "type", parameter_options["type"])
+
+    # Create card outlines
+    card_outlines = Image.new("RGB", (IMAGE_SIZE_X, IMAGE_SIZE_Y), "#fff")
+    draw = ImageDraw.Draw(card_outlines)
+    for x_coord in range(0, IMAGE_SIZE_X, IMAGE_SIZE_X - LINE_WIDTH):
+        draw.line([(x_coord, 0), (x_coord, IMAGE_SIZE_Y)], fill=LINE_COLOUR, width=LINE_WIDTH)
+    for y_coord in range(0, IMAGE_SIZE_Y, int(IMAGE_SIZE_Y / 2 - LINE_WIDTH)):
+        draw.line([(0, y_coord), (IMAGE_SIZE_X, y_coord)], fill=LINE_COLOUR, width=LINE_WIDTH)
+
+    # Prepare text data
+    if card_type == "small_numbers":
+        font_size = 800
+        text = ["1", "2", "3", "4", "5", "6"]
+    elif card_type == "large_numbers":
+        font_size = 500
+        text = []
+        numbers = sample(range(1700000, 2100000), 6)
+        for number in numbers:
+            text.append("{:,}".format(number))
+    elif card_type == "fractions":
+        font_size = 900
+        font_path = "static/fonts/NotoSans-Regular.ttf"
+        text = [u"\u00bd", u"\u2153", u"\u2154", u"\u215c", u"\u00be", u"\u215d"]
+    else:
+        font_size = 300
+        text = [
+            "tahi",
+            "rua",
+            "toru",
+            "whā",
+            "rima",
+            "ono",
+            "whitu",
+            "waru",
+            "iwa",
+            "tekau",
+            "tekau mā tahi",
+            "tekau mā waru",
+            "tekau mā toru",
+            "tekau mā whā",
+            "rua tekau",
+            "rua tekau mā ono",
+        ]
+
+    font = ImageFont.truetype(font_path, font_size)
+    card_centers = [
+        (IMAGE_SIZE_X / 2, IMAGE_SIZE_Y / 4),
+        (IMAGE_SIZE_X / 2, (IMAGE_SIZE_Y / 4) * 3),
+    ]
+
+    # Add text to cards
+    images = []
+    for (text_number, text_string) in enumerate(text):
+        if text_number % 2 == 0:
+            page = card_outlines.copy()
+            draw = ImageDraw.Draw(page)
+            (x, y) = card_centers[0]
+        else:
+            (x, y) = card_centers[1]
+
+        text_width, text_height = draw.textsize(text_string, font=font)
+        coord_x = x - (text_width / 2)
+        coord_y = y - (text_height / 1.5)
+        draw.text(
+            (coord_x, coord_y),
+            text_string,
+            font=font,
+            fill="#000"
+        )
+        # If text on second card but not last page
+        if text_number % 2 == 1 and text_number != len(text) - 1:
+            images.append(page)
+    images.append(page)
+
+    return images
+
+
+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)
+    """
+    return "{} - {}".format(
+        retrieve_query_parameter(request, "type").replace("_", " "),
+        retrieve_query_parameter(request, "paper_size")
+    )
+
+
+def valid_options():
+    """Provide dictionary of all valid parameters.
+
+    This excludes the header text parameter.
+
+    Returns:
+        All valid options (dict).
+    """
+    return {
+        "type": ["small_numbers", "large_numbers", "fractions", "maori_numbers"],
+        "paper_size": ["a4", "letter"],
+    }


Test patch:
diff --git a/csunplugged/tests/resources/urls/test_sorting_network_cards.py b/csunplugged/tests/resources/urls/test_sorting_network_cards.py
new file mode 100644
--- /dev/null
+++ b/csunplugged/tests/resources/urls/test_sorting_network_cards.py
@@ -0,0 +1,19 @@
+from tests.BaseTestWithDB import BaseTestWithDB
+from django.urls import reverse
+
+
+class SortingNetworkCardsResourceURLTest(BaseTestWithDB):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.language = "en"
+        self.RESOURCE_SLUG = "sorting-network-cards"
+        self.RESOURCE_URL_KWARGS = {"resource_slug": self.RESOURCE_SLUG}
+
+    def test_valid_sorting_network_cards_resource_url(self):
+        url = reverse("resources:resource", kwargs=self.RESOURCE_URL_KWARGS)
+        self.assertEqual(url, "/en/resources/sorting-network-cards/")
+
+    def test_valid_sorting_network_cards_resource_generate_url(self):
+        url = reverse("resources:generate", kwargs=self.RESOURCE_URL_KWARGS)
+        self.assertEqual(url, "/en/resources/sorting-network-cards/generate")
diff --git a/csunplugged/tests/resources/views/test_sorting_network_cards.py b/csunplugged/tests/resources/views/test_sorting_network_cards.py
new file mode 100644
--- /dev/null
+++ b/csunplugged/tests/resources/views/test_sorting_network_cards.py
@@ -0,0 +1,122 @@
+import itertools
+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
+
+
+class SortingNetworkCardsResourceViewTest(BaseTestWithDB):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.test_data = ResourcesTestDataGenerator()
+        self.language = "en"
+
+    def test_sorting_network_cards_resource_form_view(self):
+        resource = self.test_data.create_resource(
+            "sorting-network-cards",
+            "Sorting Network Cards",
+            "resources/sorting-network-cards.html",
+            "sorting_network_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_sorting_network_cards_resource_generation_valid_configurations(self):
+        resource = self.test_data.create_resource(
+            "sorting-network-cards",
+            "Sorting Network Cards",
+            "resources/sorting-network-cards.html",
+            "sorting_network_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()
+        valid_options["header_text"] = ["", "Example header"]
+        valid_option_keys = sorted(valid_options)
+        combinations = [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
+        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 = "{} - {}".format(
+                combination["type"].replace("_", " "),
+                combination["paper_size"],
+            )
+            self.assertEqual(
+                response.get("Content-Disposition"),
+                'attachment; filename="Resource Sorting Network Cards ({subtitle}).pdf"'.format(subtitle=subtitle)
+            )
+            print("ok")
+
+    def test_sorting_network_cards_resource_generation_missing_type_parameter(self):
+        resource = self.test_data.create_resource(
+            "sorting-network-cards",
+            "Sorting Network Cards",
+            "resources/sorting-network-cards.html",
+            "sorting_network_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_sorting_network_cards_resource_generation_missing_paper_size_parameter(self):
+        resource = self.test_data.create_resource(
+            "sorting-network-cards",
+            "Sorting Network Cards",
+            "resources/sorting-network-cards.html",
+            "sorting_network_cards.py",
+        )
+        kwargs = {
+            "resource_slug": resource.slug,
+        }
+        url = reverse("resources:generate", kwargs=kwargs)
+        get_parameters = {
+            "type": "small_numbers",
+            "header_text": "",
+        }
+        url += query_string(get_parameters)
+        response = self.client.get(url)
+        self.assertEqual(404, response.status_code)
+
+    def test_sorting_network_cards_resource_generation_missing_header_text_parameter(self):
+        resource = self.test_data.create_resource(
+            "sorting-network-cards",
+            "Sorting Network Cards",
+            "resources/sorting-network-cards.html",
+            "sorting_network_cards.py",
+        )
+        kwargs = {
+            "resource_slug": resource.slug,
+        }
+        url = reverse("resources:generate", kwargs=kwargs)
+        get_parameters = {
+            "type": "small_numbers",
+            "paper_size": "a4",
+        }
+        url += query_string(get_parameters)
+        response = self.client.get(url)
+        self.assertEqual(200, response.status_code)
+        filename = "Resource Sorting Network Cards (small numbers - a4).pdf"
+        self.assertEqual(
+            response.get("Content-Disposition"),
+            'attachment; filename="{}"'.format(filename)
+        )