Repository: ucfopen/canvasapi (language: python)

Issue:
Grade Change Log
**What resource needs additional coverage?**
Grade Change Log

**What endpoint needs to be covered?**
https://canvas.instructure.com/doc/api/grade_change_log.html

  - [ ] Query by assignment
  - [ ] Query by course
  - [ ] Query by student
  - [ ] Query by grader

_New to working on CanvasAPI? Check out our [contributing guide](https://github.com/ucfopen/canvasapi/blob/develop/.github/CONTRIBUTING.md)!_



Discussion / hints:
I'll take this one

Fix patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 848164c0..2fca249b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,7 @@
 ### New Endpoint Coverage
 
 - Delete a Rubric (Thanks, [@ggarfink](https://github.com/ggarfink))
+- Grade Change Log for Assignments, Courses, and Users (Thanks, [@matthewf-ucsd](https://github.com/matthewf-ucsd))
 - Content Migrations: List items for selective import (Thanks, [@matthewf-ucsd](https://github.com/matthewf-ucsd))
 
 ### General
diff --git a/canvasapi/assignment.py b/canvasapi/assignment.py
index 4b015668..325be72d 100644
--- a/canvasapi/assignment.py
+++ b/canvasapi/assignment.py
@@ -1,5 +1,6 @@
 from canvasapi.canvas_object import CanvasObject
 from canvasapi.exceptions import CanvasException, RequiredFieldMissing
+from canvasapi.grade_change_log import GradeChangeEvent
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.peer_review import PeerReview
 from canvasapi.progress import Progress
@@ -76,6 +77,26 @@ def edit(self, **kwargs):
 
         return Assignment(self._requester, response.json())
 
+    def get_grade_change_events(self, **kwargs):
+        """
+        Returns the grade change events for the assignment.
+
+        :calls: `/api/v1/audit/grade_change/assignments/:assignment_id \
+        <https://canvas.instructure.com/doc/api/grade_change_log.html#method.grade_change_audit_api.for_assignment>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.grade_change_log.GradeChangeEvent`
+        """
+
+        return PaginatedList(
+            GradeChangeEvent,
+            self._requester,
+            "GET",
+            "audit/grade_change/assignments/{}".format(self.id),
+            _root="events",
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_gradeable_students(self, **kwargs):
         """
         List students eligible to submit the assignment.
diff --git a/canvasapi/course.py b/canvasapi/course.py
index cca768dd..11842f9e 100644
--- a/canvasapi/course.py
+++ b/canvasapi/course.py
@@ -10,6 +10,7 @@
 from canvasapi.exceptions import RequiredFieldMissing
 from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.folder import Folder
+from canvasapi.grade_change_log import GradeChangeEvent
 from canvasapi.gradebook_history import (
     Day,
     Grader,
@@ -1412,6 +1413,26 @@ def get_full_discussion_topic(self, topic, **kwargs):
         )
         return response.json()
 
+    def get_grade_change_events(self, **kwargs):
+        """
+        Returns the grade change events for the course.
+
+        :calls: `GET /api/v1/audit/grade_change/courses/:course_id \
+        <https://canvas.instructure.com/doc/api/grade_change_log.html#method.grade_change_audit_api.for_course>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.grade_change_log.GradeChangeEvent`
+        """
+
+        return PaginatedList(
+            GradeChangeEvent,
+            self._requester,
+            "GET",
+            "audit/grade_change/courses/{}".format(self.id),
+            _root="events",
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_gradebook_history_dates(self, **kwargs):
         """
         Returns a map of dates to grader/assignment groups
diff --git a/canvasapi/grade_change_log.py b/canvasapi/grade_change_log.py
new file mode 100644
index 00000000..f2b4595e
--- /dev/null
+++ b/canvasapi/grade_change_log.py
@@ -0,0 +1,8 @@
+from canvasapi.canvas_object import CanvasObject
+
+
+class GradeChangeEvent(CanvasObject):
+    def __str__(self):
+        return "{} {} - {} ({})".format(
+            self.event_type, self.grade_before, self.grade_after, self.id
+        )
diff --git a/canvasapi/user.py b/canvasapi/user.py
index 74c50791..7f3f4f54 100644
--- a/canvasapi/user.py
+++ b/canvasapi/user.py
@@ -3,6 +3,7 @@
 from canvasapi.communication_channel import CommunicationChannel
 from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.folder import Folder
+from canvasapi.grade_change_log import GradeChangeEvent
 from canvasapi.license import License
 from canvasapi.paginated_list import PaginatedList
 from canvasapi.pairing_code import PairingCode
@@ -632,6 +633,45 @@ def get_folders(self, **kwargs):
             _kwargs=combine_kwargs(**kwargs),
         )
 
+    def get_grade_change_events_for_grader(self, **kwargs):
+        """
+        Returns the grade change events for a grader.
+
+        :calls: `/api/v1/audit/grade_change/graders/:grader_id \
+        <https://canvas.instructure.com/doc/api/grade_change_log.html#method.grade_change_audit_api.for_grader>`_
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.grade_change_log.GradeChangeEvent`
+        """
+        return PaginatedList(
+            GradeChangeEvent,
+            self._requester,
+            "GET",
+            "audit/grade_change/graders/{}".format(self.id),
+            _root="events",
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
+    def get_grade_change_events_for_student(self, **kwargs):
+        """
+        Returns the grade change events for the current student.
+
+        :calls: `/api/v1/audit/grade_change/students/:student_id \
+        <https://canvas.instructure.com/doc/api/grade_change_log.html#method.grade_change_audit_api.for_student>`_
+
+
+        :rtype: :class:`canvasapi.paginated_list.PaginatedList` of
+            :class:`canvasapi.grade_change_log.GradeChangeEvent`
+        """
+        return PaginatedList(
+            GradeChangeEvent,
+            self._requester,
+            "GET",
+            "audit/grade_change/students/{}".format(self.id),
+            _root="events",
+            _kwargs=combine_kwargs(**kwargs),
+        )
+
     def get_licenses(self, **kwargs):
         """
         Returns a paginated list of the licenses that can be applied to the
diff --git a/docs/class-reference.rst b/docs/class-reference.rst
index cb5e00dd..ef92cd24 100644
--- a/docs/class-reference.rst
+++ b/docs/class-reference.rst
@@ -33,6 +33,7 @@ Class Reference
     file-ref
     folder-ref
     grading-period-ref
+    grade-change-log-ref
     group-ref
     login-ref
     license-ref
diff --git a/docs/grade-change-log-ref.rst b/docs/grade-change-log-ref.rst
new file mode 100644
index 00000000..6f459360
--- /dev/null
+++ b/docs/grade-change-log-ref.rst
@@ -0,0 +1,6 @@
+================
+GradeChangeEvent
+================
+
+.. autoclass:: canvasapi.grade_change_log.GradeChangeEvent
+    :members:


Test patch:
diff --git a/tests/fixtures/assignment.json b/tests/fixtures/assignment.json
index c6f62857..95d8cc34 100644
--- a/tests/fixtures/assignment.json
+++ b/tests/fixtures/assignment.json
@@ -162,6 +162,87 @@
 		},
 		"status_code": 200
 	},
+	"get_grade_change_events": {
+		"method": "GET",
+		"endpoint": "audit/grade_change/assignments/1",
+		"data": {
+			"links": {
+				"events.assignment": "https://canvas.com/api/v1/assignment/test",
+				"events.course": "https://canvas.com/api/v1/assignment/course",
+				"events.student": {
+					"href": null,
+					"type": "user"
+				},
+				"events.grader": {
+					"href": null,
+					"type": "user"
+				},
+				"events.page_view": null
+			},
+			"events": [
+				{
+					"id": "dd5-6a",
+					"event_type": "grade_change",
+					"grade_before": "5",
+					"grade_after": "2",
+					"version_number": 2,
+					"links": {
+						"assignment": 10,
+						"course": 1,
+						"student": 100,
+						"grader": 200,
+						"page_view": null
+					}
+				},
+				{
+					"id": "fg-43",
+					"created_at": "2019-10-25T14:08:46Z",
+					"event_type": "grade_change",
+					"grade_before": null,
+					"grade_after": "5",
+					"version_number": 1,
+					"links": {
+						"assignment": 10,
+						"course": 1,
+						"student": 100,
+						"grader": 200,
+						"page_view": null
+					}
+				}
+			],
+			"linked": {
+				"page_views": [],
+				"assignments": [
+					{
+						"id": 10,
+						"description": "This is a test assignment",
+						"points_possible": 10.0,
+						"grading_type": "points",
+						"course_id": 1,
+						"name": "Test assignment",
+						"submission_types": ["online_text_entry"]
+					}
+				],
+				"courses": [
+					{
+						"id": 10,
+						"name": "Dev Course"
+					}
+				],
+				"users": [
+					{
+						"id": 100,
+						"name": "John Smith"
+					},
+					{
+						"id": 200,
+						"name": "Billy Smith"
+					}
+				]
+			}
+		},
+		"status_code": 200
+	},
 	"list_assignment_groups": {
 		"method": "GET",
 		"endpoint": "courses/1/assignment_groups",
diff --git a/tests/fixtures/course.json b/tests/fixtures/course.json
index df2bc190..cc750c9e 100644
--- a/tests/fixtures/course.json
+++ b/tests/fixtures/course.json
@@ -417,6 +417,87 @@
 		},
 		"status_code": 200
 	},
+	"get_grade_change_events": {
+		"method": "GET",
+		"endpoint": "audit/grade_change/courses/1",
+		"data": {
+			"links": {
+				"events.assignment": "https://canvas.com/api/v1/assignment/test",
+				"events.course": "https://canvas.com/api/v1/assignment/course",
+				"events.student": {
+					"href": null,
+					"type": "user"
+				},
+				"events.grader": {
+					"href": null,
+					"type": "user"
+				},
+				"events.page_view": null
+			},
+			"events": [
+				{
+					"id": "dd5-6a",
+					"event_type": "grade_change",
+					"grade_before": "5",
+					"grade_after": "2",
+					"version_number": 2,
+					"links": {
+						"assignment": 10,
+						"course": 1,
+						"student": 100,
+						"grader": 200,
+						"page_view": null
+					}
+				},
+				{
+					"id": "fg-43",
+					"created_at": "2019-10-25T14:08:46Z",
+					"event_type": "grade_change",
+					"grade_before": null,
+					"grade_after": "5",
+					"version_number": 1,
+					"links": {
+						"assignment": 10,
+						"course": 1,
+						"student": 100,
+						"grader": 200,
+						"page_view": null
+					}
+				}
+			],
+			"linked": {
+				"page_views": [],
+				"assignments": [
+					{
+						"id": 10,
+						"description": "This is a test assignment",
+						"points_possible": 10.0,
+						"grading_type": "points",
+						"course_id": 1,
+						"name": "Test assignment",
+						"submission_types": ["online_text_entry"]
+					}
+				],
+				"courses": [
+					{
+						"id": 10,
+						"name": "Dev Course"
+					}
+				],
+				"users": [
+					{
+						"id": 100,
+						"name": "John Smith"
+					},
+					{
+						"id": 200,
+						"name": "Billy Smith"
+					}
+				]
+			}
+		},
+		"status_code": 200
+	},
 	"get_outcome_import_status":{
 		"method" : "GET",
 		"endpoint" : "courses/1/outcome_imports/1",
diff --git a/tests/fixtures/user.json b/tests/fixtures/user.json
index 5c49be59..09e40e4b 100644
--- a/tests/fixtures/user.json
+++ b/tests/fixtures/user.json
@@ -824,6 +824,168 @@
 		],
 		"status_code": 200
 	},
+	"get_grade_change_events_for_student": {
+		"method": "GET",
+		"endpoint": "audit/grade_change/students/1",
+		"data": {
+			"links": {
+				"events.assignment": "https://canvas.com/api/v1/assignment/test",
+				"events.course": "https://canvas.com/api/v1/assignment/course",
+				"events.student": {
+					"href": null,
+					"type": "user"
+				},
+				"events.grader": {
+					"href": null,
+					"type": "user"
+				},
+				"events.page_view": null
+			},
+			"events": [
+				{
+					"id": "dd5-6a",
+					"event_type": "grade_change",
+					"grade_before": "5",
+					"grade_after": "2",
+					"version_number": 2,
+					"links": {
+						"assignment": 10,
+						"course": 1,
+						"student": 100,
+						"grader": 200,
+						"page_view": null
+					}
+				},
+				{
+					"id": "fg-43",
+					"created_at": "2019-10-25T14:08:46Z",
+					"event_type": "grade_change",
+					"grade_before": null,
+					"grade_after": "5",
+					"version_number": 1,
+					"links": {
+						"assignment": 10,
+						"course": 1,
+						"student": 100,
+						"grader": 200,
+						"page_view": null
+					}
+				}
+			],
+			"linked": {
+				"page_views": [],
+				"assignments": [
+					{
+						"id": 10,
+						"description": "This is a test assignment",
+						"points_possible": 10.0,
+						"grading_type": "points",
+						"course_id": 1,
+						"name": "Test assignment",
+						"submission_types": ["online_text_entry"]
+					}
+				],
+				"courses": [
+					{
+						"id": 10,
+						"name": "Dev Course"
+					}
+				],
+				"users": [
+					{
+						"id": 100,
+						"name": "John Smith"
+					},
+					{
+						"id": 200,
+						"name": "Billy Smith"
+					}
+				]
+			}
+		},
+		"status_code": 200
+	},
+	"get_grade_change_events_for_grader": {
+		"method": "GET",
+		"endpoint": "audit/grade_change/graders/1",
+		"data": {
+			"links": {
+				"events.assignment": "https://canvas.com/api/v1/assignment/test",
+				"events.course": "https://canvas.com/api/v1/assignment/course",
+				"events.student": {
+					"href": null,
+					"type": "user"
+				},
+				"events.grader": {
+					"href": null,
+					"type": "user"
+				},
+				"events.page_view": null
+			},
+			"events": [
+				{
+					"id": "dd5-6a",
+					"event_type": "grade_change",
+					"grade_before": "5",
+					"grade_after": "2",
+					"version_number": 2,
+					"links": {
+						"assignment": 10,
+						"course": 1,
+						"student": 100,
+						"grader": 200,
+						"page_view": null
+					}
+				},
+				{
+					"id": "fg-43",
+					"created_at": "2019-10-25T14:08:46Z",
+					"event_type": "grade_change",
+					"grade_before": null,
+					"grade_after": "5",
+					"version_number": 1,
+					"links": {
+						"assignment": 10,
+						"course": 1,
+						"student": 100,
+						"grader": 200,
+						"page_view": null
+					}
+				}
+			],
+			"linked": {
+				"page_views": [],
+				"assignments": [
+					{
+						"id": 10,
+						"description": "This is a test assignment",
+						"points_possible": 10.0,
+						"grading_type": "points",
+						"course_id": 1,
+						"name": "Test assignment",
+						"submission_types": ["online_text_entry"]
+					}
+				],
+				"courses": [
+					{
+						"id": 10,
+						"name": "Dev Course"
+					}
+				],
+				"users": [
+					{
+						"id": 100,
+						"name": "John Smith"
+					},
+					{
+						"id": 200,
+						"name": "Billy Smith"
+					}
+				]
+			}
+		},
+		"status_code": 200
+	},
 	"get_migration_systems_multiple": {
 		"method": "GET",
 		"endpoint": "users/1/content_migrations/migrators",
diff --git a/tests/test_assignment.py b/tests/test_assignment.py
index b86314b6..28f36deb 100644
--- a/tests/test_assignment.py
+++ b/tests/test_assignment.py
@@ -12,6 +12,8 @@
     AssignmentOverride,
 )
 from canvasapi.exceptions import CanvasException, RequiredFieldMissing
+from canvasapi.grade_change_log import GradeChangeEvent
+from canvasapi.paginated_list import PaginatedList
 from canvasapi.peer_review import PeerReview
 from canvasapi.progress import Progress
 from canvasapi.submission import Submission
@@ -84,6 +86,20 @@ def test_get_gradeable_students(self, m):
         self.assertEqual(len(student_list), 2)
         self.assertIsInstance(student_list[0], UserDisplay)
 
+    # get_grade_change_events()
+    def test_get_grade_change_events(self, m):
+        register_uris({"assignment": ["get_grade_change_events"]}, m)
+
+        response = self.assignment.get_grade_change_events()
+
+        self.assertIsInstance(response, PaginatedList)
+        self.assertEqual(len([event for event in response]), 2)
+
+        for event in response:
+            self.assertEqual(event.links["course"], self.assignment.id)
+            self.assertIsInstance(event, GradeChangeEvent)
+            self.assertEqual(event.event_type, "grade_change")
+
     # get_override()
     def test_get_override(self, m):
         register_uris({"assignment": ["get_assignment_override"]}, m)
diff --git a/tests/test_course.py b/tests/test_course.py
index aeb58be3..915d8f5a 100644
--- a/tests/test_course.py
+++ b/tests/test_course.py
@@ -22,6 +22,7 @@
 from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.file import File
 from canvasapi.folder import Folder
+from canvasapi.grade_change_log import GradeChangeEvent
 from canvasapi.gradebook_history import (
     Day,
     Grader,
@@ -1604,6 +1605,20 @@ def test_get_grading_periods(self, m):
         self.assertEqual(response[0].title, "Grading period 1")
         self.assertEqual(response[1].title, "Grading period 2")
 
+    # get_grade_change_events()
+    def test_get_grade_change_events(self, m):
+        register_uris({"course": ["get_grade_change_events"]}, m)
+
+        response = self.course.get_grade_change_events()
+
+        self.assertIsInstance(response, PaginatedList)
+        self.assertEqual(len([event for event in response]), 2)
+
+        for event in response:
+            self.assertEqual(event.links["course"], self.course.id)
+            self.assertIsInstance(event, GradeChangeEvent)
+            self.assertEqual(event.event_type, "grade_change")
+
     # get_grading_period()
     def test_get_grading_period(self, m):
         register_uris({"course": ["get_grading_period"]}, m)
diff --git a/tests/test_grade_change_log.py b/tests/test_grade_change_log.py
new file mode 100644
index 00000000..c8266060
--- /dev/null
+++ b/tests/test_grade_change_log.py
@@ -0,0 +1,27 @@
+import unittest
+
+import requests_mock
+
+from canvasapi import Canvas
+from tests import settings
+from tests.util import register_uris
+
+
+@requests_mock.Mocker()
+class TestGradeChangeEvent(unittest.TestCase):
+    def setUp(self):
+        self.canvas = Canvas(settings.BASE_URL, settings.API_KEY)
+
+        with requests_mock.Mocker() as m:
+            requires = {"course": ["get_by_id"]}
+            register_uris(requires, m)
+
+            self.course = self.canvas.get_course(1)
+
+    # __str__()
+    def test__str__(self, m):
+        requires = {"course": ["get_grade_change_events"]}
+        register_uris(requires, m)
+
+        for event in self.course.get_grade_change_events():
+            self.assertIsInstance(str(event), str)
diff --git a/tests/test_user.py b/tests/test_user.py
index 2234e0ad..fce4896c 100644
--- a/tests/test_user.py
+++ b/tests/test_user.py
@@ -16,6 +16,7 @@
 from canvasapi.feature import Feature, FeatureFlag
 from canvasapi.file import File
 from canvasapi.folder import Folder
+from canvasapi.grade_change_log import GradeChangeEvent
 from canvasapi.license import License
 from canvasapi.login import Login
 from canvasapi.page_view import PageView
@@ -72,6 +73,34 @@ def test_get_courses(self, m):
         self.assertEqual(len(course_list), 4)
         self.assertIsInstance(course_list[0], Course)
 
+    # get_grade_change_events_for_student()
+    def test_get_grade_change_events_for_student(self, m):
+        register_uris({"user": ["get_grade_change_events_for_student"]}, m)
+
+        response = self.user.get_grade_change_events_for_student()
+
+        self.assertIsInstance(response, PaginatedList)
+        self.assertEqual(len([event for event in response]), 2)
+
+        for event in response:
+            self.assertEqual(event.links["course"], self.user.id)
+            self.assertIsInstance(event, GradeChangeEvent)
+            self.assertEqual(event.event_type, "grade_change")
+
+    # get_grade_change_events_for_grader()
+    def test_get_grade_change_events_for_grader(self, m):
+        register_uris({"user": ["get_grade_change_events_for_grader"]}, m)
+
+        response = self.user.get_grade_change_events_for_grader()
+
+        self.assertIsInstance(response, PaginatedList)
+        self.assertEqual(len([event for event in response]), 2)
+
+        for event in response:
+            self.assertEqual(event.links["course"], self.user.id)
+            self.assertIsInstance(event, GradeChangeEvent)
+            self.assertEqual(event.event_type, "grade_change")
+
     # get_missing_submissions()
     def test_get_missing_submissions(self, m):
         register_uris({"user": ["missing_sub", "missing_sub_p2"]}, m)