From 58da5571b8469b594a924533c00008b23334f1bc Mon Sep 17 00:00:00 2001 From: Andrew Gardener Date: Wed, 4 Dec 2019 16:23:40 -0800 Subject: [PATCH 01/22] Add quiz statistics endpoint --- canvasapi/quiz.py | 26 +++++ tests/fixtures/quiz.json | 228 +++++++++++++++++++++++++++++++++++++++ tests/test_quiz.py | 38 +++++++ 3 files changed, 292 insertions(+) diff --git a/canvasapi/quiz.py b/canvasapi/quiz.py index 4e149c06..2880211a 100644 --- a/canvasapi/quiz.py +++ b/canvasapi/quiz.py @@ -377,6 +377,26 @@ def get_quiz_submission(self, quiz_submission, **kwargs): return QuizSubmission(self._requester, response_json) + def get_statistics(self, **kwargs): + """ + Get statistics for for all quiz versions, or the latest quiz version. + + :calls: `GET /api/v1/courses/:course_id/quizzes/:quiz_id/statistics \ + `_ + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.quiz.QuizStatistic` + """ + return PaginatedList( + QuizStatistic, + self._requester, + "GET", + "courses/{}/quizzes/{}/statistics".format(self.course_id, self.id), + {"course_id": self.course_id}, + _root="quiz_statistics", + _kwargs=combine_kwargs(**kwargs), + ) + def get_submissions(self, **kwargs): """ Get a list of all submissions for this quiz. @@ -452,6 +472,12 @@ def set_extensions(self, quiz_extensions, **kwargs): ] +@python_2_unicode_compatible +class QuizStatistic(CanvasObject): + def __str__(self): + return "Quiz Statistic {}".format(self.id) + + @python_2_unicode_compatible class QuizSubmission(CanvasObject): def __str__(self): diff --git a/tests/fixtures/quiz.json b/tests/fixtures/quiz.json index 22e753a2..1a0c09e8 100644 --- a/tests/fixtures/quiz.json +++ b/tests/fixtures/quiz.json @@ -303,6 +303,234 @@ }, "status_code": 200 }, + "get_statistics": { + "method": "GET", + "endpoint": "courses/1/quizzes/1/statistics", + "data": { + "quiz_statistics": [ + { + "id": "1", + "url": "http://localhost:3000/api/v1/courses/12/quizzes/43/statistics", + "html_url": "http://localhost:3000/courses/12/quizzes/43/statistics", + "multiple_attempts_exist": false, + "generated_at": "2019-11-20T18:48:20Z", + "includes_all_versions": false, + "includes_sis_ids": true, + "points_possible": null, + "anonymous_survey": false, + "speed_grader_url": null, + "quiz_submissions_zip_url": "http://localhost:3000/courses/12/quizzes/43/submissions?zip=1", + "question_statistics": [ + { + "id": "47", + "question_type": "multiple_dropdowns_question", + "question_text": "\n

What is your year of study? [year]

\n

What is your program? [program]

\n ", + "position": 1, + "responses": 3, + "answered": 3, + "correct": 0, + "partially_correct": 2, + "incorrect": 1, + "answer_sets": [ + { + "id": "84cdc76cabf41bd7c961f6ab12f117d8", + "text": "year", + "answers": [ + { + "id": "6100", + "text": "1", + "correct": true, + "responses": 0, + "user_names": [] + }, + { + "id": "8629", + "text": "2", + "correct": false, + "responses": 1, + "user_names": [ + "Student12 Student12" + ] + }, + { + "id": "6110", + "text": "3", + "correct": false, + "responses": 2, + "user_names": [ + "Student11 Student11", + "Student10 Student10" + ] + }, + { + "id": "9262", + "text": "4", + "correct": false, + "responses": 0, + "user_names": [] + }, + { + "id": "279", + "text": "5", + "correct": false, + "responses": 0, + "user_names": [] + }, + { + "id": "5721", + "text": "6+", + "correct": false, + "responses": 0, + "user_names": [] + } + ] + }, + { + "id": "a9c449d4fa44e9e5a41c574ae55ce4d9", + "text": "program", + "answers": [ + { + "id": "7270", + "text": "CompSci", + "correct": true, + "responses": 2, + "user_names": [ + "Student11 Student11", + "Student10 Student10" + ] + }, + { + "id": "2688", + "text": "Other", + "correct": false, + "responses": 1, + "user_names": [ + "Student12 Student12" + ] + } + ] + } + ] + }, + { + "id": "48", + "question_type": "multiple_choice_question", + "question_text": "How good are you at programming?", + "position": 2, + "responses": 3, + "answers": [ + { + "id": "1833", + "text": "Great", + "correct": true, + "responses": 0, + "user_names": [] + }, + { + "id": "7532", + "text": "Good", + "correct": false, + "responses": 2, + "user_names": [ + "Student12 Student12", + "Student10 Student10" + ] + }, + { + "id": "136", + "text": "Mediocre", + "correct": false, + "responses": 0, + "user_names": [] + }, + { + "id": "9694", + "text": "Bad", + "correct": false, + "responses": 1, + "user_names": [ + "Student11 Student11" + ] + }, + { + "id": "6452", + "text": "Terrible", + "correct": false, + "responses": 0, + "user_names": [] + } + ], + "answered_student_count": 3, + "top_student_count": 2, + "middle_student_count": 0, + "bottom_student_count": 1, + "correct_student_count": 0, + "incorrect_student_count": 3, + "correct_student_ratio": 0.0, + "incorrect_student_ratio": 1.0, + "correct_top_student_count": 0, + "correct_middle_student_count": 0, + "correct_bottom_student_count": 0, + "variance": 0.0, + "stdev": 0.0, + "difficulty_index": 0.0, + "alpha": null, + "point_biserials": [ + { + "answer_id": 1833, + "point_biserial": null, + "correct": true, + "distractor": false + }, + { + "answer_id": 7532, + "point_biserial": -0.49999999999999994, + "correct": false, + "distractor": true + }, + { + "answer_id": 136, + "point_biserial": null, + "correct": false, + "distractor": true + }, + { + "answer_id": 9694, + "point_biserial": 0.49999999999999994, + "correct": false, + "distractor": true + }, + { + "answer_id": 6452, + "point_biserial": null, + "correct": false, + "distractor": true + } + ] + } + ], + "submission_statistics": { + "scores": { + "0": 1, + "25": 2 + }, + "score_average": 0.3333333333333333, + "score_high": 0.5, + "score_low": 0.0, + "score_stdev": 0.2357022603955158, + "correct_count_average": 0.0, + "incorrect_count_average": 1.333333333333333, + "duration_average": 6.0, + "unique_count": 3 + }, + "links": { + "quiz": "http://localhost:3000/api/v1/courses/12/quizzes/43" + } + } + ] + }, + "status_code": 200 + }, "create_report": { "method": "POST", "endpoint": "courses/1/quizzes/1/reports", diff --git a/tests/test_quiz.py b/tests/test_quiz.py index 826e55e9..bf13a87c 100644 --- a/tests/test_quiz.py +++ b/tests/test_quiz.py @@ -9,6 +9,7 @@ from canvasapi.exceptions import RequiredFieldMissing from canvasapi.quiz import ( Quiz, + QuizStatistic, QuizSubmission, QuizSubmissionQuestion, QuizQuestion, @@ -296,6 +297,22 @@ def test_get_quiz_report(self, m): self.assertIsInstance(report, QuizReport) self.assertEqual(report.quiz_id, 1) + # get_quiz_report + def test_get_statistics(self, m): + register_uris({"quiz": ["get_statistics"]}, m) + + statistics = self.quiz.get_statistics() + + self.assertIsInstance(statistics, PaginatedList) + + statistic_list = [statistic for statistic in statistics] + + self.assertEqual(len(statistic_list), 1) + self.assertIsInstance(statistic_list[0], QuizStatistic) + self.assertEqual(statistic_list[0].id, "1") + self.assertTrue(hasattr(statistic_list[0], "question_statistics")) + self.assertEqual(len(statistic_list[0].question_statistics), 2) + # get_quiz_submission def test_get_quiz_submission(self, m): register_uris({"quiz": ["get_quiz_submission"]}, m) @@ -595,6 +612,27 @@ def test_edit(self, m): self.assertEqual(self.question.question_name, question_dict["question_name"]) +@requests_mock.Mocker() +class TestQuizStatistic(unittest.TestCase): + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + register_uris( + {"course": ["get_by_id"], "quiz": ["get_by_id", "get_statistics"]}, m + ) + + self.course = self.canvas.get_course(1) + self.quiz = self.course.get_quiz(1) + self.quiz_statistics = self.quiz.get_statistics() + self.quiz_statistic = self.quiz_statistics[0] + + # __str__() + def test__str__(self, m): + string = str(self.quiz_statistic) + self.assertIsInstance(string, str) + + @requests_mock.Mocker() class TestQuizSubmissionEvent(unittest.TestCase): def setUp(self): From e5cea816cdf94483cf61efe1b5e45fe310a6b450 Mon Sep 17 00:00:00 2001 From: Matthew Emond Date: Thu, 2 Jan 2020 12:01:42 -0500 Subject: [PATCH 02/22] add .python-version to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index e829f1b4..3cfe0c52 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ \#*# .vscode/settings.json setup.sh +.python-version From ab1e4cd7a2abaaf39d36bd06420d56b5361d9209 Mon Sep 17 00:00:00 2001 From: Matthew Emond Date: Thu, 2 Jan 2020 12:18:38 -0500 Subject: [PATCH 03/22] Add @andrew-gardener's contribution to changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ccfacf69..7f7901e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Change Log +## [Unreleased] + +### New Endpoint Coverage + +- Quiz Statistics (Thanks, [@andrew-gardener](https://github.com/andrew-gardener)) + ## [0.15.0] - 2019-11-19 ### New Endpoint Coverage @@ -403,6 +409,7 @@ Huge thanks to [@liblit](https://github.com/liblit) for lots of issues, suggesti - Fixed some incorrectly defined parameters - Fixed an issue where tests would fail due to an improperly configured requires block +[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v0.15.0...develop [0.15.0]: https://github.com/ucfopen/canvasapi/compare/v0.14.0...v0.15.0 [0.14.0]: https://github.com/ucfopen/canvasapi/compare/v0.13.0...v0.14.0 [0.13.0]: https://github.com/ucfopen/canvasapi/compare/v0.12.0...v0.13.0 From f977262a3f6a98d97339c8cb434065c931168bb9 Mon Sep 17 00:00:00 2001 From: Kenny Perez Date: Thu, 23 Jan 2020 14:20:37 -0500 Subject: [PATCH 04/22] Added method to get quiz assignment override --- canvasapi/course.py | 27 +++++++++++++++++++++++ canvasapi/quiz.py | 6 ++++++ tests/fixtures/course.json | 44 ++++++++++++++++++++++++++++++++++++++ tests/test_course.py | 29 ++++++++++++++++++++++++- tests/test_quiz.py | 17 +++++++++++++++ 5 files changed, 122 insertions(+), 1 deletion(-) diff --git a/canvasapi/course.py b/canvasapi/course.py index 08d723bc..c181bdcf 100644 --- a/canvasapi/course.py +++ b/canvasapi/course.py @@ -1698,6 +1698,33 @@ def get_quiz(self, quiz): return Quiz(self._requester, quiz_json) + def get_quiz_overrides(self, **kwargs): + """ + Retrieve the actual due-at, unlock-at, \ + and available-at dates for quizzes based on \ + the assignment overrides active for the current API user. + + :calls: `GET /api/v1/courses/:course_id/quizzes/assignment_overrides \ + `_ + + :param quiz_assignment_overrides: An array of quiz IDs. If omitted, overrides \ + for all quizzes available to the operating user will be returned. + :type quiz: :class:`canvasapi.quiz.QuizAssignmentOverrideSet` or int + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.quiz.QuizAssignmentOverrideSet` + """ + from canvasapi.quiz import QuizAssignmentOverrideSet + + return PaginatedList( + QuizAssignmentOverrideSet, + self._requester, + "GET", + "courses/{}/quizzes/assignment_overrides".format(self.id), + _root="quiz_assignment_overrides", + _kwargs=combine_kwargs(**kwargs), + ) + def get_quizzes(self, **kwargs): """ Return a list of quizzes belonging to this course. diff --git a/canvasapi/quiz.py b/canvasapi/quiz.py index 2880211a..0cce770d 100644 --- a/canvasapi/quiz.py +++ b/canvasapi/quiz.py @@ -868,3 +868,9 @@ def unflag(self, validation_token=None, **kwargs): super(QuizSubmissionQuestion, self).set_attributes(question) return True + + +@python_2_unicode_compatible +class QuizAssignmentOverrideSet(CanvasObject): + def __str__(self): + return "Overrides for quiz_id {}".format(self.quiz_id) diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json index 0fab725e..faf198b9 100644 --- a/tests/fixtures/course.json +++ b/tests/fixtures/course.json @@ -409,6 +409,50 @@ }, "status_code": 200 }, + "get_quiz_overrides": { + "method": "GET", + "endpoint": "courses/1/quizzes/assignment_overrides", + "data": { + "quiz_assignment_overrides": [{ + "quiz_id": "1", + "due_dates": [{ + "id": 1, + "due_at": "2014-02-21T06:59:59Z", + "unlock_at": null, + "lock_at": "2014-02-21T06:59:59Z", + "title": "Project X", + "base": true + }], + "all_dates": [{ + "id": 1, + "due_at": "2014-02-21T06:59:59Z", + "unlock_at": null, + "lock_at": "2014-02-21T06:59:59Z", + "title": "Project X", + "base": true + }] + },{ + "quiz_id": "2", + "due_dates": [{ + "id": 1, + "due_at": "2014-02-21T06:59:59Z", + "unlock_at": null, + "lock_at": "2014-02-21T06:59:59Z", + "title": "Project X", + "base": true + }], + "all_dates": [{ + "id": 1, + "due_at": "2014-02-21T06:59:59Z", + "unlock_at": null, + "lock_at": "2014-02-21T06:59:59Z", + "title": "Project X", + "base": true + }] + }] + }, + "status_code": 200 + }, "get_recent_students": { "method": "GET", "endpoint": "courses/1/recent_students", diff --git a/tests/test_course.py b/tests/test_course.py index b07c9ba6..e3497b67 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -37,7 +37,7 @@ from canvasapi.outcome_import import OutcomeImport from canvasapi.paginated_list import PaginatedList from canvasapi.progress import Progress -from canvasapi.quiz import Quiz, QuizExtension +from canvasapi.quiz import Quiz, QuizExtension, QuizAssignmentOverrideSet from canvasapi.rubric import Rubric, RubricAssociation from canvasapi.section import Section from canvasapi.submission import GroupedSubmission, Submission @@ -308,6 +308,33 @@ def test_get_quiz_fail(self, m): with self.assertRaises(ResourceDoesNotExist): self.course.get_quiz(settings.INVALID_ID) + # get_quiz_overrides() + def test_get_quiz_overrides(self, m): + register_uris({"course": ["get_quiz_overrides"]}, m) + + overrides = self.course.get_quiz_overrides() + override_list = list(overrides) + + self.assertEqual(len(override_list), 2) + self.assertIsInstance(override_list[0], QuizAssignmentOverrideSet) + self.assertTrue(hasattr(override_list[0], "quiz_id")) + self.assertTrue(hasattr(override_list[0], "due_dates")) + self.assertTrue(hasattr(override_list[0], "all_dates")) + + self.assertTrue("id" in override_list[0].due_dates[0]) + self.assertTrue("due_at" in override_list[0].due_dates[0]) + self.assertTrue("unlock_at" in override_list[0].due_dates[0]) + self.assertTrue("lock_at" in override_list[0].due_dates[0]) + self.assertTrue("title" in override_list[0].due_dates[0]) + self.assertTrue("base" in override_list[0].due_dates[0]) + + self.assertTrue("id" in override_list[0].all_dates[0]) + self.assertTrue("due_at" in override_list[0].all_dates[0]) + self.assertTrue("unlock_at" in override_list[0].all_dates[0]) + self.assertTrue("lock_at" in override_list[0].all_dates[0]) + self.assertTrue("title" in override_list[0].all_dates[0]) + self.assertTrue("base" in override_list[0].all_dates[0]) + # get_quizzes() def test_get_quizzes(self, m): register_uris({"course": ["list_quizzes", "list_quizzes2"]}, m) diff --git a/tests/test_quiz.py b/tests/test_quiz.py index bf13a87c..4e87972a 100644 --- a/tests/test_quiz.py +++ b/tests/test_quiz.py @@ -16,6 +16,7 @@ QuizExtension, QuizSubmissionEvent, QuizReport, + QuizAssignmentOverrideSet, ) from canvasapi.quiz_group import QuizGroup from canvasapi.paginated_list import PaginatedList @@ -730,3 +731,19 @@ def test_unflag_manual_validation_token(self, m): self.assertIsInstance(result, bool) self.assertTrue(result) self.assertFalse(self.submission_question.flagged) + + +@requests_mock.Mocker() +class TestQuizAssignmentOverrideSet(unittest.TestCase): + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + self.override_set = QuizAssignmentOverrideSet( + self.canvas._Canvas__requester, + {"quiz_id": "1", "due_dates": None, "all_dates": None}, + ) + + # __str__() + def test__str__(self, m): + string = str(self.override_set) + self.assertIsInstance(string, str) From 598c3c9279566f076c1eb649b73219932ce083cc Mon Sep 17 00:00:00 2001 From: Kenny Perez Date: Tue, 4 Feb 2020 11:58:30 -0500 Subject: [PATCH 05/22] Added documentation for new method --- canvasapi/course.py | 2 +- docs/quiz-ref.rst | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/canvasapi/course.py b/canvasapi/course.py index c181bdcf..adfab2f6 100644 --- a/canvasapi/course.py +++ b/canvasapi/course.py @@ -1709,7 +1709,7 @@ def get_quiz_overrides(self, **kwargs): :param quiz_assignment_overrides: An array of quiz IDs. If omitted, overrides \ for all quizzes available to the operating user will be returned. - :type quiz: :class:`canvasapi.quiz.QuizAssignmentOverrideSet` or int + :type quiz: :class:`canvasapi.quiz.Quiz` or int :rtype: :class:`canvasapi.paginated_list.PaginatedList` of :class:`canvasapi.quiz.QuizAssignmentOverrideSet` diff --git a/docs/quiz-ref.rst b/docs/quiz-ref.rst index 74a24a47..df495d96 100644 --- a/docs/quiz-ref.rst +++ b/docs/quiz-ref.rst @@ -32,3 +32,24 @@ QuizSubmission .. autoclass:: canvasapi.quiz.QuizSubmission :members: + +=================== +QuizSubmissionEvent +=================== + +.. autoclass:: canvasapi.quiz.QuizSubmissionEvent + :members: + +====================== +QuizSubmissionQuestion +====================== + +.. autoclass:: canvasapi.quiz.QuizSubmissionQuestion + :members: + +========================= +QuizAssignmentOverrideSet +========================= + +.. autoclass:: canvasapi.quiz.QuizAssignmentOverrideSet + :members: From 20bf362c3aec58e28606ef24988327c3449700e2 Mon Sep 17 00:00:00 2001 From: Kenny Perez Date: Mon, 10 Feb 2020 13:10:29 -0500 Subject: [PATCH 06/22] Cleaned up tests removed bad docs and reordered doc code --- canvasapi/course.py | 8 ++------ docs/quiz-ref.rst | 14 +++++++------- tests/test_course.py | 22 +++++++++------------- 3 files changed, 18 insertions(+), 26 deletions(-) diff --git a/canvasapi/course.py b/canvasapi/course.py index adfab2f6..a3850fd1 100644 --- a/canvasapi/course.py +++ b/canvasapi/course.py @@ -1700,17 +1700,13 @@ def get_quiz(self, quiz): def get_quiz_overrides(self, **kwargs): """ - Retrieve the actual due-at, unlock-at, \ - and available-at dates for quizzes based on \ + Retrieve the actual due-at, unlock-at, + and available-at dates for quizzes based on the assignment overrides active for the current API user. :calls: `GET /api/v1/courses/:course_id/quizzes/assignment_overrides \ `_ - :param quiz_assignment_overrides: An array of quiz IDs. If omitted, overrides \ - for all quizzes available to the operating user will be returned. - :type quiz: :class:`canvasapi.quiz.Quiz` or int - :rtype: :class:`canvasapi.paginated_list.PaginatedList` of :class:`canvasapi.quiz.QuizAssignmentOverrideSet` """ diff --git a/docs/quiz-ref.rst b/docs/quiz-ref.rst index df495d96..32b779da 100644 --- a/docs/quiz-ref.rst +++ b/docs/quiz-ref.rst @@ -5,6 +5,13 @@ Quiz .. autoclass:: canvasapi.quiz.Quiz :members: +========================= +QuizAssignmentOverrideSet +========================= + +.. autoclass:: canvasapi.quiz.QuizAssignmentOverrideSet + :members: + ============= QuizExtension ============= @@ -46,10 +53,3 @@ QuizSubmissionQuestion .. autoclass:: canvasapi.quiz.QuizSubmissionQuestion :members: - -========================= -QuizAssignmentOverrideSet -========================= - -.. autoclass:: canvasapi.quiz.QuizAssignmentOverrideSet - :members: diff --git a/tests/test_course.py b/tests/test_course.py index e3497b67..287b35e9 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -321,19 +321,15 @@ def test_get_quiz_overrides(self, m): self.assertTrue(hasattr(override_list[0], "due_dates")) self.assertTrue(hasattr(override_list[0], "all_dates")) - self.assertTrue("id" in override_list[0].due_dates[0]) - self.assertTrue("due_at" in override_list[0].due_dates[0]) - self.assertTrue("unlock_at" in override_list[0].due_dates[0]) - self.assertTrue("lock_at" in override_list[0].due_dates[0]) - self.assertTrue("title" in override_list[0].due_dates[0]) - self.assertTrue("base" in override_list[0].due_dates[0]) - - self.assertTrue("id" in override_list[0].all_dates[0]) - self.assertTrue("due_at" in override_list[0].all_dates[0]) - self.assertTrue("unlock_at" in override_list[0].all_dates[0]) - self.assertTrue("lock_at" in override_list[0].all_dates[0]) - self.assertTrue("title" in override_list[0].all_dates[0]) - self.assertTrue("base" in override_list[0].all_dates[0]) + attributes = ("id", "due_at", "unlock_at", "lock_at", "title", "base") + + self.assertTrue( + all(attribute in override_list[0].due_dates[0] for attribute in attributes) + ) + + self.assertTrue( + all(attribute in override_list[0].all_dates[0] for attribute in attributes) + ) # get_quizzes() def test_get_quizzes(self, m): From 309210681e28c58256ce5999a513b8a70f8aba31 Mon Sep 17 00:00:00 2001 From: Kenny G Perez <34965892+kennygperez@users.noreply.github.com> Date: Tue, 25 Feb 2020 13:35:43 -0500 Subject: [PATCH 07/22] Added the create, edit and get late policy endpoints fixes #326 (#352) * Added the create, edit and get late policy endpoints fixes #326 Co-authored-by: Matthew Emond --- canvasapi/course.py | 68 ++++++++++++++++++++++++- docs/course-ref.rst | 9 +++- tests/fixtures/course.json | 45 +++++++++++++++++ tests/test_course.py | 101 ++++++++++++++++++++++++++++++++++++- 4 files changed, 218 insertions(+), 5 deletions(-) diff --git a/canvasapi/course.py b/canvasapi/course.py index a3850fd1..839958a1 100644 --- a/canvasapi/course.py +++ b/canvasapi/course.py @@ -358,6 +358,26 @@ def create_group_category(self, name, **kwargs): ) return GroupCategory(self._requester, response.json()) + def create_late_policy(self, **kwargs): + """ + Create a late policy. If the course already has a late policy, a bad_request + is returned since there can only be one late policy per course. + + :calls: `POST /api/v1/courses/:id/late_policy \ + `_ + + :rtype: :class:`canvasapi.course.LatePolicy` + """ + + response = self._requester.request( + "POST", + "courses/{}/late_policy".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + late_policy_json = response.json() + + return LatePolicy(self._requester, late_policy_json["late_policy"]) + def create_module(self, module, **kwargs): """ Create a new module. @@ -554,6 +574,25 @@ def edit_front_page(self, **kwargs): return Page(self._requester, page_json) + def edit_late_policy(self, **kwargs): + """ + Patch a late policy. No body is returned upon success. + + :calls: `PATCH /api/v1/courses/:id/late_policy \ + `_ + + :returns: True if Late Policy was updated successfully. False otherwise. + :rtype: bool + """ + + response = self._requester.request( + "PATCH", + "courses/{}/late_policy".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + + return response.status_code == 204 + def enroll_user(self, user, enrollment_type, **kwargs): """ Create a new user enrollment for a course or a section. @@ -1410,6 +1449,25 @@ def get_groups(self, **kwargs): _kwargs=combine_kwargs(**kwargs), ) + def get_late_policy(self, **kwargs): + """ + Returns the late policy for a course. + + :calls: `GET /api/v1/courses/:id/late_policy \ + `_ + + :rtype: :class:`canvasapi.course.LatePolicy` + """ + + response = self._requester.request( + "GET", + "courses/{}/late_policy".format(self.id), + _kwargs=combine_kwargs(**kwargs), + ) + late_policy_json = response.json() + + return LatePolicy(self._requester, late_policy_json["late_policy"]) + def get_licenses(self, **kwargs): """ Returns a paginated list of the licenses that can be applied to the @@ -2748,8 +2806,8 @@ def update(self, **kwargs): :calls: `PUT /api/v1/courses/:id \ `_ - :returns: True if the course was updated, False otherwise. - :rtype: bool + :returns: `True` if the course was updated, `False` otherwise. + :rtype: `bool` """ response = self._requester.request( "PUT", "courses/{}".format(self.id), _kwargs=combine_kwargs(**kwargs) @@ -2901,3 +2959,9 @@ def remove(self): "DELETE", "users/self/course_nicknames/{}".format(self.course_id) ) return CourseNickname(self._requester, response.json()) + + +@python_2_unicode_compatible +class LatePolicy(CanvasObject): + def __str__(self): + return "Late Policy {}".format(self.id) diff --git a/docs/course-ref.rst b/docs/course-ref.rst index f2df1d3d..3d43f672 100644 --- a/docs/course-ref.rst +++ b/docs/course-ref.rst @@ -10,4 +10,11 @@ CourseNickname ============== .. autoclass:: canvasapi.course.CourseNickname - :members: + :members: + +========== +LatePolicy +========== + +.. autoclass:: canvasapi.course.LatePolicy + :members: diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json index faf198b9..7346d0b5 100644 --- a/tests/fixtures/course.json +++ b/tests/fixtures/course.json @@ -2088,6 +2088,51 @@ }, "status_code": 200 }, + "get_late_policy": { + "method": "GET", + "endpoint": "courses/1/late_policy", + "data": { + "late_policy": { + "id": 123, + "course_id": 123, + "missing_submission_deduction_enabled": true, + "missing_submission_deduction": 12.34, + "late_submission_deduction_enabled": true, + "late_submission_deduction": 12.34, + "late_submission_interval": "hour", + "late_submission_minimum_percent_enabled": true, + "late_submission_minimum_percent": 12.34, + "created_at": "2012-07-01T23:59:00-06:00", + "updated_at": "2012-07-01T23:59:00-06:00" + } + }, + "status_code": 200 + }, + "edit_late_policy": { + "method": "PATCH", + "endpoint": "courses/1/late_policy", + "status_code": 204 + }, + "create_late_policy": { + "method": "POST", + "endpoint": "courses/1/late_policy", + "data": { + "late_policy": { + "id": 123, + "course_id": 123, + "missing_submission_deduction_enabled": true, + "missing_submission_deduction": 12.34, + "late_submission_deduction_enabled": true, + "late_submission_deduction": 12.34, + "late_submission_interval": "hour", + "late_submission_minimum_percent_enabled": true, + "late_submission_minimum_percent": 12.34, + "created_at": "2012-07-01T23:59:00-06:00", + "updated_at": "2012-07-01T23:59:00-06:00" + } + }, + "status_code": 200 + }, "get_licenses": { "method": "GET", "endpoint": "courses/1/content_licenses", diff --git a/tests/test_course.py b/tests/test_course.py index 287b35e9..d78219da 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -12,7 +12,7 @@ from canvasapi.assignment import Assignment, AssignmentGroup, AssignmentOverride from canvasapi.blueprint import BlueprintSubscription from canvasapi.blueprint import BlueprintTemplate -from canvasapi.course import Course, CourseNickname, Page +from canvasapi.course import Course, CourseNickname, Page, LatePolicy from canvasapi.discussion_topic import DiscussionTopic from canvasapi.gradebook_history import ( Day, @@ -487,7 +487,7 @@ def test_show_front_page(self, m): self.assertTrue(hasattr(front_page, "url")) self.assertTrue(hasattr(front_page, "title")) - # create_front_page() + # edit_front_page() def test_edit_front_page(self, m): register_uris({"course": ["edit_front_page"]}, m) @@ -497,6 +497,75 @@ def test_edit_front_page(self, m): self.assertTrue(hasattr(new_front_page, "url")) self.assertTrue(hasattr(new_front_page, "title")) + # edit_late_policy() + def test_edit_late_policy(self, m): + register_uris({"course": ["edit_late_policy"]}, m) + + late_policy_result = self.course.edit_late_policy( + late_policy={"missing_submission_deduction": 5} + ) + + self.assertTrue(late_policy_result) + + # get_late_policy + def test_get_late_policy(self, m): + register_uris({"course": ["get_late_policy"]}, m) + + late_policy = self.course.get_late_policy() + + self.assertIsInstance(late_policy, LatePolicy) + + attributes = ( + "id", + "course_id", + "missing_submission_deduction_enabled", + "missing_submission_deduction", + "late_submission_deduction_enabled", + "late_submission_deduction", + "late_submission_interval", + "late_submission_minimum_percent_enabled", + "late_submission_minimum_percent", + "created_at", + "updated_at", + ) + + for attribute in attributes: + self.assertTrue(hasattr(late_policy, attribute)) + + def test_create_late_policy(self, m): + register_uris({"course": ["create_late_policy"]}, m) + + late_policy = self.course.create_late_policy( + late_policy={ + "missing_submission_deduction_enabled": True, + "missing_submission_deduction": 12.34, + "late_submission_deduction_enabled": True, + "late_submission_deduction": 12.34, + "late_submission_interval": "hour", + "late_submission_minimum_percent_enabled": True, + "late_submission_minimum_percent": 12.34, + } + ) + + self.assertIsInstance(late_policy, LatePolicy) + + attributes = ( + "id", + "course_id", + "missing_submission_deduction_enabled", + "missing_submission_deduction", + "late_submission_deduction_enabled", + "late_submission_deduction", + "late_submission_interval", + "late_submission_minimum_percent_enabled", + "late_submission_minimum_percent", + "created_at", + "updated_at", + ) + + for attribute in attributes: + self.assertTrue(hasattr(late_policy, attribute)) + # get_page() def test_get_page(self, m): register_uris({"course": ["get_page"]}, m) @@ -1954,3 +2023,31 @@ def test_remove(self, m): self.assertIsInstance(deleted_nick, CourseNickname) self.assertTrue(hasattr(deleted_nick, "nickname")) + + +@requests_mock.Mocker() +class TestLatePolicy(unittest.TestCase): + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + self.late_policy = LatePolicy( + self.canvas._Canvas__requester, + { + "id": 123, + "course_id": 123, + "missing_submission_deduction_enabled": True, + "missing_submission_deduction": 12.34, + "late_submission_deduction_enabled": True, + "late_submission_deduction": 12.34, + "late_submission_interval": "hour", + "late_submission_minimum_percent_enabled": True, + "late_submission_minimum_percent": 12.34, + "created_at": "2012-07-01T23:59:00-06:00", + "updated_at": "2012-07-01T23:59:00-06:00", + }, + ) + + # __str__() + def test__str__(self, m): + string = str(self.late_policy) + self.assertIsInstance(string, str) From 29a894149d60d6be732e578eb5da8560bd6f67e0 Mon Sep 17 00:00:00 2001 From: Mike Nahmias Date: Wed, 11 Mar 2020 13:15:22 -0400 Subject: [PATCH 08/22] Issue/347 quiz submission data (#351) * Return included data Co-authored-by: Matthew Emond --- canvasapi/quiz.py | 18 ++++++++++++++++++ tests/fixtures/quiz.json | 24 ++++++++++++++++++++++++ tests/test_quiz.py | 31 +++++++++++++++++++------------ 3 files changed, 61 insertions(+), 12 deletions(-) diff --git a/canvasapi/quiz.py b/canvasapi/quiz.py index 0cce770d..efd945b8 100644 --- a/canvasapi/quiz.py +++ b/canvasapi/quiz.py @@ -8,6 +8,8 @@ from canvasapi.exceptions import RequiredFieldMissing from canvasapi.paginated_list import PaginatedList from canvasapi.quiz_group import QuizGroup +from canvasapi.submission import Submission +from canvasapi.user import User from canvasapi.util import combine_kwargs, obj_or_id @@ -374,6 +376,22 @@ def get_quiz_submission(self, quiz_submission, **kwargs): response_json = response.json()["quiz_submissions"][0] response_json.update({"course_id": self.course_id}) + if len(response.json().get("quizzes", [])) > 0: + response_json.update( + {"quiz": Quiz(self._requester, response.json()["quizzes"][0])} + ) + if len(response.json().get("submissions", [])) > 0: + response_json.update( + { + "submission": Submission( + self._requester, response.json()["submissions"][0] + ) + } + ) + if len(response.json().get("users", [])) > 0: + response_json.update( + {"user": User(self._requester, response.json()["users"][0])} + ) return QuizSubmission(self._requester, response_json) diff --git a/tests/fixtures/quiz.json b/tests/fixtures/quiz.json index 1a0c09e8..253e05cd 100644 --- a/tests/fixtures/quiz.json +++ b/tests/fixtures/quiz.json @@ -219,6 +219,30 @@ "validation_token": "this is a validation token", "score": 0 } + ], + "quizzes": [ + { + "id": 1, + "title": "Test Quiz", + "quiz_type": "survey" + } + ], + "submissions": [ + { + "id": 1, + "body": "user: 1, quiz: 1, score: 1.0, time: 2020-01-01 00:00:00Z", + "grade": "1", + "score": 1, + "assignment_id": 1234, + "user_id": 1 + } + ], + "users": [ + { + "id": 1, + "name": "Test User", + "login_id": "testuser@example.com" + } ] }, "status_code": 200 diff --git a/tests/test_quiz.py b/tests/test_quiz.py index 4e87972a..cf0b010d 100644 --- a/tests/test_quiz.py +++ b/tests/test_quiz.py @@ -22,6 +22,8 @@ from canvasapi.paginated_list import PaginatedList from tests import settings from tests.util import register_uris +from canvasapi.user import User +from canvasapi.submission import Submission @requests_mock.Mocker() @@ -319,19 +321,24 @@ def test_get_quiz_submission(self, m): register_uris({"quiz": ["get_quiz_submission"]}, m) quiz_id = 1 - submission = self.quiz.get_quiz_submission(quiz_id) + quiz_submission = self.quiz.get_quiz_submission( + quiz_id, include=["quiz", "submission", "user"] + ) - self.assertIsInstance(submission, QuizSubmission) - self.assertTrue(hasattr(submission, "id")) - self.assertEqual(submission.quiz_id, quiz_id) - self.assertTrue(hasattr(submission, "quiz_version")) - self.assertEqual(submission.quiz_version, 1) - self.assertTrue(hasattr(submission, "user_id")) - self.assertEqual(submission.user_id, 1) - self.assertTrue(hasattr(submission, "validation_token")) - self.assertEqual(submission.validation_token, "this is a validation token") - self.assertTrue(hasattr(submission, "score")) - self.assertEqual(submission.score, 0) + self.assertIsInstance(quiz_submission, QuizSubmission) + self.assertTrue(hasattr(quiz_submission, "id")) + self.assertEqual(quiz_submission.quiz_id, quiz_id) + self.assertTrue(hasattr(quiz_submission, "quiz_version")) + self.assertEqual(quiz_submission.quiz_version, 1) + self.assertTrue(hasattr(quiz_submission, "user_id")) + self.assertEqual(quiz_submission.user_id, 1) + self.assertTrue(hasattr(quiz_submission, "validation_token")) + self.assertEqual(quiz_submission.validation_token, "this is a validation token") + self.assertTrue(hasattr(quiz_submission, "score")) + self.assertEqual(quiz_submission.score, 0) + self.assertIsInstance(quiz_submission.quiz, Quiz) + self.assertIsInstance(quiz_submission.submission, Submission) + self.assertIsInstance(quiz_submission.user, User) # create_submission def test_create_submission(self, m): From 8217fbd1efc35376c069ff7b0e8c682f7b788c0b Mon Sep 17 00:00:00 2001 From: Mike Nahmias Date: Thu, 12 Mar 2020 10:25:15 -0400 Subject: [PATCH 09/22] Issue/294 print change record class (#356) * Fix ChangeRecord.__str__() * Add test for ChangeRecord class Co-authored-by: Matthew Emond --- canvasapi/blueprint.py | 4 ++-- tests/test_blueprint.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/canvasapi/blueprint.py b/canvasapi/blueprint.py index ad40862e..f8e7fae9 100644 --- a/canvasapi/blueprint.py +++ b/canvasapi/blueprint.py @@ -236,8 +236,8 @@ def get_import_details(self, **kwargs): @python_2_unicode_compatible class ChangeRecord(CanvasObject): - def __str__(self): # pragma: no cover - return "{} {}".format(self.id, self.template_id) + def __str__(self): + return "{} {}".format(self.asset_id, self.asset_name) @python_2_unicode_compatible diff --git a/tests/test_blueprint.py b/tests/test_blueprint.py index fd4b60a2..6b3c3b75 100644 --- a/tests/test_blueprint.py +++ b/tests/test_blueprint.py @@ -196,3 +196,25 @@ def test_get_import_details(self, m): import_details = self.b_import.get_import_details() self.assertIsInstance(import_details, PaginatedList) self.assertIsInstance(import_details[0], ChangeRecord) + + +@requests_mock.Mocker() +class TestChangeRecord(unittest.TestCase): + def setUp(self): + self.canvas = Canvas(settings.BASE_URL, settings.API_KEY) + + with requests_mock.Mocker() as m: + requires = { + "course": ["get_blueprint", "get_by_id"], + "blueprint": ["get_unsynced_changes"], + } + register_uris(requires, m) + + self.course = self.canvas.get_course(1) + self.blueprint = self.course.get_blueprint(1) + self.change_record = self.blueprint.get_unsynced_changes()[0] + + # __str__() + def test__str__(self, m): + string = str(self.change_record) + self.assertIsInstance(string, str) From f00d69a8dff7f40b7e2977f9ed26d14dfcf3514b Mon Sep 17 00:00:00 2001 From: Mike Nahmias Date: Thu, 12 Mar 2020 12:12:40 -0400 Subject: [PATCH 10/22] Issue/206 print account report (#357) * Fix AccountReport.__str__() AccountReport can be a report type or instance. Allows printing of both. * Test AccountReport.__str__() * Fix account.reports fixture Add report attribute to returned data Co-authored-by: Matthew Emond --- canvasapi/account.py | 6 +++++- tests/fixtures/account.json | 6 ++++-- tests/test_account.py | 2 ++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/canvasapi/account.py b/canvasapi/account.py index d03cea3d..dca75eda 100644 --- a/canvasapi/account.py +++ b/canvasapi/account.py @@ -1925,7 +1925,11 @@ def update_global_notification(self, account_notification, **kwargs): @python_2_unicode_compatible class AccountReport(CanvasObject): def __str__(self): - return "{} ({})".format(self.report, self.id) + try: + return "{} ({})".format(self.report, self.id) + except AttributeError: + # Print params if not a report instance + return "{} ({})".format(self.report, self.parameters) def delete_report(self, **kwargs): """ diff --git a/tests/fixtures/account.json b/tests/fixtures/account.json index af424bce..9dd6b817 100644 --- a/tests/fixtures/account.json +++ b/tests/fixtures/account.json @@ -578,10 +578,12 @@ "endpoint": "accounts/1/reports", "data": [ { - "id": 1 + "id": 1, + "report": "sis_export_csv" }, { - "id": 2 + "id": 2, + "report": "grade_export_csv" } ], "headers": { diff --git a/tests/test_account.py b/tests/test_account.py index e79d275e..3cd8f35e 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -286,6 +286,7 @@ def test_get_report(self, m): self.assertIsInstance(report, AccountReport) self.assertTrue(hasattr(report, "title")) self.assertEqual(report.title, "Zero Activity") + self.assertIsInstance(str(report), str) # get_index_of_reports() def test_get_index_of_reports(self, m): @@ -310,6 +311,7 @@ def test_get_reports(self, m): self.assertEqual(len(reports_list), 4) self.assertIsInstance(reports_list[0], AccountReport) self.assertTrue(hasattr(reports_list[0], "id")) + self.assertIsInstance(str(reports_list[0]), str) # get_subaccounts() def test_get_subaccounts(self, m): From df045de094c47101367ba9f2322105f1387c1a14 Mon Sep 17 00:00:00 2001 From: Bruce Spang Date: Thu, 9 Apr 2020 16:46:25 -0700 Subject: [PATCH 11/22] Fix report_type param in create_report --- canvasapi/quiz.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canvasapi/quiz.py b/canvasapi/quiz.py index efd945b8..1c3072f1 100644 --- a/canvasapi/quiz.py +++ b/canvasapi/quiz.py @@ -143,7 +143,7 @@ def create_report(self, report_type, **kwargs): "Param `report_type` must be a either 'student_analysis' or 'item_analysis'" ) - kwargs["report_type"] = report_type + kwargs["quiz_report"] = {"report_type": report_type} response = self._requester.request( "POST", From c3a97915319f5ba249a27e97d3dd02984876b4d0 Mon Sep 17 00:00:00 2001 From: Dmitry Savransky Date: Fri, 19 Jun 2020 14:25:52 -0400 Subject: [PATCH 12/22] changing all instances of l to lic in relevant tests to appease flake8 (#377) --- tests/test_course.py | 10 +++++----- tests/test_group.py | 10 +++++----- tests/test_user.py | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/test_course.py b/tests/test_course.py index d78219da..c446e760 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -1992,11 +1992,11 @@ def test_get_licenses(self, m): self.assertIsInstance(licenses, PaginatedList) licenses = list(licenses) - for l in licenses: - self.assertIsInstance(l, License) - self.assertTrue(hasattr(l, "id")) - self.assertTrue(hasattr(l, "name")) - self.assertTrue(hasattr(l, "url")) + for lic in licenses: + self.assertIsInstance(lic, License) + self.assertTrue(hasattr(lic, "id")) + self.assertTrue(hasattr(lic, "name")) + self.assertTrue(hasattr(lic, "url")) self.assertEqual(2, len(licenses)) diff --git a/tests/test_group.py b/tests/test_group.py index 1adb4897..ef61acc4 100644 --- a/tests/test_group.py +++ b/tests/test_group.py @@ -656,11 +656,11 @@ def test_get_licenses(self, m): self.assertIsInstance(licenses, PaginatedList) licenses = list(licenses) - for l in licenses: - self.assertIsInstance(l, License) - self.assertTrue(hasattr(l, "id")) - self.assertTrue(hasattr(l, "name")) - self.assertTrue(hasattr(l, "url")) + for lic in licenses: + self.assertIsInstance(lic, License) + self.assertTrue(hasattr(lic, "id")) + self.assertTrue(hasattr(lic, "name")) + self.assertTrue(hasattr(lic, "url")) self.assertEqual(2, len(licenses)) diff --git a/tests/test_user.py b/tests/test_user.py index bd6b3bb5..dc027d8e 100644 --- a/tests/test_user.py +++ b/tests/test_user.py @@ -617,11 +617,11 @@ def test_get_licenses(self, m): self.assertIsInstance(licenses, PaginatedList) licenses = list(licenses) - for l in licenses: - self.assertIsInstance(l, License) - self.assertTrue(hasattr(l, "id")) - self.assertTrue(hasattr(l, "name")) - self.assertTrue(hasattr(l, "url")) + for lic in licenses: + self.assertIsInstance(lic, License) + self.assertTrue(hasattr(lic, "id")) + self.assertTrue(hasattr(lic, "name")) + self.assertTrue(hasattr(lic, "url")) self.assertEqual(2, len(licenses)) From ed03fdca2bad9624f2dd43ad7714065271d2b3ce Mon Sep 17 00:00:00 2001 From: Dmitry Savransky Date: Fri, 19 Jun 2020 14:29:41 -0400 Subject: [PATCH 13/22] fixing minor docstring errors in course.create_page (#376) Co-authored-by: Matthew Emond --- canvasapi/course.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/canvasapi/course.py b/canvasapi/course.py index 839958a1..95e0626e 100644 --- a/canvasapi/course.py +++ b/canvasapi/course.py @@ -414,10 +414,10 @@ def create_page(self, wiki_page, **kwargs): :calls: `POST /api/v1/courses/:course_id/pages \ `_ - :param title: The title for the page. - :type title: dict + :param wiki_page: The title for the page. + :type wiki_page: dict :returns: The created page. - :rtype: :class:`canvasapi.course.Course` + :rtype: :class:`canvasapi.page.Page` """ if isinstance(wiki_page, dict) and "title" in wiki_page: From d4f5ff97b9470cdfbc7412d1cc0ded18b3a3b58e Mon Sep 17 00:00:00 2001 From: Dmitry Savransky Date: Fri, 19 Jun 2020 15:49:07 -0400 Subject: [PATCH 14/22] addressing #375 - adding coverage for resolve_path under courses endpoint, plus unittest --- canvasapi/course.py | 22 ++++++++++++++++++++++ tests/fixtures/course.json | 36 ++++++++++++++++++++++++++++++++++++ tests/test_course.py | 14 ++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/canvasapi/course.py b/canvasapi/course.py index 95e0626e..2903a35a 100644 --- a/canvasapi/course.py +++ b/canvasapi/course.py @@ -2658,6 +2658,28 @@ def reset(self): ) return Course(self._requester, response.json()) + def resolve_path(self, full_path, **kwargs): + """ + Returns the paginated list of all of the folders in the given + path starting at the course root folder. + + :calls: `GET /api/v1/courses/:course_id/folders/by_path/*full_path \ + `_ + + :param full_path: Full path to resolve, relative to course root + :type full_path: string + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.folder.Folder` + """ + return PaginatedList( + Folder, + self._requester, + "GET", + "courses/{0}/folders/by_path/{1}".format(self.id, full_path), + _kwargs=combine_kwargs(**kwargs), + ) + def set_quiz_extensions(self, quiz_extensions, **kwargs): """ Set extensions for student all quiz submissions in a course. diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json index 7346d0b5..45b03aa5 100644 --- a/tests/fixtures/course.json +++ b/tests/fixtures/course.json @@ -2149,5 +2149,41 @@ } ], "status_code": 200 + }, + "resolve_path": { + "method": "GET", + "endpoint": "courses/1/folders/by_path/Folder_Level_1/Folder_Level_2/Folder_Level_3", + "data": [ + { + "id": 2, + "files_count": 0, + "folders_count": 1, + "name": "course_files", + "full_name": "course_files" + }, + + { + "id": 3, + "files_count": 0, + "folders_count": 1, + "name": "Folder_Level_1", + "full_name": "course_files" + }, + { + "id": 4, + "files_count": 0, + "folders_count": 1, + "name": "Folder_Level_2", + "full_name": "course_files/Folder_Level_1/Folder_Level_2" + }, + { + "id": 5, + "files_count": 0, + "folders_count": 0, + "name": "Folder_Level_3", + "full_name": "course_files/Folder_Level_1/Folder_Level_2/Folder_Level_3" + } + ], + "status_code": 200 } } diff --git a/tests/test_course.py b/tests/test_course.py index c446e760..43cd36bb 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -2000,6 +2000,20 @@ def test_get_licenses(self, m): self.assertEqual(2, len(licenses)) + # resolve_path() + def test_resolve_path(self, m): + register_uris({"course": ["resolve_path"]}, m) + + full_path = "Folder_Level_1/Folder_Level_2/Folder_Level_3" + folders = self.course.resolve_path(full_path) + folder_list = [folder for folder in folders] + self.assertEqual(len(folder_list), 4) + self.assertIsInstance(folder_list[0], Folder) + for folder_name, folder in zip( + ("course_files/" + full_path).split("/"), folders + ): + self.assertEqual(folder_name, folder.name) + @requests_mock.Mocker() class TestCourseNickname(unittest.TestCase): From 598ddb754bfcbb585be80c50650bc56e85b1bb8a Mon Sep 17 00:00:00 2001 From: "Code Hugger (Matthew Jones)" Date: Mon, 22 Jun 2020 11:28:54 -0400 Subject: [PATCH 15/22] issue/354 graphql support (#355) * Adding support for graphql endpoint * Support JSON option as a request to PATCH/PUT/POST requests. Remove custom code around detecting types. Co-authored-by: Matthew Emond --- canvasapi/canvas.py | 29 +++++++++++++++++++++++++++-- canvasapi/exceptions.py | 6 ++++++ canvasapi/requester.py | 28 +++++++++++++++++++++------- docs/exceptions.rst | 2 ++ tests/fixtures/graphql.json | 21 +++++++++++++++++++++ tests/fixtures/requests.json | 6 ++++++ tests/settings.py | 1 + tests/test_canvas.py | 31 +++++++++++++++++++++++++++++++ tests/test_requester.py | 7 +++++++ tests/util.py | 11 ++++------- 10 files changed, 126 insertions(+), 16 deletions(-) create mode 100644 tests/fixtures/graphql.json diff --git a/canvasapi/canvas.py b/canvasapi/canvas.py index 1ce81bda..13b7aad7 100644 --- a/canvasapi/canvas.py +++ b/canvasapi/canvas.py @@ -64,8 +64,6 @@ def __init__(self, base_url, access_token): # the API. access_token = access_token.strip() - base_url = new_url + "/api/v1/" - self.__requester = Requester(base_url, access_token) def clear_course_nicknames(self): @@ -1184,6 +1182,33 @@ def get_user_participants(self, appointment_group, **kwargs): _kwargs=combine_kwargs(**kwargs), ) + def graphql(self, query, variables=None, **kwargs): + """ + Makes a GraphQL formatted request to Canvas + + :calls: `POST /api/graphql \ + `_ + + :param query: The GraphQL query to execute as a String + :type query: str + :param variables: The variable values as required by the supplied query + :type variables: dict + + :rtype: dict + """ + response = self.__requester.request( + "POST", + "graphql", + headers={"Content-Type": "application/json"}, + _kwargs=combine_kwargs(**kwargs) + + [("query", query), ("variables", variables)], + # Needs to call special endpoint without api/v1 + _url=self.__requester.original_url + "/api/graphql", + json=True, + ) + + return response.json() + def list_appointment_groups(self, **kwargs): """ List appointment groups. diff --git a/canvasapi/exceptions.py b/canvasapi/exceptions.py index e68f232a..e118bc57 100644 --- a/canvasapi/exceptions.py +++ b/canvasapi/exceptions.py @@ -65,3 +65,9 @@ class Conflict(CanvasException): """Canvas had a conflict with an existing resource.""" pass + + +class UnprocessableEntity(CanvasException): + """Canvas was unable to process the entity.""" + + pass diff --git a/canvasapi/requester.py b/canvasapi/requester.py index 36ad89c9..fbb3cf9e 100644 --- a/canvasapi/requester.py +++ b/canvasapi/requester.py @@ -13,6 +13,7 @@ InvalidAccessToken, ResourceDoesNotExist, Unauthorized, + UnprocessableEntity, ) from canvasapi.util import clean_headers @@ -32,12 +33,14 @@ def __init__(self, base_url, access_token): :param access_token: The API key to authenticate requests with. :type access_token: str """ - self.base_url = base_url + # Preserve the original base url and add "/api/v1" to it + self.original_url = base_url + self.base_url = base_url + "/api/v1/" self.access_token = access_token self._session = requests.Session() self._cache = [] - def _delete_request(self, url, headers, data=None): + def _delete_request(self, url, headers, data=None, **kwargs): """ Issue a DELETE request to the specified endpoint with the data provided. @@ -50,7 +53,7 @@ def _delete_request(self, url, headers, data=None): """ return self._session.delete(url, headers=headers, data=data) - def _get_request(self, url, headers, params=None): + def _get_request(self, url, headers, params=None, **kwargs): """ Issue a GET request to the specified endpoint with the data provided. @@ -63,7 +66,7 @@ def _get_request(self, url, headers, params=None): """ return self._session.get(url, headers=headers, params=params) - def _patch_request(self, url, headers, data=None): + def _patch_request(self, url, headers, data=None, **kwargs): """ Issue a PATCH request to the specified endpoint with the data provided. @@ -76,7 +79,7 @@ def _patch_request(self, url, headers, data=None): """ return self._session.patch(url, headers=headers, data=data) - def _post_request(self, url, headers, data=None): + def _post_request(self, url, headers, data=None, json=False): """ Issue a POST request to the specified endpoint with the data provided. @@ -86,7 +89,11 @@ def _post_request(self, url, headers, data=None): :type headers: dict :param data: The data to send with this request. :type data: dict + :param json: Whether or not to send the data as json + :type json: bool """ + if json: + return self._session.post(url, headers=headers, json=dict(data)) # Grab file from data. files = None @@ -103,7 +110,7 @@ def _post_request(self, url, headers, data=None): return self._session.post(url, headers=headers, data=data, files=files) - def _put_request(self, url, headers, data=None): + def _put_request(self, url, headers, data=None, **kwargs): """ Issue a PUT request to the specified endpoint with the data provided. @@ -124,6 +131,7 @@ def request( use_auth=True, _url=None, _kwargs=None, + json=False, **kwargs ): """ @@ -146,6 +154,10 @@ def request( :param _kwargs: A list of 2-tuples representing processed keyword arguments to be sent to Canvas as params or data. :type _kwargs: `list` + :param json: Whether or not to treat the data as json instead of form data. + currently only the POST request of GraphQL is using this parameter. + For all other methods it's just passed and ignored. + :type json: `bool` :rtype: str """ full_url = _url if _url else "{}{}".format(self.base_url, endpoint) @@ -194,7 +206,7 @@ def request( if _kwargs: logger.debug("Data: {data}".format(data=pformat(_kwargs))) - response = req_method(full_url, headers, _kwargs) + response = req_method(full_url, headers, _kwargs, json=json) logger.info( "Response: {method} {url} {status}".format( method=method, url=full_url, status=response.status_code @@ -231,6 +243,8 @@ def request( raise ResourceDoesNotExist("Not Found") elif response.status_code == 409: raise Conflict(response.text) + elif response.status_code == 422: + raise UnprocessableEntity(response.text) elif response.status_code > 400: # generic catch-all for error codes raise CanvasException( diff --git a/docs/exceptions.rst b/docs/exceptions.rst index c8ffec5a..cc3419b2 100644 --- a/docs/exceptions.rst +++ b/docs/exceptions.rst @@ -21,6 +21,8 @@ Quick Guide +-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+ | :class:`~canvasapi.exceptions.Conflict` | 409 | Canvas had a conflict with an existing resource. | +-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+ +| :class:`~canvasapi.exceptions.UnprocessableEntity` | 422 | Canvas was unable to process the request. | ++-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+ | :class:`~canvasapi.exceptions.RequiredFieldMissing` | N/A | A required keyword argument was not included. | +-----------------------------------------------------+-----------------+---------------------------------------------------------------------------------+ | :class:`~canvasapi.exceptions.CanvasException` | N/A | An unknown error was thrown. | diff --git a/tests/fixtures/graphql.json b/tests/fixtures/graphql.json new file mode 100644 index 00000000..5f19eaf1 --- /dev/null +++ b/tests/fixtures/graphql.json @@ -0,0 +1,21 @@ +{ + "graphql": { + "method": "POST", + "endpoint": "graphql", + "data": { + "term": { + "coursesConnection": { + "nodes": [ + { + "_id": "1", + "assignmentsConnection": { + "nodes": [] + } + } + ] + } + } + }, + "status_code": 200 + } +} \ No newline at end of file diff --git a/tests/fixtures/requests.json b/tests/fixtures/requests.json index 5d00c691..ca123fca 100644 --- a/tests/fixtures/requests.json +++ b/tests/fixtures/requests.json @@ -32,6 +32,12 @@ "data": {}, "status_code": 409 }, + "422": { + "method": "ANY", + "endpoint": "422", + "data": {}, + "status_code": 422 + }, "500": { "method": "ANY", "endpoint": "500", diff --git a/tests/settings.py b/tests/settings.py index 4fbbcab8..f5dd44f8 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -1,6 +1,7 @@ from __future__ import absolute_import, division, print_function, unicode_literals BASE_URL = "https://example.com" +BASE_URL_GRAPHQL = "https://example.com/api/" BASE_URL_WITH_VERSION = "https://example.com/api/v1/" BASE_URL_AS_HTTP = "http://example.com" BASE_URL_AS_BLANK = "" diff --git a/tests/test_canvas.py b/tests/test_canvas.py index 7a5a78d9..77e56bee 100644 --- a/tests/test_canvas.py +++ b/tests/test_canvas.py @@ -832,3 +832,34 @@ def test_get_comm_messages(self, m): self.assertEqual(comm_messages[0].subject, "example subject line") self.assertEqual(comm_messages[1].id, 2) self.assertEqual(comm_messages[1].subject, "My Subject") + + def test_graphql(self, m): + register_uris({"graphql": ["graphql"]}, m, base_url=settings.BASE_URL_GRAPHQL) + query = """ + query MyQuery($termid: ID!) { + term(id: $termid) { + coursesConnection { + nodes { + _id + assignmentsConnection { + nodes { + name + _id + expectsExternalSubmission + } + } + } + edges { + node { + id + } + } + } + } + } + """ + variables = {"termid": 125} + + graphql_response = self.canvas.graphql(query=query, variables=variables) + # Just a super simple check right now that it gets back a dict respose + self.assertIsInstance(graphql_response, dict) diff --git a/tests/test_requester.py b/tests/test_requester.py index bd17c116..3225f334 100644 --- a/tests/test_requester.py +++ b/tests/test_requester.py @@ -14,6 +14,7 @@ InvalidAccessToken, ResourceDoesNotExist, Unauthorized, + UnprocessableEntity, ) from tests import settings from tests.util import register_uris @@ -145,6 +146,12 @@ def test_request_409(self, m): with self.assertRaises(Conflict): self.requester.request("GET", "409") + def test_request_422(self, m): + register_uris({"requests": ["422"]}, m) + + with self.assertRaises(UnprocessableEntity): + self.requester.request("GET", "422") + def test_request_500(self, m): register_uris({"requests": ["500"]}, m) diff --git a/tests/util.py b/tests/util.py index 64d7cee3..ef5dd708 100644 --- a/tests/util.py +++ b/tests/util.py @@ -4,11 +4,10 @@ import requests_mock -from canvasapi.util import get_institution_url from tests import settings -def register_uris(requirements, requests_mocker): +def register_uris(requirements, requests_mocker, base_url=None): """ Given a list of required fixtures and an requests_mocker object, register each fixture as a uri with the mocker. @@ -17,6 +16,8 @@ def register_uris(requirements, requests_mocker): :param requirements: dict :param requests_mocker: requests_mock.mocker.Mocker """ + if base_url is None: + base_url = settings.BASE_URL_WITH_VERSION for fixture, objects in requirements.items(): try: with open("tests/fixtures/{}.json".format(fixture)) as file: @@ -39,11 +40,7 @@ def register_uris(requirements, requests_mocker): if obj["endpoint"] == "ANY": url = requests_mock.ANY else: - url = ( - get_institution_url(settings.BASE_URL) - + "/api/v1/" - + obj["endpoint"] - ) + url = base_url + obj["endpoint"] try: requests_mocker.register_uri( From 1f7753b82973cb6f0d747063910d10a5a1e462c3 Mon Sep 17 00:00:00 2001 From: Dmitry Savransky Date: Tue, 23 Jun 2020 15:23:06 -0400 Subject: [PATCH 16/22] addressing #375 - adding coverage for resolve_path under courses endpoint, plus unittest (#378) Co-authored-by: Matthew Emond --- canvasapi/course.py | 22 ++++++++++++++++++++++ tests/fixtures/course.json | 36 ++++++++++++++++++++++++++++++++++++ tests/test_course.py | 14 ++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/canvasapi/course.py b/canvasapi/course.py index 95e0626e..2903a35a 100644 --- a/canvasapi/course.py +++ b/canvasapi/course.py @@ -2658,6 +2658,28 @@ def reset(self): ) return Course(self._requester, response.json()) + def resolve_path(self, full_path, **kwargs): + """ + Returns the paginated list of all of the folders in the given + path starting at the course root folder. + + :calls: `GET /api/v1/courses/:course_id/folders/by_path/*full_path \ + `_ + + :param full_path: Full path to resolve, relative to course root + :type full_path: string + + :rtype: :class:`canvasapi.paginated_list.PaginatedList` of + :class:`canvasapi.folder.Folder` + """ + return PaginatedList( + Folder, + self._requester, + "GET", + "courses/{0}/folders/by_path/{1}".format(self.id, full_path), + _kwargs=combine_kwargs(**kwargs), + ) + def set_quiz_extensions(self, quiz_extensions, **kwargs): """ Set extensions for student all quiz submissions in a course. diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json index 7346d0b5..45b03aa5 100644 --- a/tests/fixtures/course.json +++ b/tests/fixtures/course.json @@ -2149,5 +2149,41 @@ } ], "status_code": 200 + }, + "resolve_path": { + "method": "GET", + "endpoint": "courses/1/folders/by_path/Folder_Level_1/Folder_Level_2/Folder_Level_3", + "data": [ + { + "id": 2, + "files_count": 0, + "folders_count": 1, + "name": "course_files", + "full_name": "course_files" + }, + + { + "id": 3, + "files_count": 0, + "folders_count": 1, + "name": "Folder_Level_1", + "full_name": "course_files" + }, + { + "id": 4, + "files_count": 0, + "folders_count": 1, + "name": "Folder_Level_2", + "full_name": "course_files/Folder_Level_1/Folder_Level_2" + }, + { + "id": 5, + "files_count": 0, + "folders_count": 0, + "name": "Folder_Level_3", + "full_name": "course_files/Folder_Level_1/Folder_Level_2/Folder_Level_3" + } + ], + "status_code": 200 } } diff --git a/tests/test_course.py b/tests/test_course.py index c446e760..43cd36bb 100644 --- a/tests/test_course.py +++ b/tests/test_course.py @@ -2000,6 +2000,20 @@ def test_get_licenses(self, m): self.assertEqual(2, len(licenses)) + # resolve_path() + def test_resolve_path(self, m): + register_uris({"course": ["resolve_path"]}, m) + + full_path = "Folder_Level_1/Folder_Level_2/Folder_Level_3" + folders = self.course.resolve_path(full_path) + folder_list = [folder for folder in folders] + self.assertEqual(len(folder_list), 4) + self.assertIsInstance(folder_list[0], Folder) + for folder_name, folder in zip( + ("course_files/" + full_path).split("/"), folders + ): + self.assertEqual(folder_name, folder.name) + @requests_mock.Mocker() class TestCourseNickname(unittest.TestCase): From 5f28b45506f655df0e6e429d12ad0cce6fb553a7 Mon Sep 17 00:00:00 2001 From: Matthew Emond Date: Tue, 23 Jun 2020 16:37:43 -0400 Subject: [PATCH 17/22] Add method to get a single enrollment term --- canvasapi/account.py | 21 +++++++++++++++++++++ tests/fixtures/account.json | 9 +++++++++ tests/test_account.py | 13 +++++++++++++ 3 files changed, 43 insertions(+) diff --git a/canvasapi/account.py b/canvasapi/account.py index dca75eda..f8817a2b 100644 --- a/canvasapi/account.py +++ b/canvasapi/account.py @@ -970,6 +970,27 @@ def get_enrollment(self, enrollment, **kwargs): ) return Enrollment(self._requester, response.json()) + def get_enrollment_term(self, term, **kwargs): + """ + Retrieve the details for an enrollment term in the account. Includes overrides by default. + + :calls: `GET /api/v1/accounts/:account_id/terms/:id \ + `_ + + :param term: The object or ID of the enrollment term to retrieve. + :type term: :class:`canvasapi.enrollment_term.EnrollmentTerm` or int + + :rtype: :class:`canvasapi.enrollment_term.EnrollmentTerm` + """ + from canvasapi.enrollment_term import EnrollmentTerm + + term_id = obj_or_id(term, "term", (EnrollmentTerm,)) + + response = self._requester.request( + "GET", "accounts/{}/terms/{}".format(self.id, term_id) + ) + return EnrollmentTerm(self._requester, response.json()) + def get_enrollment_terms(self, **kwargs): """ List enrollment terms for a context. diff --git a/tests/fixtures/account.json b/tests/fixtures/account.json index 9dd6b817..819ac408 100644 --- a/tests/fixtures/account.json +++ b/tests/fixtures/account.json @@ -315,6 +315,15 @@ ], "status_code": 200 }, + "get_enrollment_term": { + "method": "GET", + "endpoint": "accounts/1/terms/1", + "data": { + "id": 1, + "name": "Enrollment Term 1" + }, + "status_code": 200 + }, "get_enrollment_terms": { "method": "GET", "endpoint": "accounts/1/terms", diff --git a/tests/test_account.py b/tests/test_account.py index 3cd8f35e..17fb7bcb 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -576,6 +576,19 @@ def test_list_enrollment_terms(self, m): self.assertEqual(len(warning_list), 1) self.assertEqual(warning_list[-1].category, DeprecationWarning) + # get_enrollment_term() + def test_get_enrollment_term(self, m): + register_uris({'account': ['get_enrollment_term']}, m) + + enrollment_term = self.account.get_enrollment_term(1) + + self.assertIsInstance(enrollment_term, EnrollmentTerm) + + self.assertTrue(hasattr(enrollment_term, 'id')) + self.assertEqual(enrollment_term.id, 1) + self.assertTrue(hasattr(enrollment_term, 'name')) + self.assertEqual(enrollment_term.name, 'Enrollment Term 1') + # get_enrollment_terms() def test_get_enrollment_terms(self, m): register_uris({"account": ["get_enrollment_terms"]}, m) From 2d8b35eb78db266acecd49b9d2264a9e6e0959ed Mon Sep 17 00:00:00 2001 From: Matthew Emond Date: Tue, 23 Jun 2020 16:45:16 -0400 Subject: [PATCH 18/22] ran black --- tests/test_account.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_account.py b/tests/test_account.py index 17fb7bcb..d3b36579 100644 --- a/tests/test_account.py +++ b/tests/test_account.py @@ -578,16 +578,16 @@ def test_list_enrollment_terms(self, m): # get_enrollment_term() def test_get_enrollment_term(self, m): - register_uris({'account': ['get_enrollment_term']}, m) + register_uris({"account": ["get_enrollment_term"]}, m) enrollment_term = self.account.get_enrollment_term(1) self.assertIsInstance(enrollment_term, EnrollmentTerm) - self.assertTrue(hasattr(enrollment_term, 'id')) + self.assertTrue(hasattr(enrollment_term, "id")) self.assertEqual(enrollment_term.id, 1) - self.assertTrue(hasattr(enrollment_term, 'name')) - self.assertEqual(enrollment_term.name, 'Enrollment Term 1') + self.assertTrue(hasattr(enrollment_term, "name")) + self.assertEqual(enrollment_term.name, "Enrollment Term 1") # get_enrollment_terms() def test_get_enrollment_terms(self, m): From 6bb53e0e14772c678d6b179f76b52501da68335b Mon Sep 17 00:00:00 2001 From: Jesse McBride Date: Fri, 26 Jun 2020 12:46:22 -0400 Subject: [PATCH 19/22] bug: Remove trailing and leading spaces from base URLs (#381) --- canvasapi/canvas.py | 8 ++++---- tests/settings.py | 1 + tests/test_canvas.py | 11 ++++++++--- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/canvasapi/canvas.py b/canvasapi/canvas.py index 13b7aad7..3321e067 100644 --- a/canvasapi/canvas.py +++ b/canvasapi/canvas.py @@ -3,6 +3,7 @@ import warnings from canvasapi.account import Account +from canvasapi.comm_message import CommMessage from canvasapi.course import Course from canvasapi.course_epub_export import CourseEpubExport from canvasapi.current_user import CurrentUser @@ -14,7 +15,6 @@ from canvasapi.requester import Requester from canvasapi.section import Section from canvasapi.user import User -from canvasapi.comm_message import CommMessage from canvasapi.util import combine_kwargs, get_institution_url, obj_or_id @@ -59,10 +59,10 @@ def __init__(self, base_url, access_token): UserWarning, ) - # Ensure that the user-supplied access token contains no leading or - # trailing spaces that may cause issues when communicating with - # the API. + # Ensure that the user-supplied access token and base_url contain no leading or + # trailing spaces that might cause issues when communicating with the API. access_token = access_token.strip() + base_url = base_url.strip() self.__requester = Requester(base_url, access_token) diff --git a/tests/settings.py b/tests/settings.py index f5dd44f8..679cc118 100644 --- a/tests/settings.py +++ b/tests/settings.py @@ -6,6 +6,7 @@ BASE_URL_AS_HTTP = "http://example.com" BASE_URL_AS_BLANK = "" BASE_URL_AS_INVALID = "example.com" +BASE_URL_WITH_EXTRA_SPACES = " https://example.com " API_KEY = "123" INVALID_ID = 9001 diff --git a/tests/test_canvas.py b/tests/test_canvas.py index 77e56bee..4256dce2 100644 --- a/tests/test_canvas.py +++ b/tests/test_canvas.py @@ -12,20 +12,19 @@ from canvasapi.account import Account from canvasapi.appointment_group import AppointmentGroup from canvasapi.calendar_event import CalendarEvent +from canvasapi.comm_message import CommMessage from canvasapi.conversation import Conversation from canvasapi.course import Course, CourseNickname from canvasapi.course_epub_export import CourseEpubExport from canvasapi.discussion_topic import DiscussionTopic -from canvasapi.exceptions import RequiredFieldMissing +from canvasapi.exceptions import RequiredFieldMissing, ResourceDoesNotExist from canvasapi.file import File from canvasapi.group import Group, GroupCategory -from canvasapi.exceptions import ResourceDoesNotExist from canvasapi.outcome import Outcome, OutcomeGroup from canvasapi.paginated_list import PaginatedList from canvasapi.progress import Progress from canvasapi.section import Section from canvasapi.user import User -from canvasapi.comm_message import CommMessage from tests import settings from tests.util import register_uris @@ -75,6 +74,12 @@ def test_init_strips_extra_spaces_in_api_key(self, m): client = Canvas(settings.BASE_URL, " 12345 ") self.assertEqual(client._Canvas__requester.access_token, "12345") + def test_init_strips_extra_spaces_in_base_url(self, m): + client = Canvas(settings.BASE_URL_WITH_EXTRA_SPACES, "12345") + self.assertEqual( + client._Canvas__requester.base_url, settings.BASE_URL_WITH_VERSION + ) + # create_account() def test_create_account(self, m): register_uris({"account": ["create"]}, m) From 296c489ddcfcafdaae1f58659b832b0b2c80c3b8 Mon Sep 17 00:00:00 2001 From: Matthew Emond Date: Fri, 26 Jun 2020 13:18:28 -0400 Subject: [PATCH 20/22] Update changelog for v0.16.0 --- CHANGELOG.md | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7f7901e1..363358e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,10 +2,33 @@ ## [Unreleased] +## [0.16.0] - 2020-06-26 + ### New Endpoint Coverage +- Enrollment Terms + - Get a Single Enrollment Term (Thanks, [@lcamacho](https://github.com/lcamacho)) +- Files + - Resolve Path for Course (Thanks,[@dsavransky](https://github.com/dsavransky)) +- GraphQL (Thanks,[@jonespm](https://github.com/jonespm)) +- Late Policy (Thanks, [@kennygperez](https://github.com/kennygperez)) +- Quiz Assignment Overrides (Thanks, [@kennygperez](https://github.com/kennygperez)) - Quiz Statistics (Thanks, [@andrew-gardener](https://github.com/andrew-gardener)) +### General + +- Updated README to use updated parameters for getting a user's courses by enrollment state (Thanks,[@Vishvak365](https://github.com/Vishvak365)) + +### Bugfixes + +- Fixed an issue where `Quiz.get_submission()` ignored data added from using the `include` kwarg. (Thanks,[@Mike-Nahmias](https://github.com/Mike-Nahmias)) +- Fixed the broken `__str__` method on the `ChangeRecord` class (Thanks,[@Mike-Nahmias](https://github.com/Mike-Nahmias)) +- Fixed an issue where printing an `AccountReport` would fail due to not having an ID (Thanks,[@Mike-Nahmias](https://github.com/Mike-Nahmias)) +- Fixed an issue where `"report_type"` was passed improperly (Thanks,[@brucespang](https://github.com/brucespang)) +- Fixed some new `flake8` issues (Thanks,[@dsavransky](https://github.com/dsavransky) and [@jonespm](https://github.com/jonespm)) +- Fixed an incorrect docstring for `Course.create_page()` (Thanks,[@dsavransky](https://github.com/dsavransky)) +- Fixed an issue where extra whitespace in the user-supplied canvas URL would break `PaginatedList` (Thanks,[@amorqiu](https://github.com/amorqiu)) + ## [0.15.0] - 2019-11-19 ### New Endpoint Coverage @@ -409,7 +432,8 @@ Huge thanks to [@liblit](https://github.com/liblit) for lots of issues, suggesti - Fixed some incorrectly defined parameters - Fixed an issue where tests would fail due to an improperly configured requires block -[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v0.15.0...develop +[Unreleased]: https://github.com/ucfopen/canvasapi/compare/v0.16.0...develop +[0.16.0]: https://github.com/ucfopen/canvasapi/compare/v0.15.0...v0.16.0 [0.15.0]: https://github.com/ucfopen/canvasapi/compare/v0.14.0...v0.15.0 [0.14.0]: https://github.com/ucfopen/canvasapi/compare/v0.13.0...v0.14.0 [0.13.0]: https://github.com/ucfopen/canvasapi/compare/v0.12.0...v0.13.0 From 8f7f4b1caac844120024d6b89b148543c06d4cf5 Mon Sep 17 00:00:00 2001 From: Matthew Emond Date: Fri, 26 Jun 2020 13:31:59 -0400 Subject: [PATCH 21/22] Add deprecation warnings for Python 2 support, Python <3.4 support, and all methods already marked as deprecated --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 363358e0..cf8ffb98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,16 @@ - Updated README to use updated parameters for getting a user's courses by enrollment state (Thanks,[@Vishvak365](https://github.com/Vishvak365)) +### Deprecation Warnings + +- :warning: **_This is the final release with support for Python 2.7_** :warning: + - [Python 2.7 is end-of-life as of January 2020](https://www.python.org/doc/sunset-python-2/) + - Future releases of CanvasAPI will *NOT* support any version of Python 2 +- :warning: **_This is the final release with support for Python 3.4_** :warning: + - [Python 3.4 is end-of-life as of March 2019](https://www.python.org/downloads/release/python-3410/) + - Future releases of CanvasAPI will *NOT* support Python 3.4 or below +- This is the final deprecation warning for all methods marked as deprecated in this changelog or in our documentation. They will be removed in the next release. + ### Bugfixes - Fixed an issue where `Quiz.get_submission()` ignored data added from using the `include` kwarg. (Thanks,[@Mike-Nahmias](https://github.com/Mike-Nahmias)) From 27dc79bbdb3661299b04fe5cd80b7c301411355a Mon Sep 17 00:00:00 2001 From: Matthew Emond Date: Fri, 26 Jun 2020 13:35:44 -0400 Subject: [PATCH 22/22] Bump version to 0.16.0 --- canvasapi/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/canvasapi/__init__.py b/canvasapi/__init__.py index 5dd4b6f1..bc14ad6e 100644 --- a/canvasapi/__init__.py +++ b/canvasapi/__init__.py @@ -6,4 +6,4 @@ __all__ = ["Canvas"] -__version__ = "0.15.0" +__version__ = "0.16.0"