From 36930ee461c57c06c4da070de27e76ec3a8e9341 Mon Sep 17 00:00:00 2001
From: Shimul Chowdhury <shimul@opencraft.com>
Date: Tue, 29 Dec 2020 19:33:21 +0600
Subject: [PATCH] Add support for image content type

---
 lti_consumer/lti_1p3/constants.py             |  2 +-
 .../extensions/rest_framework/constants.py    |  2 +
 .../extensions/rest_framework/serializers.py  | 19 ++++
 .../html/lti-dl/render_dl_content.html        |  2 +
 .../templates/html/lti-dl/render_image.html   | 14 +++
 .../plugin/test_views_lti_deep_linking.py     | 96 +++++++++++++++++++
 6 files changed, 134 insertions(+), 1 deletion(-)
 create mode 100644 lti_consumer/templates/html/lti-dl/render_image.html

diff --git a/lti_consumer/lti_1p3/constants.py b/lti_consumer/lti_1p3/constants.py
index b7a41de..ff0e28e 100644
--- a/lti_consumer/lti_1p3/constants.py
+++ b/lti_consumer/lti_1p3/constants.py
@@ -56,7 +56,7 @@ LTI_DEEP_LINKING_ACCEPTED_TYPES = [
     'ltiResourceLink',
     'link',
     'html',
-    # TODO: implement "image" support,
+    'image',
     # TODO: implement "file" support,
 ]
 
diff --git a/lti_consumer/lti_1p3/extensions/rest_framework/constants.py b/lti_consumer/lti_1p3/extensions/rest_framework/constants.py
index b812d79..e7797cc 100644
--- a/lti_consumer/lti_1p3/extensions/rest_framework/constants.py
+++ b/lti_consumer/lti_1p3/extensions/rest_framework/constants.py
@@ -5,6 +5,7 @@ from .serializers import (
     LtiDlLtiResourceLinkSerializer,
     LtiDlLinkSerializer,
     LtiDlHtmlSerializer,
+    LtiDlImageSerializer,
 )
 
 
@@ -12,4 +13,5 @@ LTI_DL_CONTENT_TYPE_SERIALIZER_MAP = {
     "ltiResourceLink": LtiDlLtiResourceLinkSerializer,
     "link": LtiDlLinkSerializer,
     "html": LtiDlHtmlSerializer,
+    "image": LtiDlImageSerializer,
 }
diff --git a/lti_consumer/lti_1p3/extensions/rest_framework/serializers.py b/lti_consumer/lti_1p3/extensions/rest_framework/serializers.py
index cc31a04..fb5d282 100644
--- a/lti_consumer/lti_1p3/extensions/rest_framework/serializers.py
+++ b/lti_consumer/lti_1p3/extensions/rest_framework/serializers.py
@@ -345,3 +345,22 @@ class LtiDlHtmlSerializer(serializers.Serializer):
     html = serializers.CharField()
     title = serializers.CharField(max_length=255, required=False)
     text = serializers.CharField(required=False)
+
+
+# pylint: disable=abstract-method
+class LtiDlImageSerializer(serializers.Serializer):
+    """
+    LTI Deep Linking - image Serializer.
+
+    This serializer implements validation for the Image content type.
+
+    Reference:
+    http://www.imsglobal.org/spec/lti-dl/v2p0#image
+    """
+    url = serializers.URLField(max_length=500)
+    title = serializers.CharField(max_length=255, required=False)
+    text = serializers.CharField(required=False)
+    icon = LtiDLIconPropertySerializer(required=False)
+    thumbnail = LtiDLIconPropertySerializer(required=False)
+    width = serializers.IntegerField(min_value=1, required=False)
+    height = serializers.IntegerField(min_value=1, required=False)
diff --git a/lti_consumer/templates/html/lti-dl/render_dl_content.html b/lti_consumer/templates/html/lti-dl/render_dl_content.html
index 7d2b71b..7e00c3b 100644
--- a/lti_consumer/templates/html/lti-dl/render_dl_content.html
+++ b/lti_consumer/templates/html/lti-dl/render_dl_content.html
@@ -10,6 +10,8 @@
                 {% include "html/lti-dl/render_link.html" with item=item attrs=item.attributes %}
             {% elif item.content_type == 'html' %}
                 {% include "html/lti-dl/render_html.html" with item=item attrs=item.attributes %}
+            {% elif item.content_type == 'image' %}
+                {% include "html/lti-dl/render_image.html" with item=item attrs=item.attributes %}
             {% endif %}
         {% endfor %}
     </body>
diff --git a/lti_consumer/templates/html/lti-dl/render_image.html b/lti_consumer/templates/html/lti-dl/render_image.html
new file mode 100644
index 0000000..8e66abf
--- /dev/null
+++ b/lti_consumer/templates/html/lti-dl/render_image.html
@@ -0,0 +1,14 @@
+{% if attrs.title %}<h2>{{ attrs.title }}</h2>{% endif %}
+{% if attrs.text %}<p>{{ attrs.text }}</p>{% endif %}
+
+{% if attrs.thumbnail %}
+<a href="{{ attrs.url }}" {% if attrs.title %}alt="{{ attrs.title }}"{% endif %}>
+    <img src="{{ attrs.thumbnail.url }}" {% if attrs.title %}alt="{{ attrs.title }}"{% endif %} width="{{ attrs.thumbnail.width }}" height="{{ attrs.thumbnail.height }}" />
+</a>
+{% elif attrs.icon %}
+<a href="{{ attrs.url }}" {% if attrs.title %}alt="{{ attrs.title }}"{% endif %}>
+    <img src="{{ attrs.icon.url }}" {% if attrs.title %}alt="{{ attrs.title }}"{% endif %} width="{{ attrs.icon.width }}" height="{{ attrs.icon.height }}" />
+</a>
+{% else %}
+<img src="{{ attrs.url }}" {% if attrs.title %}alt="{{ attrs.title }}" {% endif %}{% if attrs.width %}width="{{ attrs.width }}" {% endif %}{% if attrs.height %}height="{{ attrs.height }}"{% endif %} />
+{% endif %}
diff --git a/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py b/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py
index e095357..746626f 100644
--- a/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py
+++ b/lti_consumer/tests/unit/plugin/test_views_lti_deep_linking.py
@@ -350,6 +350,46 @@ class LtiDeepLinkingResponseEndpointTestCase(LtiDeepLinkingTestCase):
         content_item, is_valid = test_data
         self._content_type_validation_test_helper(content_item, is_valid)
 
+    @ddt.data(
+        ({"type": "image"}, False),
+        ({"type": "image", "url": "http://ex.com/image"}, True),
+        ({
+            "type": "image",
+            "url": "http://ex.com/image",
+            "text": "This is a link",
+            "title": "This is a link"
+        }, True),
+
+        # invalid icon
+        ({"type": "image", "url": "https://example.com/image", "icon": {}}, False),
+        # valid icon
+        ({
+            "type": "image",
+            "url": "https://example.com/image",
+            "icon": {"url": "https://ex.com/icon", "width": 20, "height": 20}
+        }, True),
+
+        # invalid thumbnail
+        ({"type": "image", "url": "https://example.com/image", "thumbnail": {}}, False),
+        # valid thumbnail
+        ({
+            "type": "image",
+            "url": "https://example.com/image",
+            "thumbnail": {"url": "https://ex.com/icon", "width": 20, "height": 20}
+        }, True),
+    )
+    def test_image_content_type(self, test_data):
+        """
+        Tests validation for `image` content type.
+
+        Args:
+            self
+            test_data (tuple): 1st element is the datastructure to test,
+                and the second one indicates wether it's valid or not.
+        """
+        content_item, is_valid = test_data
+        self._content_type_validation_test_helper(content_item, is_valid)
+
 
 @ddt.ddt
 class LtiDeepLinkingContentEndpointTestCase(LtiDeepLinkingTestCase):
@@ -508,3 +548,59 @@ class LtiDeepLinkingContentEndpointTestCase(LtiDeepLinkingTestCase):
             self.assertContains(resp, '<p>{}</p>'.format(test_data['text']))
 
         self.assertContains(resp, test_data['html'])
+
+    @ddt.data(
+        {'url': 'https://path.to.image'},
+        {'url': 'https://path.to.image', 'title': 'With Title'},
+        {'url': 'https://path.to.image', 'title': 'With Title', 'text': 'With Text'},
+        {
+            'url': 'https://path.to.image', 'title': 'With Title', 'text': 'With Text',
+            'width': '400px', 'height': '200px',
+        },
+        {
+            'url': 'https://path.to.image', 'title': 'With Title', 'text': 'With Text',
+            'icon': {'url': 'https://path.to.icon', 'width': '20px', 'height': '20px'},
+        },
+        {
+            'url': 'https://path.to.image', 'title': 'With Title', 'text': 'With Text',
+            'thumbnail': {'url': 'https://path.to.thumbnail', 'width': '20px', 'height': '20px'},
+        },
+    )
+    @patch('lti_consumer.plugin.views.has_block_access', return_value=True)
+    def test_dl_content_type_image(self, test_data, has_block_access):  # pylint: disable=unused-argument
+        """
+        Test if image content type successfully rendered.
+        """
+        attributes = {'type': LtiDlContentItem.IMAGE}
+        attributes.update(test_data)
+
+        LtiDlContentItem.objects.create(
+            lti_configuration=self.lti_config,
+            content_type=LtiDlContentItem.IMAGE,
+            attributes=attributes
+        )
+
+        resp = self.client.get(self.url)
+        self.assertEqual(resp.status_code, 200)
+
+        if test_data.get('title'):
+            self.assertContains(resp, '<h2>{}</h2>'.format(test_data['title']))
+            self.assertContains(resp, 'alt="{}"'.format(test_data['title']))
+
+        if test_data.get('text'):
+            self.assertContains(resp, '<p>{}</p>'.format(test_data['text']))
+
+        if test_data.get('thumbnail'):
+            self.assertContains(resp, '<a href="{}"'.format(test_data['url']))
+            self.assertContains(resp, '<img src="{}"'.format(test_data['thumbnail']['url']))
+        elif test_data.get('icon'):
+            self.assertContains(resp, '<a href="{}"'.format(test_data['url']))
+            self.assertContains(resp, '<img src="{}"'.format(test_data['icon']['url']))
+        else:
+            self.assertContains(resp, '<img src="{}"'.format(test_data['url']))
+
+        if test_data.get('width'):
+            self.assertContains(resp, 'width="{}"'.format(test_data['width']))
+
+        if test_data.get('height'):
+            self.assertContains(resp, 'height="{}"'.format(test_data['height']))
-- 
GitLab