-
-
{{ category }}
-
+{% for category in category_order %}
+
-
-
-{% with ltis=tools %}
- {% include "lti_list.html" %}
-{% endwith %}
-
+ {% with ltis=tools_by_category[category] %}
+ {% include "lti_list.html" %}
+ {% endwith %}
{% endfor %}
{% include "custom_tools.html" ignore missing %}
diff --git a/tests.py b/tests.py
index 5059e0a..c7c09e4 100644
--- a/tests.py
+++ b/tests.py
@@ -1,6 +1,7 @@
+from json.decoder import JSONDecodeError
import logging
import unittest
-from urllib import urlencode
+from urllib.parse import urlencode
import canvasapi
import oauthlib.oauth1
@@ -220,7 +221,7 @@ def test_index_no_auth(self, m):
self.assert_template_used("error.html")
self.assertIn(
- "Authentication error, please refresh and try again", response.data
+ b"Authentication error, please refresh and try again", response.data
)
def test_index_api_key_none(self, m):
@@ -234,7 +235,7 @@ def test_index_api_key_none(self, m):
self.assert_200(response)
self.assert_template_used("error.html")
self.assertIn(
- "Authentication error: missing API key. Please refresh and try again.",
+ b"Authentication error: missing API key. Please refresh and try again.",
response.data,
)
@@ -276,7 +277,7 @@ def test_index_api_key_invalid(self, m):
self.assert_200(response)
self.assert_template_used("error.html")
self.assertIn(
- "You are not enrolled in this course as a Teacher or Designer.",
+ b"You are not enrolled in this course as a Teacher or Designer.",
response.data,
)
@@ -322,7 +323,7 @@ def test_index_no_canvas_conn(self, m):
self.assert_200(response)
self.assert_template_used("error.html")
self.assertIn(
- "Couldn't connect to Canvas, please refresh and try again",
+ b"Couldn't connect to Canvas, please refresh and try again",
response.data,
)
@@ -366,7 +367,7 @@ def test_index_whitelist_error(self, m, filter_tool_list):
self.assert_template_used("error.html")
self.assertIn(
- "There is something wrong with the whitelist.json file", response.data
+ b"There is something wrong with the whitelist.json file", response.data
)
@patch("lti.filter_tool_list")
@@ -410,7 +411,7 @@ def test_index_canvas_error(self, m, filter_tool_list):
response = self.client.get(self.generate_launch_request(url_for("index")))
self.assert_template_used("error.html")
- self.assertIn("Couldn't connect to Canvas", response.data)
+ self.assertIn(b"Couldn't connect to Canvas", response.data)
def test_index(self, m):
with self.client.session_transaction() as sess:
@@ -530,7 +531,7 @@ def test_oauth_login_cancelled(self, m):
self.assert_200(response)
self.assert_template_used("error.html")
self.assertIn(
- "Authentication error, please refresh and try again.", response.data
+ b"Authentication error, please refresh and try again.", response.data
)
def test_oauth_login_no_access_token(self, m):
@@ -552,7 +553,7 @@ def test_oauth_login_no_access_token(self, m):
self.assert_200(response)
self.assert_template_used("error.html")
self.assertIn(
- "Authentication error, please refresh and try again.", response.data
+ b"Authentication error, please refresh and try again.", response.data
)
def test_oauth_login_new_user(self, m):
@@ -650,7 +651,7 @@ def test_oauth_login_new_user_db_error(self, m):
self.assert_template_used("error.html")
self.assertIn(
- "Authentication error, please refresh and try again.", response.data
+ b"Authentication error, please refresh and try again.", response.data
)
def test_oauth_login_existing_user(self, m):
@@ -758,7 +759,7 @@ def test_oauth_login_existing_user_db_error(self, m):
self.assert_200(response)
self.assert_template_used("error.html")
self.assertIn(
- "Authentication error, please refresh and try again.", response.data
+ b"Authentication error, please refresh and try again.", response.data
)
# refresh_access_token
@@ -1149,7 +1150,7 @@ def test_get_sessionless_url_is_course_nav_fail(self, m):
self.assert_200(response)
self.assert_template_used("error.html")
self.assertIn(
- "Error in a response from Canvas, please refresh and try again.",
+ b"Error in a response from Canvas, please refresh and try again.",
response.data,
)
@@ -1178,7 +1179,7 @@ def test_get_sessionless_url_is_course_nav_succeed(self, m):
)
self.assert_200(response)
- self.assertEqual(response.data, launch_url)
+ self.assertEqual(response.data, launch_url.encode("utf-8"))
def test_get_sessionless_url_not_course_nav_fail(self, m):
with self.client.session_transaction() as sess:
@@ -1204,7 +1205,7 @@ def test_get_sessionless_url_not_course_nav_fail(self, m):
self.assert_200(response)
self.assert_template_used("error.html")
self.assertIn(
- "Error in a response from Canvas, please refresh and try again.",
+ b"Error in a response from Canvas, please refresh and try again.",
response.data,
)
@@ -1233,7 +1234,7 @@ def test_get_sessionless_url_not_course_nav_succeed(self, m):
)
self.assert_200(response)
- self.assertEqual(response.data, launch_url)
+ self.assertEqual(response.data, launch_url.encode("utf-8"))
class UtilsTests(unittest.TestCase):
@@ -1243,13 +1244,13 @@ def setUpClass(cls):
settings.whitelist = "whitelist.json"
def test_filter_tool_list_empty_file(self):
- with self.assertRaisesRegexp(ValueError, r"No JSON object could be decoded"):
- with patch("__builtin__.open", mock_open(read_data="")):
+ with self.assertRaises(JSONDecodeError):
+ with patch("builtins.open", mock_open(read_data="")):
utils.filter_tool_list(1, "password")
def test_filter_tool_list_empty_data(self):
- with self.assertRaisesRegexp(ValueError, r"whitelist\.json is empty"):
- with patch("__builtin__.open", mock_open(read_data="{}")):
+ with self.assertRaisesRegex(ValueError, r"whitelist\.json is empty"):
+ with patch("builtins.open", mock_open(read_data="{}")):
utils.filter_tool_list(1, "password")
@patch("canvasapi.canvas.Canvas.get_course")
diff --git a/utils.py b/utils.py
index ac7f083..dcaee44 100644
--- a/utils.py
+++ b/utils.py
@@ -7,6 +7,23 @@
import settings
+def get_tool_info(whitelist, tool_name):
+ """
+ Search the whitelist by tool name.
+
+ :returns: A dictionary , or none if no tool matching that name was found.
+ :rtype: dict or None
+ """
+ for category, category_tools in whitelist.items():
+ for tool_info in category_tools:
+ print(tool_info)
+ if tool_info.get("name") == tool_name:
+ tool_info.update({"category": category})
+ return tool_info
+
+ return None
+
+
def filter_tool_list(course_id, access_token):
"""
Filter tool list down to those on whitelist and sort by category.
@@ -31,38 +48,45 @@ def filter_tool_list(course_id, access_token):
course = canvas.get_course(course_id)
installed_tools = course.get_external_tools(include_parents=True)
-
tools_by_category = defaultdict(list)
for installed_tool in installed_tools:
- for tool in whitelist:
- if installed_tool.name != tool.get("name"):
- continue
-
- is_course_navigation = hasattr(installed_tool, "course_navigation")
-
- if tool.get("is_launchable", False):
- if is_course_navigation:
- sessionless_launch_url = installed_tool.get_sessionless_launch_url(
- launch_type="course_navigation"
- )
- else:
- sessionless_launch_url = installed_tool.get_sessionless_launch_url()
- else:
- sessionless_launch_url = None
+ tool_info = get_tool_info(whitelist, installed_tool.name)
+ if not tool_info:
+ continue
- tool_info = tool
- tool_info.update(
- {
- "id": installed_tool.id,
- "lti_course_navigation": is_course_navigation,
- "sessionless_launch_url": sessionless_launch_url,
- "screenshot": "screenshots/" + tool["screenshot"],
- }
- )
+ is_course_navigation = hasattr(installed_tool, "course_navigation")
- tools_by_category[tool.get("category", "Uncategorized")].append(tool_info)
-
- return tools_by_category
+ if tool_info.get("is_launchable", False):
+ if is_course_navigation:
+ sessionless_launch_url = installed_tool.get_sessionless_launch_url(
+ launch_type="course_navigation"
+ )
+ else:
+ sessionless_launch_url = installed_tool.get_sessionless_launch_url()
+ else:
+ sessionless_launch_url = None
+
+ tool_info.update(
+ {
+ "id": installed_tool.id,
+ "lti_course_navigation": is_course_navigation,
+ "sessionless_launch_url": sessionless_launch_url,
+ "screenshot": "screenshots/" + tool_info["screenshot"],
+ }
+ )
+
+ tools_by_category[tool_info.get("category", "Uncategorized")].append(tool_info)
+
+ for category, tools in tools_by_category.items():
+ # Determine tool order based on order in whitelist
+ order = {
+ tool.get("display_name"): i for i, tool in enumerate(whitelist[category])
+ }
+ tools_by_category[category] = sorted(
+ tools, key=lambda k: order[k["display_name"]]
+ )
+
+ return tools_by_category, list(whitelist.keys())
def slugify(value):
diff --git a/whitelist.json.template b/whitelist.json.template
index c717551..6f86482 100644
--- a/whitelist.json.template
+++ b/whitelist.json.template
@@ -1,15 +1,16 @@
-[
- {
- "display_name": "Name to Display",
- "name": "Installed Tool Name",
- "tool_id": "tool_id",
- "allowed_roles": [""],
- "desc": "Tool Description",
- "screenshot": "screenshot.png",
- "logo": "logo.svg",
- "filter_by": ["all"],
- "docs_url": "https://example.com/tool/docs/",
- "is_launchable": true,
- "category": "Course Tool"
- }
-]
+{
+ "Course Tool": [
+ {
+ "display_name": "Name to Display",
+ "name": "Installed Tool Name",
+ "tool_id": "tool_id",
+ "allowed_roles": [""],
+ "desc": "Tool Description",
+ "screenshot": "screenshot.png",
+ "logo": "logo.svg",
+ "filter_by": ["all"],
+ "docs_url": "https://example.com/tool/docs/",
+ "is_launchable": true
+ }
+ ]
+}