Repository: ucfopen/canvasapi

Issue:
Create an External Tool LTI 1.3 Support
`Course.create_external_tool()` has four required parameters: `name`, `privacy_level`, `consumer_key`, and `shared_secret`. 

When Instructure added LTI 1.3 support, they added an additional "required" parameter called `client_id`. Although all five of these parameters are marked as "required" in the documentation, the actual requirement is to use either `client_id` **_or_** the other four params.

We need to update `create_external_tool()` to accept `client_id` without requiring the other four parameters.

Note that this will be a breaking change for anyone using positional arguments in `create_external_tool()`, but should work without issue for those already using keyword args.

Discussion:
coveralls: 
[![Coverage Status](https://coveralls.io/builds/32279752/badge)](https://coveralls.io/builds/32279752)

Coverage remained the same at 100.0% when pulling **3192df00b0c0b97f627c95e5ec5aec4a55f399e4 on Thetwam:issue/402-create-external-tool-client-id** into **bc55e5e755acac52e7329018ef10c548de178032 on ucfopen:develop**.


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33aebfd..e007b2f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,7 +4,12 @@
 
 ### Bugfixes
 
-- Fixed an issue where Quiz.get_quiz_group incorrectly set `course_id` to the quiz ID.  (Thanks,[@hcolclou](https://github.com/hcolclou))
+- Fixed an issue where `Quiz.get_quiz_group` incorrectly set `course_id` to the quiz ID.  (Thanks,[@hcolclou](https://github.com/hcolclou))
+- Fixed an issue where `Course.create_external_tool` didn't accept `client_id` (LTI 1.3 support).
+
+### Breaking Changes
+
+- `Course.create_external_tool` no longer supports positional arguments for its required parameters. Use keyword arguments instead.
 
 ## [1.0.0] - 2020-07-09
 
diff --git a/canvasapi/course.py b/canvasapi/course.py
index d834187..29c06a1 100644
--- a/canvasapi/course.py
+++ b/canvasapi/course.py
@@ -275,9 +275,7 @@ class Course(CanvasObject):
         )
         return ExternalFeed(self._requester, response.json())
 
-    def create_external_tool(
-        self, name, privacy_level, consumer_key, shared_secret, **kwargs
-    ):
+    def create_external_tool(self, **kwargs):
         """
         Create an external tool in the current course.
 
@@ -286,24 +284,21 @@ class Course(CanvasObject):
 
         :param name: The name of the tool
         :type name: str
-        :param privacy_level: What information to send to the external
-            tool. Options are "anonymous", "name_only", "public"
-        :type privacy_level: str
-        :param consumer_key: The consumer key for the external tool
-        :type consumer_key: str
-        :param shared_secret: The shared secret with the external tool
-        :type shared_secret: str
+
         :rtype: :class:`canvasapi.external_tool.ExternalTool`
         """
         from canvasapi.external_tool import ExternalTool
 
+        required_params = ("name", "privacy_level", "consumer_key", "shared_secret")
+        if "client_id" not in kwargs and not all(x in kwargs for x in required_params):
+            raise RequiredFieldMissing(
+                "Must pass either `client_id` parameter or "
+                "`name`, `privacy_level`, `consumer_key`, and `shared_secret` parameters."
+            )
+
         response = self._requester.request(
             "POST",
             "courses/{}/external_tools".format(self.id),
-            name=name,
-            privacy_level=privacy_level,
-            consumer_key=consumer_key,
-            shared_secret=shared_secret,
             _kwargs=combine_kwargs(**kwargs),
         )
         response_json = response.json()


Test patch:
diff --git a/tests/test_course.py b/tests/test_course.py
index 78027cb..a45d3a6 100644
--- a/tests/test_course.py
+++ b/tests/test_course.py
@@ -883,6 +883,25 @@ class TestCourse(unittest.TestCase):
         self.assertTrue(hasattr(response, "id"))
         self.assertEqual(response.id, 20)
 
+    def test_create_external_tool_client_id(self, m):
+        register_uris({"external_tool": ["create_tool_course"]}, m)
+
+        response = self.course.create_external_tool(client_id="10000000000001")
+
+        self.assertIsInstance(response, ExternalTool)
+        self.assertTrue(hasattr(response, "id"))
+        self.assertEqual(response.id, 20)
+
+    def test_create_external_tool_no_params(self, m):
+        with self.assertRaises(RequiredFieldMissing):
+            self.course.create_external_tool()
+
+    def test_create_external_tool_missing_params(self, m):
+        with self.assertRaises(RequiredFieldMissing):
+            self.course.create_external_tool(
+                consumer_key="key", shared_secret="secret",
+            )
+
     # get_collaborations
     def test_get_collaborations(self, m):
         register_uris({"course": ["get_collaborations"]}, m)