diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 00000000..13566b81 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/canvasapi.iml b/.idea/canvasapi.iml new file mode 100644 index 00000000..de5999c7 --- /dev/null +++ b/.idea/canvasapi.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dictionaries/ncdegroot.xml b/.idea/dictionaries/ncdegroot.xml new file mode 100644 index 00000000..01f513ad --- /dev/null +++ b/.idea/dictionaries/ncdegroot.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 00000000..97626ba4 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 00000000..5486f22c --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,112 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 00000000..105ce2da --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 00000000..0f460a16 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 00000000..28e4970d --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/ruff.xml b/.idea/ruff.xml new file mode 100644 index 00000000..ed9f86d9 --- /dev/null +++ b/.idea/ruff.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 00000000..35eb1ddf --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/canvasapi/course.py b/canvasapi/course.py index 9d0cacf7..d962607a 100644 --- a/canvasapi/course.py +++ b/canvasapi/course.py @@ -45,6 +45,7 @@ normalize_bool, obj_or_id, obj_or_str, + obj_or_id_or_sis_str, ) @@ -730,7 +731,7 @@ def enroll_user(self, user, enrollment_type=None, **kwargs): from canvasapi.enrollment import Enrollment from canvasapi.user import User - kwargs["enrollment[user_id]"] = obj_or_id(user, "user", (User,)) + kwargs["enrollment[user_id]"] = obj_or_id_or_sis_str(user, "user", (User,)) if enrollment_type: warnings.warn( diff --git a/canvasapi/section.py b/canvasapi/section.py index 8046b648..e07b848e 100644 --- a/canvasapi/section.py +++ b/canvasapi/section.py @@ -4,7 +4,7 @@ from canvasapi.progress import Progress from canvasapi.submission import GroupedSubmission, Submission from canvasapi.user import User -from canvasapi.util import combine_kwargs, normalize_bool, obj_or_id +from canvasapi.util import combine_kwargs, normalize_bool, obj_or_id, obj_or_id_or_sis_str class Section(CanvasObject): @@ -94,7 +94,7 @@ def enroll_user(self, user, **kwargs): :rtype: :class:`canvasapi.enrollment.Enrollment` """ - kwargs["enrollment[user_id]"] = obj_or_id(user, "user", (User,)) + kwargs["enrollment[user_id]"] = obj_or_id_or_sis_str(user, "user", (User,)) response = self._requester.request( "POST", diff --git a/canvasapi/util.py b/canvasapi/util.py index 21c2a02b..51184575 100644 --- a/canvasapi/util.py +++ b/canvasapi/util.py @@ -166,6 +166,50 @@ def obj_or_str(obj, attr, object_types): raise TypeError("Parameter {} must be of type {}.".format(obj, obj_type_list)) +def obj_or_id_or_sis_str(parameter, param_name, object_types): + """ + Accepts either an int (or long or str representation of an integer) or + a 'sis_*_id:[some id]' format string or an object. If it is an int or + a format string, return it. + If it is an object and the object is of correct type, return the object's id. Otherwise, + throw an exception. + + :param parameter: int, str, long, or object + :param param_name: str + :param object_types: tuple + :rtype: int + """ + from canvasapi.user import User + + try: + return int(parameter) + except (ValueError, TypeError): + # Special case where 'self' is a valid ID of a User object + if User in object_types and parameter == "self": + return parameter + + if ( + isinstance(parameter, str) + and parameter.startswith("sis_") + and "_id:" in parameter + ): + # not foolproof + return parameter + + for obj_type in object_types: + if isinstance(parameter, obj_type): + try: + return int(parameter.id) + except Exception: + break + + obj_type_list = ",".join([obj_type.__name__ for obj_type in object_types]) + message = "Parameter {} must be of type {} or int or a sis_*_id: format string".format( + param_name, obj_type_list + ) + raise TypeError(message) + + def get_institution_url(base_url): """ Clean up a given base URL. diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 00000000..4ac1d813 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,17 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. + +[[package]] +name = "poetry-core" +version = "1.9.1" +description = "Poetry PEP 517 Build Backend" +optional = false +python-versions = "<4.0,>=3.8" +files = [ + {file = "poetry_core-1.9.1-py3-none-any.whl", hash = "sha256:6f45dd3598e0de8d9b0367360253d4c5d4d0110c8f5c71120a14f0e0f116c1a0"}, + {file = "poetry_core-1.9.1.tar.gz", hash = "sha256:7a2d49214bf58b4f17f99d6891d947a9836c9899a67a5069f52d7b67217f61b8"}, +] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<4.0.0" +content-hash = "07ac5b6df21285b34ef14be0c8ad2467701926876a25fce627798d9d5be51f4a" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..f8e41bab --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,16 @@ +[tool.poetry] +name = "canvasapi" +version = "3.3.0" +description = "Fork to support sis_login_id" +authors = ["Nico de Groot "] +readme = "README.md" + +[tool.poetry.dependencies] +python = ">=3.9,<4.0.0" + +[tool.poetry.group.dev.dependencies] +poetry-core = "*" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/test_course.py b/tests/test_course.py index ec554c5a..dbbf93eb 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -229,6 +229,16 @@ def test_enroll_user(self, m): self.assertTrue(hasattr(enrollment_by_obj, "type")) self.assertEqual(enrollment_by_obj.type, enrollment_type) + # by sis_str: str could be {'login', + enrollment_by_sis_str = self.course.enroll_user( + "sis_login_id:u123456", enrollment={"type": enrollment_type} + ) + + self.assertIsInstance(enrollment_by_sis_str, Enrollment) + self.assertTrue(hasattr(enrollment_by_sis_str, "type")) + self.assertEqual(enrollment_by_sis_str.type, enrollment_type) + + def test_enroll_user_legacy(self, m): warnings.simplefilter("always", DeprecationWarning) diff --git a/tests/test_section.py b/tests/test_section.py index cc088d18..9420a1d5 100644 --- a/tests/test_section.py +++ b/tests/test_section.py @@ -150,3 +150,12 @@ def test_enroll_user(self, m): self.assertIsInstance(enrollment_by_obj, Enrollment) self.assertTrue(hasattr(enrollment_by_obj, "type")) self.assertEqual(enrollment_by_obj.type, enrollment_type) + + # by user pecified as sis_login_id + enrollment_by_sis_str = self.section.enroll_user( + "sis_login_id:u123456" , enrollment={"type": enrollment_type} + ) + + self.assertIsInstance(enrollment_by_sis_str, Enrollment) + self.assertTrue(hasattr(enrollment_by_sis_str, "type")) + self.assertEqual(enrollment_by_sis_str.type, enrollment_type) diff --git a/tests/test_util.py b/tests/test_util.py index cd85648b..652ccedd 100644 --- a/tests/test_util.py +++ b/tests/test_util.py @@ -16,6 +16,7 @@ normalize_bool, obj_or_id, obj_or_str, + obj_or_id_or_sis_str, ) from tests import settings from tests.util import cleanup_file, register_uris @@ -470,6 +471,72 @@ def test_obj_or_str_invalid_obj_type(self, m): with self.assertRaises(TypeError): obj_or_str("user", "name", (User,)) + # obj_or_id_or_sis_str : + # same set of tests as used in the obj_or_id tests, + # added _sis_str_{valid,invalid} tests + def test_obj_or_id_or_sis_str_int(self, m): + user_id = obj_or_id_or_sis_str(1, "user_id", (User,)) + + self.assertIsInstance(user_id, int) + self.assertEqual(user_id, 1) + + def test_obj_or_id_or_sis_str_str_valid(self, m): + user_id = obj_or_id_or_sis_str("1", "user_id", (User,)) + + self.assertIsInstance(user_id, int) + self.assertEqual(user_id, 1) + + def test_obj_or_id_or_sis_str_str_invalid(self, m): + with self.assertRaises(TypeError): + obj_or_id_or_sis_str("1a", "user_id", (User,)) + + def test_obj_or_id_or_sis_str_sis_str_valid(self, m): + user_id = obj_or_id_or_sis_str("sis_login_id:a_login_id", "user_id", (User,)) + + self.assertEqual(user_id, "sis_login_id:a_login_id") + + def test_obj_or_id_or_sis_str_sis_str_invalid(self, m): + with self.assertRaises(TypeError): + obj_or_id_or_sis_str("sys_login_id:a_login_id", "user_id", (User,)) + + def test_obj_or_id_or_sis_str_obj(self, m): + register_uris({"user": ["get_by_id"]}, m) + + user = self.canvas.get_user(1) + + user_id = obj_or_id_or_sis_str(user, "user_id", (User,)) + + self.assertIsInstance(user_id, int) + self.assertEqual(user_id, 1) + + def test_obj_or_id_or_sis_str_obj_no_id(self, m): + register_uris({"user": ["course_nickname"]}, m) + + nick = self.canvas.get_course_nickname(1) + + with self.assertRaises(TypeError): + obj_or_id_or_sis_str(nick, "nickname_id", (CourseNickname,)) + + def test_obj_or_id_or_sis_str_multiple_objs(self, m): + register_uris({"user": ["get_by_id"]}, m) + + user = self.canvas.get_user(1) + + user_id = obj_or_id_or_sis_str(user, "user_id", (CourseNickname, User)) + + self.assertIsInstance(user_id, int) + self.assertEqual(user_id, 1) + + def test_obj_or_id_or_sis_str_user_self(self, m): + user_id = obj_or_id_or_sis_str("self", "user_id", (User,)) + + self.assertIsInstance(user_id, str) + self.assertEqual(user_id, "self") + + def test_obj_or_id_or_sis_str_nonuser_self(self, m): + with self.assertRaises(TypeError): + obj_or_id_or_sis_str("self", "user_id", (CourseNickname,)) + # get_institution_url() def test_get_institution_url(self, m): correct_url = "https://my.canvas.edu"