Repository: uccser/cs-unplugged

Issue:
Always display curriculum areas for learning outcomes on a new line
This prevents a confusing interface, like so:

![image](https://user-images.githubusercontent.com/8001048/27560331-e410500a-5b17-11e7-8cd2-db32964f721b.png)

Center navbar menu text on mobile devices
![image](https://user-images.githubusercontent.com/8001048/27560437-4254d4b0-5b18-11e7-811f-6da827fa1365.png)



Discussion:

Resolving patch:
diff --git a/csunplugged/config/__init__.py b/csunplugged/config/__init__.py
--- a/csunplugged/config/__init__.py
+++ b/csunplugged/config/__init__.py
@@ -1,3 +1,3 @@
 """Module for Django system configuration."""
 
-__version__ = "2.0.0-alpha.2"
+__version__ = "2.0.0-alpha.3"
diff --git a/csunplugged/resources/views/grid.py b/csunplugged/resources/views/grid.py
new file mode 100644
--- /dev/null
+++ b/csunplugged/resources/views/grid.py
@@ -0,0 +1,63 @@
+"""Module for generating Grid resource."""
+
+from PIL import Image, ImageDraw
+from utils.retrieve_query_parameter import retrieve_query_parameter
+
+
+def resource_image(request, resource):
+    """Create a image for Grid resource.
+
+    Args:
+        request: HTTP request object (HttpRequest).
+        resource: Object of resource data (Resource).
+
+    Returns:
+        A list of Pillow image objects (list).
+    """
+    GRID_COLUMNS = 8
+    GRID_ROWS = 8
+    BOX_SIZE = 500
+    IMAGE_SIZE_X = BOX_SIZE * GRID_COLUMNS
+    IMAGE_SIZE_Y = BOX_SIZE * GRID_ROWS
+    LINE_COLOUR = "#000000"
+    LINE_WIDTH = 3
+
+    page = Image.new("RGB", (IMAGE_SIZE_X, IMAGE_SIZE_Y), "#fff")
+    draw = ImageDraw.Draw(page)
+    for x_coord in range(0, IMAGE_SIZE_X, BOX_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, BOX_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)
+
+    return [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 (HttpRequest).
+        resource: Object of resource data (Resource).
+
+    Returns:
+        text for subtitle (str).
+    """
+    return 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 {
+        "paper_size": ["a4", "letter"],
+    }
diff --git a/csunplugged/topics/views.py b/csunplugged/topics/views.py
--- a/csunplugged/topics/views.py
+++ b/csunplugged/topics/views.py
@@ -31,7 +31,7 @@ def get_queryset(self):
         Returns:
             Queryset of Topic objects ordered by name.
         """
-        return Topic.objects.order_by("name")
+        return Topic.objects.order_by("name").prefetch_related("unit_plans")
 
 
 class TopicView(generic.DetailView):


Test patch:
diff --git a/csunplugged/tests/resources/urls/test_grid.py b/csunplugged/tests/resources/urls/test_grid.py
new file mode 100644
--- /dev/null
+++ b/csunplugged/tests/resources/urls/test_grid.py
@@ -0,0 +1,19 @@
+from tests.BaseTestWithDB import BaseTestWithDB
+from django.urls import reverse
+
+
+class GridResourceURLTest(BaseTestWithDB):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.language = "en"
+        self.RESOURCE_SLUG = "grid"
+        self.RESOURCE_URL_KWARGS = {"resource_slug": self.RESOURCE_SLUG}
+
+    def test_valid_grid_resource_url(self):
+        url = reverse("resources:resource", kwargs=self.RESOURCE_URL_KWARGS)
+        self.assertEqual(url, "/en/resources/grid/")
+
+    def test_valid_grid_resource_generate_url(self):
+        url = reverse("resources:generate", kwargs=self.RESOURCE_URL_KWARGS)
+        self.assertEqual(url, "/en/resources/grid/generate")
diff --git a/csunplugged/tests/resources/views/test_grid.py b/csunplugged/tests/resources/views/test_grid.py
new file mode 100644
--- /dev/null
+++ b/csunplugged/tests/resources/views/test_grid.py
@@ -0,0 +1,98 @@
+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.resource_valid_test_configurations import resource_valid_test_configurations
+from utils.create_query_string import query_string
+
+
+class GridResourceViewTest(BaseTestWithDB):
+
+    def __init__(self, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+        self.test_data = ResourcesTestDataGenerator()
+        self.language = "en"
+
+    def test_grid_resource_form_view(self):
+        resource = self.test_data.create_resource(
+            "grid",
+            "Grid",
+            "resources/grid.html",
+            "grid.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_grid_resource_generation_valid_configurations(self):
+        resource = self.test_data.create_resource(
+            "grid",
+            "Grid",
+            "resources/grid.html",
+            "grid.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 = combination["paper_size"]
+            self.assertEqual(
+                response.get("Content-Disposition"),
+                'attachment; filename="Resource Grid ({subtitle}).pdf"'.format(subtitle=subtitle)
+            )
+            print("ok")
+
+    def test_grid_resource_generation_missing_paper_size_parameter(self):
+        resource = self.test_data.create_resource(
+            "grid",
+            "Grid",
+            "resources/grid.html",
+            "grid.py",
+        )
+        kwargs = {
+            "resource_slug": resource.slug,
+        }
+        url = reverse("resources:generate", kwargs=kwargs)
+        get_parameters = {
+            "header_text": "",
+        }
+        url += query_string(get_parameters)
+        response = self.client.get(url)
+        self.assertEqual(404, response.status_code)
+
+    def test_grid_resource_generation_missing_header_text_parameter(self):
+        resource = self.test_data.create_resource(
+            "grid",
+            "Grid",
+            "resources/grid.html",
+            "grid.py",
+        )
+        kwargs = {
+            "resource_slug": resource.slug,
+        }
+        url = reverse("resources:generate", kwargs=kwargs)
+        get_parameters = {
+            "paper_size": "a4",
+        }
+        url += query_string(get_parameters)
+        response = self.client.get(url)
+        self.assertEqual(200, response.status_code)
+        filename = "Resource Grid (a4).pdf"
+        self.assertEqual(
+            response.get("Content-Disposition"),
+            'attachment; filename="{}"'.format(filename)
+        )